session.go
⎇
Raw
1package web
2
3import (
4 "context"
5 "crypto/rand"
6 "encoding/hex"
7 "net/http"
8 "time"
9
10 "hearthforge/internal/db"
11)
12
13const (
14 sessionCookie = "session"
15 sessionIDBytes = 32
16 sessionDuration = 30 * 24 * time.Hour
17)
18
19type ctxKey int
20
21const userKey ctxKey = 0
22
23// withSession resolves the session cookie and stores the user in the context.
24func (s *Server) withSession(next http.Handler) http.Handler {
25 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
26 c, err := r.Cookie(sessionCookie)
27 if err != nil || c.Value == "" {
28 next.ServeHTTP(w, r)
29 return
30 }
31 u, err := s.DB.SessionUser(r.Context(), c.Value, db.NowISO())
32 if err != nil || u == nil {
33 next.ServeHTTP(w, r)
34 return
35 }
36 next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userKey, u)))
37 })
38}
39
40// User returns the logged-in user or nil.
41func User(r *http.Request) *db.SessionUser {
42 u, _ := r.Context().Value(userKey).(*db.SessionUser)
43 return u
44}
45
46// requireAuth redirects anonymous requests to /login.
47func (s *Server) requireAuth(next http.Handler) http.Handler {
48 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
49 if User(r) == nil {
50 http.Redirect(w, r, "/login", http.StatusFound)
51 return
52 }
53 next.ServeHTTP(w, r)
54 })
55}
56
57// requireAdmin redirects anonymous requests and rejects non-admins.
58func (s *Server) requireAdmin(next http.Handler) http.Handler {
59 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
60 u := User(r)
61 if u == nil {
62 http.Redirect(w, r, "/login", http.StatusFound)
63 return
64 }
65 if !u.IsAdmin {
66 http.Error(w, "Forbidden", http.StatusForbidden)
67 return
68 }
69 next.ServeHTTP(w, r)
70 })
71}
72
73// newSession creates a session row and returns the cookie to set.
74func (s *Server) newSession(ctx context.Context, userID int64) (*http.Cookie, error) {
75 b := make([]byte, sessionIDBytes)
76 if _, err := rand.Read(b); err != nil {
77 return nil, err
78 }
79 id := hex.EncodeToString(b)
80 now := time.Now().UTC()
81 exp := now.Add(sessionDuration)
82 if err := s.DB.CreateSession(ctx, id, userID,
83 exp.Format(db.ISOLayout), now.Format(db.ISOLayout)); err != nil {
84 return nil, err
85 }
86 return s.sessionCookie(id, int(sessionDuration.Seconds())), nil
87}
88
89func (s *Server) sessionCookie(value string, maxAge int) *http.Cookie {
90 return &http.Cookie{
91 Name: sessionCookie,
92 Value: value,
93 Path: "/",
94 HttpOnly: true,
95 SameSite: http.SameSiteLaxMode,
96 Secure: s.Cfg.PublicHTTPS,
97 MaxAge: maxAge,
98 }
99}
100
101// clearSession deletes the session row and expires the cookie.
102func (s *Server) clearSession(w http.ResponseWriter, r *http.Request) {
103 if c, err := r.Cookie(sessionCookie); err == nil && c.Value != "" {
104 _ = s.DB.DeleteSession(r.Context(), c.Value)
105 }
106 http.SetCookie(w, s.sessionCookie("", -1))
107}
108