package main import ( "bufio" "flag" "log" "os" "os/signal" "strconv" "strings" "syscall" "time" "server/internal/config" "server/internal/db" "server/internal/migrations" "server/internal/roles" "server/internal/routes" "server/internal/tokens" "server/internal/seed" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/middleware/cors" "github.com/gofiber/fiber/v3/middleware/logger" "github.com/gofiber/fiber/v3/middleware/recover" "github.com/gofiber/fiber/v3/middleware/requestid" ) // Typescript: TSDeclaration= Nullable = T | null; // Typescript: TSDeclaration= Record = { [P in K]: T; } func main() { loadDotEnv(".env") seedCount := flag.Int("seed", 0, "seed N fake users at startup (0 to skip)") flag.Parse() cfg, err := config.GetConfig() if err != nil { log.Fatalf("config: %v", err) } dbConn, err := db.GetDB() if err != nil { log.Fatalf("init db: %v", err) } if err := migrations.AutoMigrate(dbConn); err != nil { log.Fatalf("migrate user: %v", err) } tokenService, err := tokens.GetTockenService() if err != nil { log.Fatalf("init tokens: %v", err) } app := fiber.New(fiber.Config{ AppName: cfg.AppName, ReadTimeout: time.Duration(cfg.ReadTimeoutSeconds) * time.Second, WriteTimeout: time.Duration(cfg.WriteTimeoutSeconds) * time.Second, IdleTimeout: time.Duration(cfg.IdleTimeoutSeconds) * time.Second, ErrorHandler: func(c fiber.Ctx, err error) error { code := fiber.StatusInternalServerError msg := "internal server error: " + err.Error() if e, ok := err.(*fiber.Error); ok { code = e.Code msg = e.Message } reqID := requestid.FromContext(c) log.Printf("error request_id=%s status=%d method=%s path=%s ip=%s ua=%q err=%v", reqID, code, c.Method(), c.Path(), c.IP(), c.Get("User-Agent"), err) if code >= 500 { msg = "internal server error: " + err.Error() } return c.Status(code).JSON(fiber.Map{"data": nil, "error": msg}) }, }) app.Use(requestid.New()) app.Use(recover.New()) app.Use(logger.New()) app.Use(cors.New(cors.Config{ AllowOrigins: []string{"http://localhost:9000"}, AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, AllowHeaders: []string{"Origin", "Auth-Token", "cache-control", "Content-Type", "Accept", "Authorization"}, AllowCredentials: true, ExposeHeaders: []string{"Auth-Token"}, })) app.Options("/*", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusNoContent) }) // Make the DB handle available in request context for future handlers. app.Use(func(c fiber.Ctx) error { c.Locals("db", dbConn) return c.Next() }) app.Use(roles.RequireEndpointPermission(dbConn, tokenService)) routes.Register(app) port := envOrDefault("PORT", "3000") seedToRun := *seedCount if seedToRun <= 0 { seedToRun = envIntOrDefault("SEED", 0) } if seedToRun > 0 { _, creds, err := seed.SeedUsers(dbConn, seedToRun) if err != nil { log.Fatalf("seed users: %v", err) } for _, cred := range creds { log.Printf("seeded user email=%s password=%s", cred.Email, cred.Password) } } // Graceful shutdown so ctrl+c or SIGTERM stops the server cleanly. go func() { if err := app.Listen(":" + port); err != nil { log.Fatalf("server failed: %v", err) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("Shutting down server...") if err := app.Shutdown(); err != nil { log.Printf("shutdown error: %v", err) } } func envOrDefault(key, fallback string) string { if value := os.Getenv(key); value != "" { return value } return fallback } func envIntOrDefault(key string, fallback int) int { value := os.Getenv(key) if value == "" { return fallback } parsed, err := strconv.Atoi(value) if err != nil { log.Printf("warning: invalid %s=%q, using default %d", key, value, fallback) return fallback } return parsed } func loadDotEnv(path string) { file, err := os.Open(path) if err != nil { return } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } parts := strings.SplitN(line, "=", 2) if len(parts) != 2 { continue } key := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) if key == "" { continue } if _, exists := os.LookupEnv(key); !exists { _ = os.Setenv(key, value) } } }