233 lines
5.2 KiB
Go
233 lines
5.2 KiB
Go
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()
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (h *Handler) Router() http.Handler {
|
|
mux := http.NewServeMux()
|
|
h.registerRoutes(mux)
|
|
return mux
|
|
}
|
|
|
|
func (h *Handler) Serve(ctx context.Context) error {
|
|
go h.runCleanupLoop(ctx)
|
|
|
|
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)
|
|
}
|