package main import ( "crypto/tls" "encoding/json" "fmt" "io" "log" "net/http" "net/http/httputil" "net/url" "os" "os/exec" "path/filepath" "strings" "time" "github.com/gorilla/mux" "gopkg.in/gomail.v2" ) type App struct { ID string `json:"id"` Name string `json:"name"` URL string `json:"url"` Description string `json:"description,omitempty"` Icon string `json:"icon,omitempty"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } type TripEntry struct { Name string `json:"name"` Vehicle string `json:"vehicle"` Destination string `json:"destination"` DateStart string `json:"date_start"` TimeStart string `json:"time_start"` DateEnd string `json:"date_end"` TimeEnd string `json:"time_end"` Purpose string `json:"purpose"` KmStart int `json:"km_start"` KmEnd int `json:"km_end"` Coordinates *GeoCoords `json:"coordinates,omitempty"` } type GeoCoords struct { Lat string `json:"lat"` Lng string `json:"lng"` } func main() { log.SetFlags(log.LstdFlags | log.Lshortfile) // Create necessary directories if err := os.MkdirAll("data", 0755); err != nil { log.Fatalf("Failed to create data directory: %v", err) } if err := os.MkdirAll("uploads", 0755); err != nil { log.Fatalf("Failed to create uploads directory: %v", err) } r := mux.NewRouter() // Set up reverse proxy to kontakt service kontaktURL, _ := url.Parse("http://webportal:8080") kontaktProxy := httputil.NewSingleHostReverseProxy(kontaktURL) // Public routes r.PathPrefix("/kontakt/").Handler(http.StripPrefix("/kontakt", kontaktProxy)) r.PathPrefix("/uploads/").Handler(http.StripPrefix("/uploads/", http.FileServer(http.Dir("./uploads")))) r.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"status":"ok"}`)) }).Methods("GET", "OPTIONS") // Authentication routes r.HandleFunc("/api/login", LoginHandler).Methods("POST", "OPTIONS") // Protected API routes api := r.PathPrefix("/api").Subrouter() api.Use(AuthMiddleware) api.HandleFunc("/submit", handleSubmit).Methods("POST") api.HandleFunc("/banner/update", UpdateBannerHandler).Methods("POST", "OPTIONS") // App management routes api.HandleFunc("/apps", GetAppsHandler).Methods("GET") api.HandleFunc("/apps", CreateAppHandler).Methods("POST") api.HandleFunc("/apps/{id}", GetAppHandler).Methods("GET") api.HandleFunc("/apps/{id}", UpdateAppHandler).Methods("PUT") api.HandleFunc("/apps/{id}", DeleteAppHandler).Methods("DELETE") // Public endpoints r.HandleFunc("/api/banner", GetBannerHandler).Methods("GET", "OPTIONS") // Important: This public submit endpoint must be defined BEFORE the static file server r.HandleFunc("/submit", handleSubmit).Methods("POST", "OPTIONS") // Public submit endpoint for evidence-aut.html // Add CORS middleware for API r.Use(func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } next.ServeHTTP(w, r) }) }) // Admin routes r.HandleFunc("/admin", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "admin.html") }).Methods("GET") r.HandleFunc("/admin/dashboard", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "admin-dashboard.html") }).Methods("GET") // Redirect root to index.html r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { http.ServeFile(w, r, "index.html") } }).Methods("GET") // Public route for evidence-aut.html r.HandleFunc("/evidence-aut", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "evidence-aut.html") }).Methods("GET") // Static file server for public files - must be the last route defined fs := http.FileServer(http.Dir(".")) r.PathPrefix("/").Handler(fs) r.HandleFunc("/kontakt", func(w http.ResponseWriter, r *http.Request) { // Check if kontakt service is already running resp, err := http.Get("http://webportal:8080/health") if err == nil && resp.StatusCode == 200 { http.Redirect(w, r, "http://webportal:8080/", http.StatusFound) return } // Start the service if not running cmd := exec.Command("make", "dev") cmd.Dir = "kontakt" err = cmd.Start() if err != nil { http.Error(w, "Failed to start kontakt service", http.StatusInternalServerError) return } // Wait briefly for service to start time.Sleep(2 * time.Second) http.Redirect(w, r, "http://webportal:8080/", http.StatusFound) }).Methods("GET") // Apply CORS middleware to all routes handler := enableCORS(r) port := os.Getenv("PORT") if port == "" { port = "80" } log.Printf("Server běží na portu %s", port) err := http.ListenAndServe(":"+port, handler) if err != nil { log.Fatalf("Chyba při spuštění serveru: %v", err) } } func enableCORS(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } next.ServeHTTP(w, r) }) } // File path for storing apps const appsFile = "data/apps.json" // loadApps loads apps from the JSON file func loadApps() ([]App, error) { var apps []App // Check if file exists if _, err := os.Stat(appsFile); os.IsNotExist(err) { // Return empty slice if file doesn't exist return []App{}, nil } // Read file data, err := os.ReadFile(appsFile) if err != nil { return nil, fmt.Errorf("error reading apps file: %v", err) } // Unmarshal JSON if err := json.Unmarshal(data, &apps); err != nil { return nil, fmt.Errorf("error parsing apps JSON: %v", err) } return apps, nil } // saveApps saves apps to the JSON file func saveApps(apps []App) error { // Create data directory if it doesn't exist if err := os.MkdirAll(filepath.Dir(appsFile), 0755); err != nil { return fmt.Errorf("error creating data directory: %v", err) } // Marshal to pretty-printed JSON data, err := json.MarshalIndent(apps, "", " ") if err != nil { return fmt.Errorf("error marshaling apps to JSON: %v", err) } // Write to file if err := os.WriteFile(appsFile, data, 0644); err != nil { return fmt.Errorf("error writing apps file: %v", err) } return nil } // App Handlers func GetAppsHandler(w http.ResponseWriter, r *http.Request) { apps, err := loadApps() if err != nil { log.Printf("Error loading apps: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } // Return empty array if no apps if apps == nil { apps = []App{} } // Add hardcoded apps hardcodedApps := []App{ { ID: "hardcoded-car", Name: "Záznam služebních jízd", URL: "/evidence-aut", Description: "Jednoduchý systém pro evidenci a správu jízd služebními vozidly.", Icon: "fa-car-side", CreatedAt: time.Now().Format(time.RFC3339), UpdatedAt: time.Now().Format(time.RFC3339), }, { ID: "hardcoded-lunch", Name: "Objednávka obědů", URL: "http://ppc-app/pwkweb2/", Description: "Portál pro objednávku a přehled firemních obědů", Icon: "fa-utensils", CreatedAt: time.Now().Format(time.RFC3339), UpdatedAt: time.Now().Format(time.RFC3339), }, { ID: "hardcoded-osticket", Name: "OSTicket", URL: "http://osticket/", Description: "Systém technické podpory a hlášení problémů", Icon: "fa-headset", CreatedAt: time.Now().Format(time.RFC3339), UpdatedAt: time.Now().Format(time.RFC3339), }, { ID: "hardcoded-kanboard", Name: "Kanboard", URL: "http://kanboard/", Description: "Správa úkolů a projektů v přehledném kanban stylu", Icon: "fa-tasks", CreatedAt: time.Now().Format(time.RFC3339), UpdatedAt: time.Now().Format(time.RFC3339), }, } // Combine hardcoded and dynamic apps allApps := append(hardcodedApps, apps...) w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(allApps); err != nil { log.Printf("Error encoding apps to JSON: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) } } func GetAppHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) appID := vars["id"] // Check if it's a hardcoded app if strings.HasPrefix(appID, "hardcoded-") { // Return 404 for non-existent hardcoded apps http.NotFound(w, r) return } // Load apps from file apps, err := loadApps() if err != nil { log.Printf("Error loading apps: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } // Find the app by ID var foundApp *App for i, app := range apps { if app.ID == appID { foundApp = &apps[i] break } } if foundApp == nil { http.NotFound(w, r) return } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(foundApp); err != nil { log.Printf("Error encoding app to JSON: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) } } func CreateAppHandler(w http.ResponseWriter, r *http.Request) { // Parse form data if err := r.ParseMultipartForm(10 << 20); err != nil { // 10 MB max file size http.Error(w, "Error parsing form data", http.StatusBadRequest) return } // Get form values name := r.FormValue("name") url := r.FormValue("url") description := r.FormValue("description") // Validate required fields if name == "" || url == "" { http.Error(w, "Name and URL are required", http.StatusBadRequest) return } // Handle file upload var iconPath string file, handler, err := r.FormFile("icon") if err == nil { defer file.Close() // Create uploads directory if it doesn't exist if err := os.MkdirAll("uploads", 0755); err != nil { log.Printf("Error creating uploads directory: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } // Generate a unique filename with original extension ext := filepath.Ext(handler.Filename) iconPath = fmt.Sprintf("icon_%d%s", time.Now().UnixNano(), ext) // Create the file f, err := os.Create(filepath.Join("uploads", iconPath)) if err != nil { log.Printf("Error creating file: %v", err) http.Error(w, "Error saving file", http.StatusInternalServerError) return } defer f.Close() // Copy the uploaded file to the created file if _, err = io.Copy(f, file); err != nil { log.Printf("Error copying file: %v", err) http.Error(w, "Error saving file", http.StatusInternalServerError) return } } else if err != http.ErrMissingFile { log.Printf("Error getting uploaded file: %v", err) http.Error(w, "Error processing file upload", http.StatusBadRequest) return } // Create a new app app := App{ ID: fmt.Sprintf("%d", time.Now().UnixNano()), Name: strings.TrimSpace(name), URL: strings.TrimSpace(url), Description: strings.TrimSpace(description), Icon: iconPath, CreatedAt: time.Now().Format(time.RFC3339), UpdatedAt: time.Now().Format(time.RFC3339), } // Load existing apps apps, err := loadApps() if err != nil { log.Printf("Error loading apps: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } // Add the new app to the list apps = append(apps, app) // Save the updated list of apps if err := saveApps(apps); err != nil { log.Printf("Error saving apps: %v", err) // Try to clean up the uploaded file if saving failed if iconPath != "" { if err := os.Remove(filepath.Join("uploads", iconPath)); err != nil { log.Printf("Error cleaning up icon file: %v", err) } } http.Error(w, "Internal server error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) if err := json.NewEncoder(w).Encode(app); err != nil { log.Printf("Error encoding app to JSON: %v", err) } } func UpdateAppHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) appID := vars["id"] // Prevent updating hardcoded apps if strings.HasPrefix(appID, "hardcoded-") { http.Error(w, "Cannot update hardcoded app", http.StatusForbidden) return } // Parse form data if err := r.ParseMultipartForm(10 << 20); err != nil { // 10 MB max file size http.Error(w, "Error parsing form data", http.StatusBadRequest) return } // Load existing apps apps, err := loadApps() if err != nil { log.Printf("Error loading apps: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } // Find the app to update var appIndex = -1 var existingApp *App for i := range apps { if apps[i].ID == appID { appIndex = i existingApp = &apps[i] break } } if appIndex == -1 { http.NotFound(w, r) return } // Get form values name := r.FormValue("name") url := r.FormValue("url") description := r.FormValue("description") // Handle file upload if a new file is provided var iconPath string file, handler, err := r.FormFile("icon") if err == nil { defer file.Close() // Create uploads directory if it doesn't exist if err := os.MkdirAll("uploads", 0755); err != nil { log.Printf("Error creating uploads directory: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } // Generate a unique filename ext := filepath.Ext(handler.Filename) iconPath = fmt.Sprintf("icon_%d%s", time.Now().UnixNano(), ext) // Create the file f, err := os.Create(filepath.Join("uploads", iconPath)) if err != nil { log.Printf("Error creating file: %v", err) http.Error(w, "Error saving file", http.StatusInternalServerError) return } defer f.Close() // Copy the uploaded file to the created file if _, err = io.Copy(f, file); err != nil { log.Printf("Error copying file: %v", err) http.Error(w, "Error saving file", http.StatusInternalServerError) return } // Remove old icon file if it exists and is not used by other apps if existingApp.Icon != "" { oldIconPath := filepath.Join("uploads", existingApp.Icon) if _, err := os.Stat(oldIconPath); err == nil { // Check if any other app is using this icon iconInUse := false for _, a := range apps { if a.ID != appID && a.Icon == existingApp.Icon { iconInUse = true break } } // Delete the old icon if not in use if !iconInUse { if err := os.Remove(oldIconPath); err != nil { log.Printf("Error removing old icon: %v", err) } } } } } else if err != http.ErrMissingFile { log.Printf("Error getting uploaded file: %v", err) http.Error(w, "Error processing file upload", http.StatusBadRequest) return } else { // Keep the existing icon if no new file was uploaded iconPath = existingApp.Icon } // Update the app updatedApp := App{ ID: appID, Name: name, URL: url, Description: description, Icon: iconPath, CreatedAt: existingApp.CreatedAt, // Keep the original creation time UpdatedAt: time.Now().Format(time.RFC3339), } // Update the app in the slice apps[appIndex] = updatedApp // Save the updated apps if err := saveApps(apps); err != nil { log.Printf("Error saving apps: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(updatedApp); err != nil { log.Printf("Error encoding app to JSON: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) } } func DeleteAppHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) appID := vars["id"] // Prevent deleting hardcoded apps if strings.HasPrefix(appID, "hardcoded-") { http.Error(w, "Cannot delete hardcoded app", http.StatusForbidden) return } // Load existing apps apps, err := loadApps() if err != nil { log.Printf("Error loading apps: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } // Find the app to delete var appIndex = -1 var iconToDelete string for i, app := range apps { if app.ID == appID { appIndex = i iconToDelete = app.Icon break } } if appIndex == -1 { http.NotFound(w, r) return } // Remove the app from the slice updatedApps := append(apps[:appIndex], apps[appIndex+1:]...) // Save the updated apps if err := saveApps(updatedApps); err != nil { log.Printf("Error saving apps: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } // If the app had an icon, check if it's used by any other app before deleting if iconToDelete != "" { // Check if any other app is using this icon iconInUse := false for _, app := range updatedApps { if app.Icon == iconToDelete { iconInUse = true break } } // Delete the icon file if it's not in use if !iconInUse { iconPath := filepath.Join("uploads", iconToDelete) if _, err := os.Stat(iconPath); err == nil { if err := os.Remove(iconPath); err != nil { log.Printf("Error deleting icon file: %v", err) // Continue even if we can't delete the file } } } } w.WriteHeader(http.StatusNoContent) } func handleSubmit(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if r.Method != http.MethodPost { if r.Method == http.MethodOptions { w.WriteHeader(http.StatusOK) return } w.WriteHeader(http.StatusMethodNotAllowed) w.Write([]byte(`{"error":"Only POST method is allowed"}`)) return } body, err := io.ReadAll(r.Body) if err != nil { log.Printf("Chyba při čtení těla požadavku: %v", err) w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"Failed to read request body"}`)) return } defer r.Body.Close() log.Printf("Přijatá data: %s", string(body)) var entry TripEntry err = json.Unmarshal(body, &entry) if err != nil { log.Printf("Chyba při parsování JSON: %v", err) w.WriteHeader(http.StatusBadRequest) w.Write([]byte(fmt.Sprintf(`{"error":"Failed to parse JSON: %v"}`, err))) return } if entry.Name == "" || entry.Destination == "" || entry.DateStart == "" || entry.DateEnd == "" || entry.Purpose == "" { log.Printf("Chybějící povinná pole: %+v", entry) w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"Missing required fields"}`)) return } if entry.KmEnd < entry.KmStart { log.Printf("Neplatný stav tachometru: %d -> %d", entry.KmStart, entry.KmEnd) w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"End kilometers must be greater than or equal to start kilometers"}`)) return } // Formátování dat do českého formátu czechMonths := []string{ "ledna", "února", "března", "dubna", "května", "června", "července", "srpna", "září", "října", "listopadu", "prosince", } // Zpracování začátku cesty parsedDateStart, err := time.Parse("2006-01-02", entry.DateStart) if err != nil { log.Printf("Chyba při parsování data začátku: %v", err) } // Zpracování konce cesty parsedDateEnd, err := time.Parse("2006-01-02", entry.DateEnd) if err != nil { log.Printf("Chyba při parsování data konce: %v", err) } err = sendEmail(entry, parsedDateStart, parsedDateEnd, czechMonths) if err != nil { log.Printf("Chyba při odesílání emailu: %v", err) w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(fmt.Sprintf(`{"error":"Failed to send email: %v"}`, err))) return } w.WriteHeader(http.StatusOK) w.Write([]byte(`{"message":"Záznam byl úspěšně uložen a email odeslán"}`)) } func sendEmail(entry TripEntry, parsedDateStart, parsedDateEnd time.Time, czechMonths []string) error { smtpHost := "mail.pp-kunovice.cz" smtpPort := 465 sender := "sluzebnicek@pp-kunovice.cz" password := "7g}qznB5bj" recipient := "sluzebnicek@pp-kunovice.cz" m := gomail.NewMessage() m.SetHeader("From", sender) m.SetHeader("To", recipient) m.SetHeader("Subject", "Nový záznam o jízdě služebním autem") var htmlContent strings.Builder htmlContent.WriteString(`