92 lines
2.6 KiB
Go
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
|
|
}
|