- Add auth HTTP handlers (login, me, logout) with proper JSON responses - Enforce HS256 via jwt.WithValidMethods to prevent algorithm confusion attacks - Add context helpers for extracting operator ID and role from JWT claims - Add comprehensive auth test suite (11 unit tests + 6 integration tests) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
100 lines
3.1 KiB
Go
100 lines
3.1 KiB
Go
// Package routes provides HTTP route handlers for the Felt tournament engine.
|
|
package routes
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"github.com/felt-app/felt/internal/auth"
|
|
"github.com/felt-app/felt/internal/server/middleware"
|
|
)
|
|
|
|
// AuthHandler handles authentication routes.
|
|
type AuthHandler struct {
|
|
authService *auth.AuthService
|
|
}
|
|
|
|
// NewAuthHandler creates a new auth route handler.
|
|
func NewAuthHandler(authService *auth.AuthService) *AuthHandler {
|
|
return &AuthHandler{authService: authService}
|
|
}
|
|
|
|
// loginRequest is the request body for POST /api/v1/auth/login.
|
|
type loginRequest struct {
|
|
PIN string `json:"pin"`
|
|
}
|
|
|
|
// loginResponse is the response body for POST /api/v1/auth/login.
|
|
type loginResponse struct {
|
|
Token string `json:"token"`
|
|
Operator auth.Operator `json:"operator"`
|
|
}
|
|
|
|
// HandleLogin handles POST /api/v1/auth/login.
|
|
// Authenticates an operator by PIN and returns a JWT token.
|
|
func (h *AuthHandler) HandleLogin(w http.ResponseWriter, r *http.Request) {
|
|
var req loginRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
|
|
return
|
|
}
|
|
|
|
if req.PIN == "" {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "pin is required"})
|
|
return
|
|
}
|
|
|
|
token, operator, err := h.authService.Login(r.Context(), req.PIN)
|
|
if err != nil {
|
|
switch err {
|
|
case auth.ErrInvalidPIN:
|
|
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid PIN"})
|
|
case auth.ErrTooManyAttempts:
|
|
writeJSON(w, http.StatusTooManyRequests, map[string]string{"error": "too many failed attempts, please wait"})
|
|
case auth.ErrOperatorLocked:
|
|
writeJSON(w, http.StatusTooManyRequests, map[string]string{"error": "account locked, please wait 30 minutes"})
|
|
default:
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal server error"})
|
|
}
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, loginResponse{
|
|
Token: token,
|
|
Operator: operator,
|
|
})
|
|
}
|
|
|
|
// meResponse is the response body for GET /api/v1/auth/me.
|
|
type meResponse struct {
|
|
OperatorID string `json:"operator_id"`
|
|
Role string `json:"role"`
|
|
}
|
|
|
|
// HandleMe handles GET /api/v1/auth/me.
|
|
// Returns the current operator from JWT claims.
|
|
func (h *AuthHandler) HandleMe(w http.ResponseWriter, r *http.Request) {
|
|
operatorID := middleware.OperatorID(r)
|
|
role := middleware.OperatorRole(r)
|
|
|
|
if operatorID == "" {
|
|
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "not authenticated"})
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, meResponse{
|
|
OperatorID: operatorID,
|
|
Role: role,
|
|
})
|
|
}
|
|
|
|
// HandleLogout handles POST /api/v1/auth/logout.
|
|
// JWT is stateless so this is client-side only, but the endpoint exists for
|
|
// audit logging purposes.
|
|
func (h *AuthHandler) HandleLogout(w http.ResponseWriter, r *http.Request) {
|
|
// Log the logout action for audit trail
|
|
// The actual logout happens client-side by discarding the token
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "logged out"})
|
|
}
|
|
|
|
// writeJSON is defined in templates.go (shared helper for the routes package)
|