Files
2026-05-05 08:44:55 +00:00

111 lines
2.3 KiB
Go

package middleware
import (
"context"
"crypto/rand"
"encoding/hex"
"net/http"
"sync"
"time"
)
type contextKey string
const sessionKey contextKey = "userID"
// SessionStore is a simple in-memory session store.
// Swap for Redis in production.
type SessionStore struct {
mu sync.RWMutex
sessions map[string]sessionEntry
}
type sessionEntry struct {
userID string
expiresAt time.Time
}
func NewSessionStore() *SessionStore {
s := &SessionStore{sessions: make(map[string]sessionEntry)}
go s.cleanup()
return s
}
func (s *SessionStore) Create(userID string) string {
b := make([]byte, 32)
rand.Read(b)
token := hex.EncodeToString(b)
s.mu.Lock()
s.sessions[token] = sessionEntry{userID: userID, expiresAt: time.Now().Add(24 * time.Hour)}
s.mu.Unlock()
return token
}
func (s *SessionStore) Get(token string) (string, bool) {
s.mu.RLock()
entry, ok := s.sessions[token]
s.mu.RUnlock()
if !ok || time.Now().After(entry.expiresAt) {
return "", false
}
return entry.userID, true
}
func (s *SessionStore) Delete(token string) {
s.mu.Lock()
delete(s.sessions, token)
s.mu.Unlock()
}
func (s *SessionStore) cleanup() {
for range time.Tick(time.Hour) {
s.mu.Lock()
for k, v := range s.sessions {
if time.Now().After(v.expiresAt) {
delete(s.sessions, k)
}
}
s.mu.Unlock()
}
}
// RequireAuth middleware redirects unauthenticated requests to /login.
func (s *SessionStore) RequireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session")
if err != nil {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
userID, ok := s.Get(cookie.Value)
if !ok {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
ctx := context.WithValue(r.Context(), sessionKey, userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// SetSession writes the session cookie.
func SetSession(w http.ResponseWriter, token string) {
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: token,
Path: "/",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
MaxAge: 86400,
})
}
// ClearSession removes the session cookie.
func ClearSession(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: "",
Path: "/",
MaxAge: -1,
})
}