package handlers import ( "encoding/json" "net/http" "github.com/go-chi/chi/v5" "github.com/orchard9/rdev/internal/auth" "github.com/orchard9/rdev/pkg/api" ) // KeysHandler handles API key management endpoints. type KeysHandler struct { authService *auth.Service } // NewKeysHandler creates a new keys handler. func NewKeysHandler(authService *auth.Service) *KeysHandler { return &KeysHandler{authService: authService} } // Mount registers the keys routes. func (h *KeysHandler) Mount(r api.Router) { r.Route("/keys", func(r chi.Router) { // All key endpoints require authentication (handled by global middleware) r.With(auth.RequireScope(auth.ScopeKeysRead, auth.ScopeAdmin)).Get("/", h.List) r.With(auth.RequireScope(auth.ScopeKeysWrite, auth.ScopeAdmin)).Post("/", h.Create) r.With(auth.RequireScope(auth.ScopeKeysRead, auth.ScopeAdmin)).Get("/{id}", h.Get) r.With(auth.RequireScope(auth.ScopeKeysWrite, auth.ScopeAdmin)).Delete("/{id}", h.Revoke) }) } // CreateKeyRequest is the JSON body for creating a key. type CreateKeyRequest struct { Name string `json:"name"` Scopes []string `json:"scopes"` ProjectIDs []string `json:"project_ids,omitempty"` // null = all projects ExpiresIn string `json:"expires_in,omitempty"` // "30d", "60d", "90d", "1y", "never" } // KeyResponse is the JSON response for a key (without secret). type KeyResponse struct { ID string `json:"id"` Name string `json:"name"` KeyPrefix string `json:"key_prefix"` Scopes []string `json:"scopes"` ProjectIDs []string `json:"project_ids,omitempty"` CreatedAt string `json:"created_at"` ExpiresAt *string `json:"expires_at,omitempty"` LastUsedAt *string `json:"last_used_at,omitempty"` RevokedAt *string `json:"revoked_at,omitempty"` CreatedBy string `json:"created_by"` Active bool `json:"active"` } // CreateKeyResponse includes the secret (shown only once). type CreateKeyResponse struct { Key KeyResponse `json:"key"` Secret string `json:"secret"` } // apiKeyToResponse converts an APIKey to a JSON response. func apiKeyToResponse(k *auth.APIKey) KeyResponse { resp := KeyResponse{ ID: k.ID, Name: k.Name, KeyPrefix: k.KeyPrefix, Scopes: auth.ScopesToStrings(k.Scopes), CreatedAt: k.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), CreatedBy: k.CreatedBy, Active: k.IsActive(), } if k.ProjectIDs != nil { resp.ProjectIDs = k.ProjectIDs } if k.ExpiresAt != nil { s := k.ExpiresAt.Format("2006-01-02T15:04:05Z07:00") resp.ExpiresAt = &s } if k.LastUsedAt != nil { s := k.LastUsedAt.Format("2006-01-02T15:04:05Z07:00") resp.LastUsedAt = &s } if k.RevokedAt != nil { s := k.RevokedAt.Format("2006-01-02T15:04:05Z07:00") resp.RevokedAt = &s } return resp } // List returns all API keys. // GET /keys func (h *KeysHandler) List(w http.ResponseWriter, r *http.Request) { keys, err := h.authService.List(r.Context()) if err != nil { api.WriteInternalError(w, r, "Failed to list keys") return } resp := make([]KeyResponse, len(keys)) for i, k := range keys { resp[i] = apiKeyToResponse(k) } api.WriteSuccess(w, r, resp) } // Create generates a new API key. // POST /keys func (h *KeysHandler) Create(w http.ResponseWriter, r *http.Request) { var req CreateKeyRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { api.WriteBadRequest(w, r, "Invalid JSON body") return } if req.Name == "" { api.WriteBadRequest(w, r, "name is required") return } if len(req.Scopes) == 0 { api.WriteBadRequest(w, r, "scopes is required") return } // Validate scopes scopes := auth.ScopesFromStrings(req.Scopes) if !auth.ValidateScopes(scopes) { api.WriteBadRequest(w, r, "invalid scope(s)") return } // Parse expiration expiresIn, err := auth.ParseExpiration(req.ExpiresIn) if err != nil { api.WriteBadRequest(w, r, err.Error()) return } // Get creator from authenticated key creator := "admin" if apiKey := auth.GetAPIKey(r.Context()); apiKey != nil && apiKey.ID != "admin" { creator = apiKey.ID } result, err := h.authService.Create(r.Context(), auth.CreateKeyRequest{ Name: req.Name, Scopes: scopes, ProjectIDs: req.ProjectIDs, ExpiresIn: expiresIn, CreatedBy: creator, }) if err != nil { api.WriteInternalError(w, r, "Failed to create key") return } api.WriteCreated(w, r, CreateKeyResponse{ Key: apiKeyToResponse(result.Key), Secret: result.Secret, }) } // Get returns a single API key. // GET /keys/{id} func (h *KeysHandler) Get(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") key, err := h.authService.Get(r.Context(), id) if err != nil { if err == auth.ErrKeyNotFound { api.WriteNotFound(w, r, "Key not found") return } api.WriteInternalError(w, r, "Failed to get key") return } api.WriteSuccess(w, r, apiKeyToResponse(key)) } // Revoke marks an API key as revoked. // DELETE /keys/{id} func (h *KeysHandler) Revoke(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") if err := h.authService.Revoke(r.Context(), id); err != nil { if err == auth.ErrKeyNotFound { api.WriteNotFound(w, r, "Key not found") return } api.WriteInternalError(w, r, "Failed to revoke key") return } api.WriteSuccess(w, r, map[string]string{ "status": "revoked", "id": id, }) }