hako/internal/server/middleware/auth.go
Felipe M. 484b225af7
feat: add user administration API and UI
Add admin-only CRUD endpoints for user management under /api/v1/system/users
with full frontend implementation including create, edit, change password,
and delete operations with self-protection guards.

- Extend AuthDomain interface with ListUsers, GetUser, UpdateUser,
  UpdateUserPassword, DeleteUser methods
- Add List, Update, Delete methods to UserStore with shared scanUser helper
- Create UserHandler with 5 endpoints protected by admin middleware
- Complete Users.vue admin page with modals and error handling
- Make CreateUser accept role parameter for atomic user creation
- Fix auth middleware to use database role as source of truth instead of
  stale JWT claims, preventing privilege persistence after demotion
- Fix pre-existing gofmt issues in yt_dlp.go and config.go
2026-04-02 10:33:36 +02:00

120 lines
3.9 KiB
Go

package middleware
import (
"net/http"
"strings"
"git.nakama.town/fmartingr/hako/internal/auth"
"git.nakama.town/fmartingr/hako/internal/auth/store"
"git.nakama.town/fmartingr/hako/internal/server/webcontext"
"github.com/google/uuid"
)
// AuthMiddleware is a global middleware that validates JWT tokens if present
// and adds user info to context. It does not block requests if no token is provided.
// This allows routes to optionally use authentication information.
// The middleware also verifies that the user exists in the database to prevent
// authentication with tokens for deleted users.
func AuthMiddleware(jwtService *auth.JWTService, userStore *store.UserStore) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Create webcontext for this request
c := webcontext.NewWebContext(w, r)
var tokenString string
// Extract token from Authorization header if present
authHeader := r.Header.Get("Authorization")
if authHeader != "" {
// Check Bearer prefix
parts := strings.Split(authHeader, " ")
if len(parts) == 2 && parts[0] == "Bearer" {
tokenString = parts[1]
}
}
// Fallback: extract token from query parameter if not in header
// This allows embedding authenticated URLs (e.g., for images)
if tokenString == "" {
tokenString = r.URL.Query().Get("token")
}
// Validate token if present
if tokenString != "" {
// Validate token - if valid, verify user exists; if invalid, continue without context
claims, err := jwtService.ValidateToken(tokenString)
if err == nil {
// Token is valid, verify user exists in database
userID, parseErr := uuid.Parse(claims.UserID)
if parseErr == nil {
user, userErr := userStore.FindByID(userID)
if userErr == nil && user != nil {
// User exists, add user info to context
// Use role from database (source of truth)
role := user.Role
if role == "" {
role = "user"
}
c.SetUserWithRole(claims.UserID, claims.Email, role)
// Update request with new context
r = c.Request()
}
// If user doesn't exist, continue without setting context (don't block)
}
}
// If token is invalid, continue without setting context (don't block)
}
// Continue with request (with or without user context)
// Use the updated request if context was modified
next.ServeHTTP(w, r)
})
}
}
// RequireAuthMiddleware is a middleware that requires authentication.
// It checks if user info is in context and returns 401 if not authenticated.
func RequireAuthMiddleware() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Create webcontext for this request
c := webcontext.NewWebContext(w, r)
// Check if user is authenticated
if !c.UserIsLogged() {
http.Error(w, "Authorization required", http.StatusUnauthorized)
return
}
// User is authenticated, continue
next.ServeHTTP(w, r)
})
}
}
// RequireAdminMiddleware is a middleware that requires admin role.
// It checks if user is authenticated and has admin role, returns 403 if not admin.
func RequireAdminMiddleware() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Create webcontext for this request
c := webcontext.NewWebContext(w, r)
// Check if user is authenticated
if !c.UserIsLogged() {
http.Error(w, "Authorization required", http.StatusUnauthorized)
return
}
// Check if user has admin role
role := c.GetUserRole()
if role != "admin" {
http.Error(w, "Admin access required", http.StatusForbidden)
return
}
// User is authenticated and is admin, continue
next.ServeHTTP(w, r)
})
}
}