go-quasar-partial-ssr/backend/cmd/server/main.go

191 lines
4.6 KiB
Go

package main
import (
"bufio"
"flag"
"log"
"os"
"os/signal"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
"server/internal/config"
"server/internal/db"
"server/internal/middleware"
"server/internal/migrations"
"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> = T | null;
// Typescript: TSDeclaration= Record<K extends string | number | symbol, T> = { [P in K]: T; }
const spaDistPath = "http/static/spa"
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()
})
api := app.Group("/api", middleware.GetAuthClaims(dbConn, tokenService))
routes.Register(api)
app.Get("/", func(c fiber.Ctx) error {
return c.SendFile(filepath.Join(spaDistPath, "index.html"))
})
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)
}
}
}