🧪 test: add JWT secret rotation BDD scenarios and step implementations (#12)
✨ merge: implement JWT secret rotation with BDD scenario isolation - Implement JWT secret rotation mechanism (closes #8) - Add per-scenario state isolation for BDD tests (closes #14) - Validate password reset workflow via BDD tests (closes #7) - Fix port conflicts in test validation - Add state tracer for debugging test execution - Document BDD isolation strategies in ADR 0025 - Fix PostgreSQL configuration environment variables Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai> Co-authored-by: Gabriel Radureau <arcodange@gmail.com> Co-committed-by: Gabriel Radureau <arcodange@gmail.com>
This commit was merged in pull request #12.
This commit is contained in:
149
pkg/user/api/admin_handler.go
Normal file
149
pkg/user/api/admin_handler.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"dance-lessons-coach/pkg/user"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// AdminHandler handles admin-related HTTP requests
|
||||
type AdminHandler struct {
|
||||
authService user.AuthService
|
||||
}
|
||||
|
||||
// NewAdminHandler creates a new admin handler
|
||||
func NewAdminHandler(authService user.AuthService) *AdminHandler {
|
||||
return &AdminHandler{
|
||||
authService: authService,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes registers admin routes
|
||||
func (h *AdminHandler) RegisterRoutes(router chi.Router) {
|
||||
router.Route("/jwt", func(r chi.Router) {
|
||||
r.Post("/secrets", h.handleAddJWTSecret)
|
||||
r.Post("/secrets/rotate", h.handleRotateJWTSecret)
|
||||
})
|
||||
}
|
||||
|
||||
// AddJWTSecretRequest represents a request to add a new JWT secret
|
||||
type AddJWTSecretRequest struct {
|
||||
Secret string `json:"secret" validate:"required,min=16"`
|
||||
IsPrimary bool `json:"is_primary"`
|
||||
ExpiresIn int64 `json:"expires_in"` // Expiration time in hours
|
||||
}
|
||||
|
||||
// handleAddJWTSecret godoc
|
||||
//
|
||||
// @Summary Add JWT secret
|
||||
// @Description Add a new JWT secret for rotation purposes
|
||||
// @Tags API/v1/Admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body AddJWTSecretRequest true "JWT secret details"
|
||||
// @Success 200 {object} map[string]string "Secret added successfully"
|
||||
// @Failure 400 {object} map[string]string "Invalid request"
|
||||
// @Failure 401 {object} map[string]string "Unauthorized"
|
||||
// @Failure 500 {object} map[string]string "Server error"
|
||||
// @Router /v1/admin/jwt/secrets [post]
|
||||
func (h *AdminHandler) handleAddJWTSecret(w http.ResponseWriter, r *http.Request) {
|
||||
// Decode request body into a map to handle flexible boolean parsing
|
||||
var body map[string]interface{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
http.Error(w, `{"error":"invalid_request","message":"Invalid JSON request body"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract and validate fields
|
||||
secret, ok := body["secret"].(string)
|
||||
if !ok || secret == "" {
|
||||
http.Error(w, `{"error":"invalid_request","message":"secret is required and must be a string"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle is_primary as either bool or string
|
||||
isPrimary := false // default
|
||||
if val, exists := body["is_primary"]; exists {
|
||||
switch v := val.(type) {
|
||||
case bool:
|
||||
isPrimary = v
|
||||
case string:
|
||||
isPrimary = v == "true"
|
||||
default:
|
||||
http.Error(w, `{"error":"invalid_request","message":"is_primary must be a boolean or string"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Handle expires_in as either int64 or float64 (JSON numbers)
|
||||
expiresInHours := int64(0)
|
||||
if val, exists := body["expires_in"]; exists {
|
||||
switch v := val.(type) {
|
||||
case int64:
|
||||
expiresInHours = v
|
||||
case float64:
|
||||
expiresInHours = int64(v)
|
||||
default:
|
||||
http.Error(w, `{"error":"invalid_request","message":"expires_in must be a number"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Convert expires_in from hours to time.Duration
|
||||
expiresIn := time.Duration(expiresInHours) * time.Hour
|
||||
if expiresIn <= 0 {
|
||||
// If expires_in is 0 or not provided, set to no expiration for secondary secrets
|
||||
// For primary secrets, use a reasonable default
|
||||
if isPrimary {
|
||||
expiresIn = 24 * 365 * time.Hour // 1 year for primary secrets
|
||||
} else {
|
||||
expiresIn = 0 // No expiration for secondary secrets
|
||||
}
|
||||
}
|
||||
|
||||
// Add the secret to the manager
|
||||
h.authService.AddJWTSecret(secret, isPrimary, expiresIn)
|
||||
|
||||
// Return success
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "JWT secret added successfully"})
|
||||
}
|
||||
|
||||
// RotateJWTSecretRequest represents a request to rotate JWT secrets
|
||||
type RotateJWTSecretRequest struct {
|
||||
NewSecret string `json:"new_secret" validate:"required,min=16"`
|
||||
}
|
||||
|
||||
// handleRotateJWTSecret godoc
|
||||
//
|
||||
// @Summary Rotate JWT secret
|
||||
// @Description Rotate to a new primary JWT secret
|
||||
// @Tags API/v1/Admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body RotateJWTSecretRequest true "New JWT secret"
|
||||
// @Success 200 {object} map[string]string "Secret rotated successfully"
|
||||
// @Failure 400 {object} map[string]string "Invalid request"
|
||||
// @Failure 401 {object} map[string]string "Unauthorized"
|
||||
// @Failure 500 {object} map[string]string "Server error"
|
||||
// @Router /v1/admin/jwt/secrets/rotate [post]
|
||||
func (h *AdminHandler) handleRotateJWTSecret(w http.ResponseWriter, r *http.Request) {
|
||||
var req RotateJWTSecretRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, `{"error":"invalid_request","message":"Invalid JSON request body"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Rotate to the new secret
|
||||
h.authService.RotateJWTSecret(req.NewSecret)
|
||||
|
||||
// Return success
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "JWT secret rotated successfully"})
|
||||
}
|
||||
Reference in New Issue
Block a user