Files
uPortal2/backend/internal/rest/server.go
T

232 lines
5.2 KiB
Go
Raw Normal View History

2026-10-21 13:17:44 +03:00
package rest
import (
"context"
"database/sql"
"errors"
"log/slog"
"net/http"
"time"
"goAuth/internal/auth"
"goAuth/internal/invite"
"goAuth/internal/jitsi"
"goAuth/internal/middleware"
"goAuth/internal/migration"
"goAuth/internal/user"
_ "github.com/mattn/go-sqlite3"
)
type Handler struct {
cfg Config
db *sql.DB
users *user.Service
invites *invite.Service
sessions *auth.Service
jitsi *jitsi.Service
}
func New(cfg Config) (*Handler, error) {
if cfg.Cookies.SessionName == "" {
cfg.Cookies.SessionName = DefaultSessionCookie
}
if cfg.Cookies.CsrfName == "" {
cfg.Cookies.CsrfName = DefaultCsrfCookie
}
if cfg.Sessions.TokenSize <= 0 {
cfg.Sessions.TokenSize = DefaultTokenSize
}
db, err := openDatabase(cfg.DatabasePath)
if err != nil {
return nil, err
}
if err := migrate(context.Background(), db); err != nil {
db.Close()
return nil, err
}
handler, err := wire(cfg, db)
if err != nil {
db.Close()
return nil, err
}
if err := seed(context.Background(), handler.users); err != nil {
handler.Close()
return nil, err
}
return handler, nil
}
func openDatabase(path string) (*sql.DB, error) {
db, err := sql.Open("sqlite3", path+"?_foreign_keys=on")
if err != nil {
slog.Error("open database", slog.String("op", "rest.openDatabase"), slog.Any("error", err))
return nil, err
}
db.SetMaxOpenConns(1)
return db, nil
}
func wire(cfg Config, db *sql.DB) (*Handler, error) {
userRepo, err := user.NewRepository(db)
if err != nil {
return nil, err
}
inviteRepo, err := invite.NewRepository(db)
if err != nil {
return nil, err
}
sessionRepo, err := auth.NewRepository(db)
if err != nil {
return nil, err
}
jitsiRepo, err := jitsi.NewRepository(db)
if err != nil {
return nil, err
}
userService, err := user.NewService(userRepo)
if err != nil {
return nil, err
}
inviteService, err := invite.NewService(inviteRepo, 0, 0)
if err != nil {
return nil, err
}
sessionService, err := auth.NewService(
sessionRepo,
userService,
inviteService,
cfg.Sessions.ShortDuration,
cfg.Sessions.LongDuration,
cfg.Sessions.TokenSize,
)
if err != nil {
return nil, err
}
jitsiService, err := jitsi.NewService(
jitsiRepo,
cfg.Jitsi.Secret,
cfg.Jitsi.AppId,
cfg.Jitsi.AppName,
cfg.Jitsi.Domain,
)
if err != nil {
return nil, err
}
return &Handler{
cfg: cfg,
db: db,
users: userService,
invites: inviteService,
sessions: sessionService,
jitsi: jitsiService,
}, nil
}
func migrate(ctx context.Context, db *sql.DB) error {
migrator := migration.NewMigrator(db)
migrator.Register("user", user.Migrations()...)
migrator.Register("auth", auth.Migrations()...)
migrator.Register("invite", invite.Migrations()...)
migrator.Register("jitsi", jitsi.Migrations()...)
return migrator.Run(ctx)
}
func (h *Handler) Close() error {
if h.db == nil {
return nil
}
return h.db.Close()
}
2026-10-21 20:13:27 +03:00
// Cleanup purges expired sessions, spent/expired invites and never-activated
// users. Each target is cleaned independently so a single failure does not
// prevent the others from running.
func (h *Handler) Cleanup(ctx context.Context) {
if err := h.sessions.CleanupSessions(ctx); err != nil {
slog.Error("cleanup sessions", slog.String("op", "rest.Cleanup"), slog.Any("error", err))
}
if err := h.invites.CleanupInvites(ctx); err != nil {
slog.Error("cleanup invites", slog.String("op", "rest.Cleanup"), slog.Any("error", err))
}
if err := h.users.CleanupUsers(ctx); err != nil {
slog.Error("cleanup users", slog.String("op", "rest.Cleanup"), slog.Any("error", err))
}
}
// runCleanupLoop runs Cleanup immediately and then on every tick until ctx is
// cancelled. It is a no-op when the configured interval is non-positive.
func (h *Handler) runCleanupLoop(ctx context.Context) {
interval := h.cfg.CleanupInterval
if interval <= 0 {
slog.Info("background cleanup disabled", slog.String("op", "rest.runCleanupLoop"))
return
}
slog.Info("background cleanup started", slog.String("op", "rest.runCleanupLoop"), slog.Duration("interval", interval))
h.Cleanup(ctx)
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
h.Cleanup(ctx)
}
}
}
2026-10-21 13:17:44 +03:00
func (h *Handler) Router() http.Handler {
mux := http.NewServeMux()
h.registerRoutes(mux)
return mux
}
func (h *Handler) Serve(ctx context.Context) error {
2026-10-21 20:13:27 +03:00
go h.runCleanupLoop(ctx)
2026-10-21 13:17:44 +03:00
server := &http.Server{
Addr: h.cfg.Addr,
Handler: h.Router(),
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
serveErr := make(chan error, 1)
go func() {
slog.Info("rest server listening", slog.String("addr", h.cfg.Addr))
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
serveErr <- err
return
}
serveErr <- nil
}()
select {
case err := <-serveErr:
return err
case <-ctx.Done():
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return server.Shutdown(shutdownCtx)
}
}
func (h *Handler) sessionMiddleware() middleware.Middleware {
return h.sessions.NewMiddleware(h.cfg.Cookies.SessionName)
}