package auth import ( "crypto/rand" "encoding/hex" "net/http" "strings" "sync" "time" ) // SessionStore manages admin authentication sessions. type SessionStore struct { mu sync.Mutex sessions map[string]time.Time } // NewSessionStore creates a new session store. func NewSessionStore() *SessionStore { return &SessionStore{ sessions: make(map[string]time.Time), } } // GenerateToken creates a cryptographically random 64-char hex session token. func GenerateToken() (string, error) { buf := make([]byte, 32) if _, err := rand.Read(buf); err != nil { return "", err } return hex.EncodeToString(buf), nil } // Validate checks if a session token is valid and not expired. // Returns true if valid, false otherwise. Expired sessions are cleaned up. func (s *SessionStore) Validate(token string) bool { if token == "" { return false } s.mu.Lock() defer s.mu.Unlock() expiresAt, ok := s.sessions[token] if !ok { return false } if time.Now().After(expiresAt) { delete(s.sessions, token) return false } return true } // Add stores a session token with expiry. func (s *SessionStore) Add(token string, expiresAt time.Time) { s.mu.Lock() defer s.mu.Unlock() s.sessions[token] = expiresAt } // Remove deletes a session token. func (s *SessionStore) Remove(token string) { s.mu.Lock() defer s.mu.Unlock() delete(s.sessions, token) } // CountActive returns the number of unexpired sessions. func (s *SessionStore) CountActive() int { now := time.Now() s.mu.Lock() defer s.mu.Unlock() for token, expiresAt := range s.sessions { if now.After(expiresAt) { delete(s.sessions, token) } } return len(s.sessions) } // CookieSecure determines if the session cookie should have Secure flag. func CookieSecure(r *http.Request) bool { if forwardedProto := strings.TrimSpace(r.Header.Get("X-Forwarded-Proto")); forwardedProto != "" { return strings.EqualFold(forwardedProto, "https") } return r.TLS != nil } // SameSiteForUserCookie returns the SameSite policy for the viewer session cookie. // Over HTTPS it returns SameSite=None so the cookie is sent cross-site from the // Workers-served SPA; over plain HTTP (local dev) it falls back to Lax. func SameSiteForUserCookie(r *http.Request) http.SameSite { if CookieSecure(r) { return http.SameSiteNoneMode } return http.SameSiteLaxMode } // SetCookie writes a session cookie with the given SameSite policy. // SameSite=None is only valid together with Secure, which is enforced here. func SetCookie(w http.ResponseWriter, r *http.Request, cookieName, token string, expiresAt time.Time, sameSite http.SameSite) { secure := CookieSecure(r) if sameSite == http.SameSiteNoneMode { secure = true } http.SetCookie(w, &http.Cookie{ Name: cookieName, Value: token, Path: "/", HttpOnly: true, SameSite: sameSite, Secure: secure, Expires: expiresAt, MaxAge: int(time.Until(expiresAt).Seconds()), }) } // ClearCookie removes a session cookie with the given SameSite policy. func ClearCookie(w http.ResponseWriter, r *http.Request, cookieName string, sameSite http.SameSite) { secure := CookieSecure(r) if sameSite == http.SameSiteNoneMode { secure = true } http.SetCookie(w, &http.Cookie{ Name: cookieName, Value: "", Path: "/", HttpOnly: true, SameSite: sameSite, Secure: secure, Expires: time.Unix(0, 0), MaxAge: -1, }) } // GetTokenFromCookie extracts the session token from the request cookie. func GetTokenFromCookie(r *http.Request, cookieName string) (string, bool) { cookie, err := r.Cookie(cookieName) if err != nil || cookie.Value == "" { return "", false } return cookie.Value, true } // RequireAdmin returns middleware that checks for a valid admin session. func RequireAdmin(store *SessionStore, cookieName string) func(http.HandlerFunc) http.HandlerFunc { return func(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { token, ok := GetTokenFromCookie(r, cookieName) if !ok || !store.Validate(token) { http.Error(w, "unauthorized", http.StatusUnauthorized) return } next(w, r) } } } // UserSessionStore maps user session tokens to usernames with expiry. type UserSessionStore struct { mu sync.Mutex sessions map[string]userSession } type userSession struct { Username string ExpiresAt time.Time } // NewUserSessionStore creates a new user session store. func NewUserSessionStore() *UserSessionStore { return &UserSessionStore{sessions: make(map[string]userSession)} } // Add stores a user session token with username and expiry. func (s *UserSessionStore) Add(token, username string, expiresAt time.Time) { s.mu.Lock() defer s.mu.Unlock() s.sessions[token] = userSession{Username: username, ExpiresAt: expiresAt} } // Username returns the username for a valid, unexpired token. func (s *UserSessionStore) Username(token string) (string, bool) { if token == "" { return "", false } s.mu.Lock() defer s.mu.Unlock() sess, ok := s.sessions[token] if !ok { return "", false } if time.Now().After(sess.ExpiresAt) { delete(s.sessions, token) return "", false } return sess.Username, true } // Remove deletes a user session token. func (s *UserSessionStore) Remove(token string) { s.mu.Lock() defer s.mu.Unlock() delete(s.sessions, token) }