135 lines
3.5 KiB
Go
135 lines
3.5 KiB
Go
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 <token>"
|
|
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
|
|
}
|