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() { resetAdminPassword := flag.Bool("reset-admin-password", false, "Reset admin password to a random value and exit") 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.Usage = func() { fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options]\n\nOptions:\n", os.Args[0]) flag.PrintDefaults() } 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 *resetAdminPassword { password := generateRandomPassword(16) fmt.Printf("Generated password: %s\n", password) hash, err := auth.HashPassword(password) if err != nil { log.Fatalf("Failed to hash password: %v", err) } if err := database.EnsureAdminUser("admin", hash); err != nil { log.Fatalf("Failed to reset admin password: %v", err) } fmt.Println("Admin password reset successfully.") return } 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()) router.HandleFunc("/favicon.png", serveStaticFile(frontendFS, "embed/favicon.png")) router.Handle("/_app/", http.StripPrefix("/_app/", serveStaticPrefixed(frontendFS, "embed/_app/"))) router.HandleFunc("/", serveSPA(frontendFS)) 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 { log.Fatalf("Failed to generate random password: %v", err) } 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" } }