// Package main is the entry point for the imc-vibe mail server admin application. // This single binary contains both the Go backend API and the embedded SvelteKit frontend. package main import ( "crypto/rand" // cryptographically secure random number generator "flag" // standard library for parsing command-line flags "fmt" // formatted I/O, used here for printing output "log" // logging package "net/http" // HTTP server and client "os" // OS-level operations like reading command-line args "strings" // string manipulation utilities "github.com/imc-vibe/backend/internal/api" // HTTP API routing and handlers "github.com/imc-vibe/backend/internal/auth" // JWT authentication "github.com/imc-vibe/backend/internal/config" // configuration loading "github.com/imc-vibe/backend/internal/db" // database connection and operations ) // main is the entry point of the application. // It handles command-line flags, initializes the database, sets up the HTTP router, // and starts the web server. func main() { // Parse command-line flags. // Flags are optional arguments passed after the program name. // Example: ./imc-vibe --bind=0.0.0.0 --port=8080 // --reset-admin-password: If set, generate a random password for the admin user and exit. // Useful for initial setup or recovering from a lost password. resetAdminPassword := flag.Bool("reset-admin-password", false, "Reset admin password to a random value and exit") // --bind: The IP address the server should listen on. // 0.0.0.0 means listen on all network interfaces (accessible from other machines). // 127.0.0.1 means listen only on localhost (accessible only from this machine). bind := flag.String("bind", "", "IP address to bind to (default: 0.0.0.0)") // --port: The TCP port the server should listen on. // Common ports: 80 (HTTP), 443 (HTTPS), 8080 (HTTP alternate). port := flag.String("port", "", "Port to listen on (default: 8080)") // Customize the help output to show double-hyphen style (Unix convention). // By default, Go's flag package uses single hyphens. flag.Usage = func() { fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options]\n\nOptions:\n", os.Args[0]) flag.PrintDefaults() } // Actually parse the flags from command line arguments. flag.Parse() // Load configuration from environment variables. // Environment variables take precedence over code defaults. // See config.Load() for all available environment variables. cfg := config.Load() // Override configuration with command-line flags if they were provided. // Empty string means the flag was not set, so we keep the config value. if *bind != "" { cfg.Bind = *bind } if *port != "" { cfg.Port = *port } // Connect to the database (MariaDB/MySQL). // The database stores both ISPmail data (virtual_users, virtual_domains, etc.) // and imc-vibe's own data (imc_users, imc_users2domains, etc.). database, err := db.Connect(cfg) if err != nil { log.Fatalf("Failed to connect to database: %v", err) // Fatal = print and exit } // Create database tables if they don't exist. // This only creates imc-vibe's own tables, not the ISPmail tables. if err := database.InitSchema(); err != nil { log.Printf("Warning: Could not initialize schema: %v", err) // Non-fatal = continue } // Handle --reset-admin-password flag. // When this flag is set, we generate a random password, update the database, and exit. // We don't start the web server in this case. if *resetAdminPassword { // Generate a cryptographically secure random password. // The password will be 16 characters long, containing letters, numbers, and symbols. password := generateRandomPassword(16) fmt.Printf("Generated password: %s\n", password) // Hash the password before storing it. // We never store plain-text passwords, only their hashes. // This uses bcrypt, which is a slow hashing algorithm designed to resist brute-force attacks. hash, err := auth.HashPassword(password) if err != nil { log.Fatalf("Failed to hash password: %v", err) } // Store or update the admin user in the database. // EnsureAdminUser creates the user if it doesn't exist, or updates the password if it does. if err := database.EnsureAdminUser("admin", hash); err != nil { log.Fatalf("Failed to reset admin password: %v", err) } fmt.Println("Admin password reset successfully.") return // Exit the program } // Start the web server. // From here on, we set up the HTTP router and handlers. // Get the embedded filesystem containing the frontend. // The frontend is compiled into the binary during build time. frontendFS := FrontendFileSystem() // Create a new HTTP request multiplexer (router). // The router maps URL paths to handler functions. router := http.NewServeMux() // Add health check endpoint. // This is useful for load balancers and monitoring systems. // Returns {"status":"ok"} with HTTP 200 status. router.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"status":"ok"}`)) }) // Set up the API router for all /api/* endpoints. // The API router handles authentication, domain management, user management, etc. apiRouter := api.New(database, cfg) router.Handle("/api/", apiRouter.Handler()) // The trailing slash is important! // Serve static files from the embedded frontend. // These routes handle the SvelteKit frontend application. // Serve the favicon at /favicon.png router.HandleFunc("/favicon.png", serveStaticFile(frontendFS, "embed/favicon.png")) // Serve SvelteKit's hashed assets (JavaScript, CSS) at /_app/* // The StripPrefix removes "/_app" from the URL before looking up the file. router.Handle("/_app/", http.StripPrefix("/_app/", serveStaticPrefixed(frontendFS, "embed/_app/"))) // Serve the main HTML file for all other routes. // This is a Single Page Application (SPA) router - all routes serve index.html, // and JavaScript handles showing the correct page. router.HandleFunc("/", serveSPA(frontendFS)) // Build the address string for binding: "IP:PORT" // Example: "0.0.0.0:8080" or "127.0.0.1:8080" addr := fmt.Sprintf("%s:%s", cfg.Bind, cfg.Port) // Log the startup message. log.Printf("Server starting on http://%s", addr) // Start the HTTP server. // ListenAndServe blocks until the server is stopped. // log.Fatal prints the error and exits if the server can't start. log.Fatal(http.ListenAndServe(addr, router)) } // generateRandomPassword creates a cryptographically secure random password. // length: how many characters the password should have. // Returns: a string containing random characters from a safe charset. func generateRandomPassword(length int) string { // Characters that are safe to use in passwords. // We avoid ambiguous characters like 0/O and 1/l/I. const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*" // Create a byte slice to hold random bytes. result := make([]byte, length) // Use crypto/rand for cryptographically secure randomness. // This is better than math/rand for security-sensitive applications. if _, err := rand.Read(result); err != nil { log.Fatalf("Failed to generate random password: %v", err) } // Convert random bytes to characters from our charset. // We use modulo to map bytes to charset indices. // This isn't perfectly uniform distribution, but is good enough for passwords. for i := range result { result[i] = charset[int(result[i])%len(charset)] } return string(result) // Convert bytes to string } // serveStaticPrefixed serves static files from a specific prefix in the embedded filesystem. // fsys: the embedded filesystem containing the files. // prefix: the directory prefix, e.g., "embed/_app/" for SvelteKit assets. // Returns: an HTTP handler function. func serveStaticPrefixed(fsys http.FileSystem, prefix string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Build the full path by combining prefix and URL path. // Example: prefix="embed/_app/", URL="/_app/chunk.js" -> "embed/_app/chunk.js" path := prefix + r.URL.Path // Try to open the file from the embedded filesystem. file, err := fsys.Open(path) if err != nil { // File not found - return 404. http.NotFound(w, r) return } defer file.Close() // Always close the file, even if there's an error later // Get file information (size, modification time, etc.). fi, err := file.Stat() if err != nil { http.NotFound(w, r) return } // Don't serve directories - return 404. if fi.IsDir() { http.NotFound(w, r) return } // Set the Content-Type header based on file extension. contentType := mimeType(path) w.Header().Set("Content-Type", contentType) // Serve the file content. // ServeContent handles Range requests (for video/audio), caching headers, etc. http.ServeContent(w, r, fi.Name(), fi.ModTime(), file) } } // serveStaticFile serves a single static file at a fixed path. // fsys: the embedded filesystem. // filename: the exact path to the file, e.g., "embed/favicon.png". // Returns: an HTTP handler function. 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) } } // serveSPA serves the Single Page Application (SvelteKit frontend). // It always serves index.html for any route that isn't an API call. // The JavaScript running in the browser then handles routing to the correct page. // fsys: the embedded filesystem containing the frontend files. // Returns: an HTTP handler function. func serveSPA(fsys http.FileSystem) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path // If this looks like an API call, something is wrong - return 404. // The API router should have handled /api/* routes. // If we get here, the request slipped through somehow. if strings.HasPrefix(path, "/api/") { http.NotFound(w, r) return } // For SPA, always serve index.html regardless of the requested path. // This allows client-side routing (e.g., /domains/example.org). 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() // Tell the browser this is HTML. w.Header().Set("Content-Type", "text/html; charset=utf-8") http.ServeContent(w, r, fi.Name(), fi.ModTime(), file) } } // mimeType returns the MIME type for a file based on its extension. // MIME types tell the browser what kind of file it is. // This is important for browsers to correctly interpret files. func mimeType(path string) string { // Use switch statement with multiple conditions (Go's patterns work like regex). // Check file extension and return appropriate MIME type. 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: // Fallback for unknown types - treat as plain text. return "text/plain" } }