package web import ( "context" "crypto/rand" "encoding/hex" "net/http" "time" "hearthforge/internal/db" ) const ( sessionCookie = "session" sessionIDBytes = 32 sessionDuration = 30 * 24 * time.Hour ) type ctxKey int const userKey ctxKey = 0 // withSession resolves the session cookie and stores the user in the context. func (s *Server) withSession(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, err := r.Cookie(sessionCookie) if err != nil || c.Value == "" { next.ServeHTTP(w, r) return } u, err := s.DB.SessionUser(r.Context(), c.Value, db.NowISO()) if err != nil || u == nil { next.ServeHTTP(w, r) return } next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userKey, u))) }) } // User returns the logged-in user or nil. func User(r *http.Request) *db.SessionUser { u, _ := r.Context().Value(userKey).(*db.SessionUser) return u } // requireAuth redirects anonymous requests to /login. func (s *Server) requireAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if User(r) == nil { http.Redirect(w, r, "/login", http.StatusFound) return } next.ServeHTTP(w, r) }) } // requireAdmin redirects anonymous requests and rejects non-admins. func (s *Server) requireAdmin(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { u := User(r) if u == nil { http.Redirect(w, r, "/login", http.StatusFound) return } if !u.IsAdmin { http.Error(w, "Forbidden", http.StatusForbidden) return } next.ServeHTTP(w, r) }) } // newSession creates a session row and returns the cookie to set. func (s *Server) newSession(ctx context.Context, userID int64) (*http.Cookie, error) { b := make([]byte, sessionIDBytes) if _, err := rand.Read(b); err != nil { return nil, err } id := hex.EncodeToString(b) now := time.Now().UTC() exp := now.Add(sessionDuration) if err := s.DB.CreateSession(ctx, id, userID, exp.Format(db.ISOLayout), now.Format(db.ISOLayout)); err != nil { return nil, err } return s.sessionCookie(id, int(sessionDuration.Seconds())), nil } func (s *Server) sessionCookie(value string, maxAge int) *http.Cookie { return &http.Cookie{ Name: sessionCookie, Value: value, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, Secure: s.Cfg.PublicHTTPS, MaxAge: maxAge, } } // clearSession deletes the session row and expires the cookie. func (s *Server) clearSession(w http.ResponseWriter, r *http.Request) { if c, err := r.Cookie(sessionCookie); err == nil && c.Value != "" { _ = s.DB.DeleteSession(r.Context(), c.Value) } http.SetCookie(w, s.sessionCookie("", -1)) }