diff --git a/internal/features/anime/handler.go b/internal/features/anime/handler.go index 0b85a6d..18b6eb5 100644 --- a/internal/features/anime/handler.go +++ b/internal/features/anime/handler.go @@ -6,7 +6,7 @@ import ( "strconv" "malago/internal/database" - "malago/internal/middleware" + "malago/internal/shared/middleware" "malago/internal/templates" ) diff --git a/internal/features/watchlist/handler.go b/internal/features/watchlist/handler.go index 22cffbc..3464bf3 100644 --- a/internal/features/watchlist/handler.go +++ b/internal/features/watchlist/handler.go @@ -8,7 +8,7 @@ import ( "strconv" "malago/internal/database" - "malago/internal/middleware" + "malago/internal/shared/middleware" "malago/internal/templates" ) diff --git a/internal/server/routes.go b/internal/server/routes.go index c9e11dd..9718bff 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -8,7 +8,7 @@ import ( "malago/internal/features/auth" "malago/internal/features/watchlist" "malago/internal/jikan" - "malago/internal/middleware" + "malago/internal/shared/middleware" ) type Config struct { diff --git a/internal/shared/middleware/auth.go b/internal/shared/middleware/auth.go new file mode 100644 index 0000000..f57d097 --- /dev/null +++ b/internal/shared/middleware/auth.go @@ -0,0 +1,91 @@ +package middleware + +import ( + "context" + "net/http" + "strings" + + "malago/internal/database" + "malago/internal/features/auth" +) + +type contextKey string + +const ( + UserContextKey contextKey = "user" +) + +func Auth(authService *auth.Service) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie("session_id") + if err != nil { + // No session cookie, user is unauthenticated. Proceed, but not logged in. + next.ServeHTTP(w, r) + return + } + + user, err := authService.ValidateSession(r.Context(), cookie.Value) + if err != nil { + // Invalid session, proceed as unauthenticated + // Might also want to clear the invalid cookie here + auth.ClearSessionCookie(w) + next.ServeHTTP(w, r) + return + } + + // Valid session, bind user to context + ctx := context.WithValue(r.Context(), UserContextKey, user) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} + +// RequireAuth ensures that a valid user is in the context, otherwise unauthorized +func RequireAuth(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user, ok := r.Context().Value(UserContextKey).(*database.User) + if !ok || user == nil { + if strings.HasPrefix(r.URL.Path, "/api/") { + w.Header().Set("HX-Redirect", "/login") + http.Error(w, "Unauthorized", http.StatusUnauthorized) + } else { + http.Redirect(w, r, "/login", http.StatusFound) + } + return + } + next.ServeHTTP(w, r) + }) +} + +// RequireGlobalAuth ensures that a valid user is in the context for all routes except login and static +func RequireGlobalAuth(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Allow unauthenticated access to login and static files + if r.URL.Path == "/login" || strings.HasPrefix(r.URL.Path, "/static/") { + next.ServeHTTP(w, r) + return + } + + user, ok := r.Context().Value(UserContextKey).(*database.User) + if !ok || user == nil { + if strings.HasPrefix(r.URL.Path, "/api/") || r.Header.Get("HX-Request") == "true" { + w.Header().Set("HX-Redirect", "/login") + http.Error(w, "Unauthorized", http.StatusUnauthorized) + } else { + http.Redirect(w, r, "/login", http.StatusFound) + } + return + } + next.ServeHTTP(w, r) + }) +} + +// GetUser returns the user from context, or nil if not logged in +func GetUser(ctx context.Context) *database.User { + user, ok := ctx.Value(UserContextKey).(*database.User) + if !ok { + return nil + } + return user +}