Add comprehensive human-readable comments to all Go files
- Comments explain what each function does and why - Fixed unused strconv import in queue.go - Better documentation for SMTP email service - Explained journalctl log parsing in logs.go - Clarified queue operations in queue.go
This commit is contained in:
parent
3e3620a24f
commit
68285d861a
9 changed files with 728 additions and 203 deletions
|
|
@ -1,33 +1,60 @@
|
|||
// 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"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"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"
|
||||
"github.com/imc-vibe/backend/internal/auth"
|
||||
"github.com/imc-vibe/backend/internal/config"
|
||||
"github.com/imc-vibe/backend/internal/db"
|
||||
"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
|
||||
}
|
||||
|
|
@ -35,93 +62,172 @@ func main() {
|
|||
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)
|
||||
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)
|
||||
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
|
||||
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())
|
||||
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)
|
||||
|
||||
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()
|
||||
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)
|
||||
|
|
@ -143,15 +249,25 @@ func serveStaticFile(fsys http.FileSystem, filename string) http.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
|
@ -162,12 +278,19 @@ func serveSPA(fsys http.FileSystem) http.HandlerFunc {
|
|||
}
|
||||
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"
|
||||
|
|
@ -194,6 +317,7 @@ func mimeType(path string) string {
|
|||
case strings.HasSuffix(path, ".eot"):
|
||||
return "application/vnd.ms-fontobject"
|
||||
default:
|
||||
// Fallback for unknown types - treat as plain text.
|
||||
return "text/plain"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue