package main import ( "crypto/rand" "flag" "fmt" "log" "net/http" "os" "strings" "github.com/imc-vibe/backend/internal/api" "github.com/imc-vibe/backend/internal/auth" "github.com/imc-vibe/backend/internal/config" "github.com/imc-vibe/backend/internal/db" ) func main() { setup := flag.Bool("setup", false, "Create admin user and exit") adminUser := flag.String("admin-user", "admin", "Admin username for setup") adminPassword := flag.String("admin-password", "", "Admin password for setup (required with --setup)") bind := flag.String("bind", "", "IP address to bind to (default: 0.0.0.0)") port := flag.String("port", "", "Port to listen on (default: 8080)") flag.Parse() cfg := config.Load() if *bind != "" { cfg.Bind = *bind } if *port != "" { cfg.Port = *port } database, err := db.Connect(cfg) if err != nil { log.Fatalf("Failed to connect to database: %v", err) } if err := database.InitSchema(); err != nil { log.Printf("Warning: Could not initialize schema: %v", err) } if *setup { password := *adminPassword if password == "" { password = generateRandomPassword(16) fmt.Printf("Generated random password: %s\n", password) } hash, err := auth.HashPassword(password) if err != nil { log.Fatalf("Failed to hash password: %v", err) } if err := database.EnsureAdminUser(*adminUser, hash); err != nil { log.Fatalf("Failed to create admin user: %v", err) } fmt.Printf("Admin user '%s' created successfully.\n", *adminUser) fmt.Printf("You can now login with username: %s\n", *adminUser) fmt.Printf("Password: %s\n", password) return } if cfg.AdminUser != "" && cfg.AdminPassword != "" { hash, err := auth.HashPassword(cfg.AdminPassword) if err != nil { log.Printf("Warning: Failed to hash admin password: %v", err) } else { if err := database.EnsureAdminUser(cfg.AdminUser, hash); err != nil { log.Printf("Warning: Failed to create admin user: %v", err) } else { log.Printf("Admin user '%s' created or already exists", cfg.AdminUser) } } } useEmbedded := os.Getenv("USE_EMBEDDED") != "false" frontendFS := FrontendFileSystem() router := http.NewServeMux() router.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"status":"ok"}`)) }) apiRouter := api.New(database, cfg) router.Handle("/api/", apiRouter.Handler()) if useEmbedded { router.HandleFunc("/favicon.png", serveStaticFile(frontendFS, "embed/favicon.png")) router.Handle("/_app/", http.StripPrefix("/_app/", serveStaticPrefixed(frontendFS, "embed/_app/"))) router.HandleFunc("/", serveSPA(frontendFS)) log.Println("Using embedded frontend") } else { router.Handle("/", http.FileServer(http.Dir("../frontend/build"))) log.Println("Using filesystem frontend") } addr := fmt.Sprintf("%s:%s", cfg.Bind, cfg.Port) log.Printf("Server starting on http://%s", addr) log.Fatal(http.ListenAndServe(addr, router)) } func generateRandomPassword(length int) string { const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*" result := make([]byte, length) if _, err := rand.Read(result); err != nil { for i := range result { result[i] = charset[i%len(charset)] } return string(result) } for i := range result { result[i] = charset[int(result[i])%len(charset)] } return string(result) } func serveStaticPrefixed(fsys http.FileSystem, prefix string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { path := prefix + r.URL.Path file, err := fsys.Open(path) if err != nil { http.NotFound(w, r) return } defer file.Close() fi, err := file.Stat() if err != nil { http.NotFound(w, r) return } if fi.IsDir() { http.NotFound(w, r) return } contentType := mimeType(path) w.Header().Set("Content-Type", contentType) http.ServeContent(w, r, fi.Name(), fi.ModTime(), file) } } func serveStaticFile(fsys http.FileSystem, filename string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { file, err := fsys.Open(filename) if err != nil { http.NotFound(w, r) return } defer file.Close() fi, err := file.Stat() if err != nil { http.NotFound(w, r) return } contentType := mimeType(filename) w.Header().Set("Content-Type", contentType) http.ServeContent(w, r, fi.Name(), fi.ModTime(), file) } } func serveSPA(fsys http.FileSystem) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path if strings.HasPrefix(path, "/api/") { http.NotFound(w, r) return } path = "embed/index.html" file, err := fsys.Open(path) if err != nil { log.Printf("Error opening %s: %v", path, err) http.Error(w, "Not found", http.StatusNotFound) return } defer file.Close() fi, _ := file.Stat() w.Header().Set("Content-Type", "text/html; charset=utf-8") http.ServeContent(w, r, fi.Name(), fi.ModTime(), file) } } func mimeType(path string) string { switch { case strings.HasSuffix(path, ".html"): return "text/html; charset=utf-8" case strings.HasSuffix(path, ".css"): return "text/css" case strings.HasSuffix(path, ".js"): return "application/javascript" case strings.HasSuffix(path, ".json"): return "application/json" case strings.HasSuffix(path, ".png"): return "image/png" case strings.HasSuffix(path, ".jpg") || strings.HasSuffix(path, ".jpeg"): return "image/jpeg" case strings.HasSuffix(path, ".svg"): return "image/svg+xml" case strings.HasSuffix(path, ".ico"): return "image/x-icon" case strings.HasSuffix(path, ".woff"): return "font/woff" case strings.HasSuffix(path, ".woff2"): return "font/woff2" case strings.HasSuffix(path, ".ttf"): return "font/ttf" case strings.HasSuffix(path, ".eot"): return "application/vnd.ms-fontobject" default: return "text/plain" } }