package api import ( "context" "fmt" "net/http" "strings" "github.com/golang-jwt/jwt/v5" "workorders/internal/model" ) var roleLevel = map[string]int{ "admin": 4, "dispatcher": 3, "field_tech": 2, "viewer": 1, } // JWTAuth validates a locally-issued HMAC-signed JWT in the Authorization header. func JWTAuth(secret string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization") if !strings.HasPrefix(auth, "Bearer ") { jsonError(w, "unauthorized", http.StatusUnauthorized) return } raw := strings.TrimPrefix(auth, "Bearer ") var claims model.LocalClaims _, err := jwt.ParseWithClaims(raw, &claims, func(t *jwt.Token) (any, error) { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) } return []byte(secret), nil }, jwt.WithExpirationRequired()) if err != nil { jsonError(w, "invalid token", http.StatusUnauthorized) return } user := model.UserClaims{ UserID: claims.UserID, Username: claims.Username, Email: claims.Email, DisplayName: claims.DisplayName, Role: claims.Role, } ctx := context.WithValue(r.Context(), model.CtxUserKey, user) next.ServeHTTP(w, r.WithContext(ctx)) }) } } // RequireRole blocks requests where the user's role is below the required level. func RequireRole(minRole string) func(http.Handler) http.Handler { minLevel := roleLevel[minRole] return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := UserFromCtx(r) if roleLevel[user.Role] >= minLevel { next.ServeHTTP(w, r) return } jsonError(w, "forbidden", http.StatusForbidden) }) } } func CORS(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type") if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } func jsonError(w http.ResponseWriter, msg string, code int) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(code) fmt.Fprintf(w, `{"error":%q}`, msg) } func UserFromCtx(r *http.Request) model.UserClaims { u, _ := r.Context().Value(model.CtxUserKey).(model.UserClaims) return u }