package httpapp import ( "context" "net/http" "strings" "gitea.avt.data-center.id/othman.suseno/atlas/internal/auth" "gitea.avt.data-center.id/othman.suseno/atlas/internal/models" ) const ( userCtxKey ctxKey = "user" roleCtxKey ctxKey = "role" ) // authMiddleware validates JWT tokens and extracts user info func (a *App) authMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Skip auth for public endpoints if a.isPublicEndpoint(r.URL.Path) { next.ServeHTTP(w, r) return } // Extract token from Authorization header authHeader := r.Header.Get("Authorization") if authHeader == "" { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "missing authorization header"}) return } // Parse "Bearer " parts := strings.Split(authHeader, " ") if len(parts) != 2 || parts[0] != "Bearer" { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid authorization header format"}) return } token := parts[1] claims, err := a.authService.ValidateToken(token) if err != nil { if err == auth.ErrExpiredToken { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "token expired"}) } else { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid token"}) } return } // Get user from store user, err := a.userStore.GetByID(claims.UserID) if err != nil { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "user not found"}) return } if !user.Active { writeJSON(w, http.StatusForbidden, map[string]string{"error": "user account is disabled"}) return } // Add user info to context ctx := context.WithValue(r.Context(), userCtxKey, user) ctx = context.WithValue(ctx, roleCtxKey, user.Role) next.ServeHTTP(w, r.WithContext(ctx)) }) } // requireRole middleware checks if user has required role func (a *App) requireRole(allowedRoles ...models.Role) 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(roleCtxKey).(models.Role) if !ok { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"}) return } // Check if user role is in allowed roles allowed := false for _, allowedRole := range allowedRoles { if role == allowedRole { allowed = true break } } if !allowed { writeJSON(w, http.StatusForbidden, map[string]string{"error": "insufficient permissions"}) return } next.ServeHTTP(w, r) }) } } // isPublicEndpoint checks if an endpoint is public (no auth required) func (a *App) isPublicEndpoint(path string) bool { publicPaths := []string{ "/healthz", "/metrics", "/api/v1/auth/login", "/api/v1/auth/logout", "/", // Dashboard (can be made protected later) } for _, publicPath := range publicPaths { if path == publicPath || strings.HasPrefix(path, publicPath+"/") { return true } } // Static files are public if strings.HasPrefix(path, "/static/") { return true } return false } // getUserFromContext extracts user from request context func getUserFromContext(r *http.Request) (*models.User, bool) { user, ok := r.Context().Value(userCtxKey).(*models.User) return user, ok } // getRoleFromContext extracts role from request context func getRoleFromContext(r *http.Request) (models.Role, bool) { role, ok := r.Context().Value(roleCtxKey).(models.Role) return role, ok }