- JWT revocation with JTI (UUID v4): in-memory tracking, POST /api/logout, refresh handler revokes old JTI, RequireAuth rejects revoked tokens - Fix extractKey for S3 portfolio deletion: extracts full key path from URLs instead of just filename, preventing orphaned storage files - Notes validation: max=1000000 on all 13 Notes fields across 4 booking structs - CharCounter: grapheme-aware counter (Intl.Segmenter), threshold 750K, color-coded, integrated into 6 booking/admin components - loginInProgress: timestamp-based tracking, 30s staleness, 20-entry cap (429), ticker cleanup for stuck entries - Profile picture 15MB client-side limit, portfolio 20MB backend limit - Exceptional scheduling: expand query start to Monday of week - TodayCalendar: week-range fetching, closing time indicator, short-day lunch skip - NavBar: link reorder, mobile burger badge, slide transition, backdrop - ImageUpload: 20MB limit with visual feedback - formatDateISO: shared YYYY-MM-DD utility, shouldApplyLunchProtection helper - Update README.md and all Obsidian docs (Overview, Technical, Admin, Future Work) - Add 28 new tests: JWT (11), auth handlers (7), portfolio extractKey (5), notes validation (5). go build + go vet clean with test,dev tags
120 lines
3.2 KiB
Go
120 lines
3.2 KiB
Go
package mw
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"crussell/auth"
|
|
)
|
|
|
|
type contextKey string
|
|
|
|
const (
|
|
UserIDKey contextKey = "user_id"
|
|
UserRoleKey contextKey = "user_role"
|
|
JTIKey contextKey = "jti"
|
|
)
|
|
|
|
// RequireAuth middleware - validates JWT and adds user info to context
|
|
func RequireAuth(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
authHeader := r.Header.Get("Authorization")
|
|
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
|
|
http.Error(w, "missing or invalid authorization header", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
|
|
|
userID, role, jti, err := auth.VerifyToken(tokenString, r.Context())
|
|
if err != nil {
|
|
http.Error(w, "invalid token", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Add user info and JTI to context
|
|
ctx := context.WithValue(r.Context(), UserIDKey, userID)
|
|
ctx = context.WithValue(ctx, UserRoleKey, role)
|
|
ctx = context.WithValue(ctx, JTIKey, jti)
|
|
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
|
|
// OptionalAuth middleware - extracts user info if token present, otherwise passes through
|
|
func OptionalAuth(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
authHeader := r.Header.Get("Authorization")
|
|
if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
|
|
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
|
|
|
userID, role, jti, err := auth.VerifyToken(tokenString, r.Context())
|
|
if err == nil {
|
|
ctx := context.WithValue(r.Context(), UserIDKey, userID)
|
|
ctx = context.WithValue(ctx, UserRoleKey, role)
|
|
ctx = context.WithValue(ctx, JTIKey, jti)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
return
|
|
}
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// RequireRole middleware - checks if user has required role(s)
|
|
func RequireRole(allowedRoles ...string) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
role, ok := r.Context().Value(UserRoleKey).(string)
|
|
if !ok {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Check if user has one of the allowed roles
|
|
hasRole := false
|
|
for _, allowedRole := range allowedRoles {
|
|
if role == allowedRole {
|
|
hasRole = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !hasRole {
|
|
http.Error(w, "forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
// RequireVerified middleware - only allows verified_email and admin
|
|
func RequireVerified(next http.Handler) http.Handler {
|
|
return RequireRole("verified_email", "admin")(next)
|
|
}
|
|
|
|
// RequireAdmin middleware - only allows admin
|
|
func RequireAdmin(next http.Handler) http.Handler {
|
|
return RequireRole("admin")(next)
|
|
}
|
|
|
|
// Helper functions to get user info from context
|
|
func GetUserID(ctx context.Context) (string, bool) {
|
|
userID, ok := ctx.Value(UserIDKey).(string)
|
|
return userID, ok
|
|
}
|
|
|
|
func GetUserRole(ctx context.Context) (string, bool) {
|
|
role, ok := ctx.Value(UserRoleKey).(string)
|
|
return role, ok
|
|
}
|
|
|
|
func GetJTI(ctx context.Context) (string, bool) {
|
|
jti, ok := ctx.Value(JTIKey).(string)
|
|
return jti, ok
|
|
}
|