Files

92 lines
2.6 KiB
Go

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
}