Files
qbank/cmd/server/main.go
T
Jānis Kacēns d9de37d3d8 Phase 2: auth, session management, layout, PWA manifest
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 11:54:37 +03:00

147 lines
3.5 KiB
Go

package main
import (
"context"
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"log/slog"
"net/http"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"qbank/internal/auth"
"qbank/internal/config"
"qbank/internal/db"
"qbank/internal/handlers"
)
func main() {
cfg := config.Load()
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
slog.SetDefault(logger)
if err := os.MkdirAll(cfg.DataDir, 0755); err != nil {
slog.Error("create data dir", "err", err)
os.Exit(1)
}
database, err := db.Open(filepath.Join(cfg.DataDir, "qbank.db"))
if err != nil {
slog.Error("open database", "err", err)
os.Exit(1)
}
defer database.Close()
if err := db.Seed(database, cfg.AdminUsers); err != nil {
slog.Error("seed users", "err", err)
os.Exit(1)
}
repo := db.New(database)
authMgr := auth.NewManager(database)
renderer := handlers.NewRenderer("web/templates")
ensureIcons("web/static")
authH := handlers.NewAuthHandler(authMgr, repo, renderer)
homeH := handlers.NewHomeHandler(authMgr, renderer)
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(requestLogger(logger))
r.Use(middleware.Recoverer)
r.Use(authMgr.SM.LoadAndSave)
r.Get("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})
r.Get("/sw.js", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "web/static/sw.js")
})
r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static"))))
r.Get("/login", authH.LoginGet)
r.Post("/login", authH.LoginPost)
r.Post("/logout", authH.Logout)
r.Group(func(r chi.Router) {
r.Use(authMgr.RequireAuth)
r.Get("/", homeH.Handle)
})
srv := &http.Server{
Addr: ":" + cfg.Port,
Handler: r,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
slog.Info("server starting", "port", cfg.Port)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("server error", "err", err)
os.Exit(1)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
slog.Error("shutdown error", "err", err)
}
slog.Info("server stopped")
}
// ensureIcons generates simple solid-color PNG icons if they don't already exist.
func ensureIcons(dir string) {
for _, size := range []int{192, 512} {
path := filepath.Join(dir, fmt.Sprintf("icon-%d.png", size))
if _, err := os.Stat(path); err == nil {
continue
}
img := image.NewRGBA(image.Rect(0, 0, size, size))
blue := color.RGBA{R: 37, G: 99, B: 235, A: 255} // Tailwind blue-600
draw.Draw(img, img.Bounds(), &image.Uniform{C: blue}, image.Point{}, draw.Src)
f, err := os.Create(path)
if err != nil {
slog.Warn("create icon", "path", path, "err", err)
continue
}
png.Encode(f, img)
f.Close()
}
}
func requestLogger(logger *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
next.ServeHTTP(ww, r)
logger.Info("request",
"method", r.Method,
"path", r.URL.Path,
"status", ww.Status(),
"duration_ms", time.Since(start).Milliseconds(),
"request_id", middleware.GetReqID(r.Context()),
)
})
}
}