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 } // SetCookie writes the admin session cookie. func SetCookie(w http.ResponseWriter, r *http.Request, cookieName, token string, expiresAt time.Time) { http.SetCookie(w, &http.Cookie{ Name: cookieName, Value: token, Path: "/", HttpOnly: true, SameSite: http.SameSiteStrictMode, Secure: CookieSecure(r), Expires: expiresAt, MaxAge: int(time.Until(expiresAt).Seconds()), }) } // ClearCookie removes the admin session cookie. func ClearCookie(w http.ResponseWriter, r *http.Request, cookieName string) { http.SetCookie(w, &http.Cookie{ Name: cookieName, Value: "", Path: "/", HttpOnly: true, SameSite: http.SameSiteStrictMode, Secure: CookieSecure(r), 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) } } }