package handlers import ( "encoding/json" "errors" "net/http" "strconv" "time" "github.com/go-chi/chi/v5" apimw "github.com/Silo-Server/silo-server/internal/api/middleware" "github.com/Silo-Server/silo-server/internal/auth" "github.com/Silo-Server/silo-server/internal/models" ) // APIKeyHandler handles API key management endpoints. type APIKeyHandler struct { repo *auth.APIKeyRepository } // NewAPIKeyHandler creates a new APIKeyHandler. func NewAPIKeyHandler(repo *auth.APIKeyRepository) *APIKeyHandler { return &APIKeyHandler{repo: repo} } // --- Request/Response types --- type createAPIKeyRequest struct { Label string `json:"label"` } type apiKeyResponse struct { ID int64 `json:"id"` UserID int `json:"user_id"` Label string `json:"label"` Key string `json:"key"` RateTier string `json:"rate_tier"` CreatedAt time.Time `json:"created_at"` LastUsedAt *time.Time `json:"last_used_at,omitempty"` } func toAPIKeyResponse(k *models.APIKey) apiKeyResponse { return apiKeyResponse{ ID: k.ID, UserID: k.UserID, Label: k.Label, Key: k.Key, RateTier: k.RateTier, CreatedAt: k.CreatedAt, LastUsedAt: k.LastUsedAt, } } type adminApiKeyResponse struct { ID int64 `json:"id"` UserID int `json:"user_id"` Username string `json:"username"` Label string `json:"label"` Key string `json:"key"` RateTier string `json:"rate_tier"` CreatedAt time.Time `json:"created_at"` LastUsedAt *time.Time `json:"last_used_at,omitempty"` } type adminCreateAPIKeyRequest struct { Label string `json:"label"` UserID *int `json:"user_id,omitempty"` } // requireJWTAuth checks that the request was authenticated with a JWT, not an API key. // Returns the claims if valid, or writes a 403 and returns nil. func requireJWTAuth(w http.ResponseWriter, r *http.Request) *auth.Claims { claims := apimw.GetClaims(r.Context()) if claims == nil { writeError(w, http.StatusUnauthorized, "unauthorized", "Authentication required") return nil } if claims.TokenType == auth.TokenTypeAPIKey { writeError(w, http.StatusForbidden, "forbidden", "API key management is not accessible via API key authentication") return nil } return claims } // HandleCreateAPIKey handles POST /api-keys. func (h *APIKeyHandler) HandleCreateAPIKey(w http.ResponseWriter, r *http.Request) { claims := requireJWTAuth(w, r) if claims == nil { return } var req createAPIKeyRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "bad_request", "Invalid request body") return } if req.Label == "" { writeError(w, http.StatusBadRequest, "bad_request", "Label is required") return } key, err := h.repo.Create(r.Context(), claims.UserID, req.Label) if err != nil { writeError(w, http.StatusInternalServerError, "internal_error", "Failed to create API key") return } writeJSON(w, http.StatusCreated, toAPIKeyResponse(key)) } // HandleListAPIKeys handles GET /api-keys. func (h *APIKeyHandler) HandleListAPIKeys(w http.ResponseWriter, r *http.Request) { claims := requireJWTAuth(w, r) if claims == nil { return } keys, err := h.repo.ListByUser(r.Context(), claims.UserID) if err != nil { writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list API keys") return } resp := make([]apiKeyResponse, 0, len(keys)) for _, k := range keys { resp = append(resp, toAPIKeyResponse(k)) } writeJSON(w, http.StatusOK, resp) } // HandleDeleteAPIKey handles DELETE /api-keys/{id}. func (h *APIKeyHandler) HandleDeleteAPIKey(w http.ResponseWriter, r *http.Request) { claims := requireJWTAuth(w, r) if claims == nil { return } id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "bad_request", "Invalid API key ID") return } if err := h.repo.Delete(r.Context(), id, claims.UserID); err != nil { if errors.Is(err, auth.ErrAPIKeyNotFound) { writeError(w, http.StatusNotFound, "not_found", "API key not found") return } writeError(w, http.StatusInternalServerError, "internal_error", "Failed to delete API key") return } w.WriteHeader(http.StatusNoContent) } // HandleAdminListUserAPIKeys handles GET /admin/users/{userId}/api-keys. func (h *APIKeyHandler) HandleAdminListUserAPIKeys(w http.ResponseWriter, r *http.Request) { userID, err := strconv.Atoi(chi.URLParam(r, "userId")) if err != nil { writeError(w, http.StatusBadRequest, "bad_request", "Invalid user ID") return } keys, err := h.repo.ListByUserAdmin(r.Context(), userID) if err != nil { writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list API keys") return } resp := make([]apiKeyResponse, 0, len(keys)) for _, k := range keys { resp = append(resp, toAPIKeyResponse(k)) } writeJSON(w, http.StatusOK, resp) } // HandleAdminDeleteAPIKey handles DELETE /admin/api-keys/{id}. func (h *APIKeyHandler) HandleAdminDeleteAPIKey(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "bad_request", "Invalid API key ID") return } if err := h.repo.DeleteByAdmin(r.Context(), id); err != nil { if errors.Is(err, auth.ErrAPIKeyNotFound) { writeError(w, http.StatusNotFound, "not_found", "API key not found") return } writeError(w, http.StatusInternalServerError, "internal_error", "Failed to delete API key") return } w.WriteHeader(http.StatusNoContent) } // HandleAdminListAllAPIKeys handles GET /admin/api-keys. func (h *APIKeyHandler) HandleAdminListAllAPIKeys(w http.ResponseWriter, r *http.Request) { keys, err := h.repo.ListAll(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list API keys") return } resp := make([]adminApiKeyResponse, 0, len(keys)) for _, k := range keys { resp = append(resp, adminApiKeyResponse{ ID: k.ID, UserID: k.UserID, Username: k.Username, Label: k.Label, Key: k.Key, RateTier: k.RateTier, CreatedAt: k.CreatedAt, LastUsedAt: k.LastUsedAt, }) } writeJSON(w, http.StatusOK, resp) } // HandleAdminUpdateTier handles PUT /admin/api-keys/{id}/tier. func (h *APIKeyHandler) HandleAdminUpdateTier(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "bad_request", "Invalid API key ID") return } var req struct { Tier string `json:"tier"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "bad_request", "Invalid request body") return } if req.Tier != "standard" && req.Tier != "elevated" { writeError(w, http.StatusBadRequest, "bad_request", "Tier must be 'standard' or 'elevated'") return } if err := h.repo.UpdateTier(r.Context(), id, req.Tier); err != nil { if errors.Is(err, auth.ErrAPIKeyNotFound) { writeError(w, http.StatusNotFound, "not_found", "API key not found") return } writeError(w, http.StatusInternalServerError, "internal_error", "Failed to update tier") return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) } // HandleAdminCreateAPIKey handles POST /admin/api-keys. func (h *APIKeyHandler) HandleAdminCreateAPIKey(w http.ResponseWriter, r *http.Request) { claims := apimw.GetClaims(r.Context()) if claims == nil { writeError(w, http.StatusUnauthorized, "unauthorized", "Authentication required") return } var req adminCreateAPIKeyRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "bad_request", "Invalid request body") return } if req.Label == "" { writeError(w, http.StatusBadRequest, "bad_request", "Label is required") return } targetUserID := claims.UserID if req.UserID != nil { targetUserID = *req.UserID } key, err := h.repo.Create(r.Context(), targetUserID, req.Label) if err != nil { writeError(w, http.StatusInternalServerError, "internal_error", "Failed to create API key") return } writeJSON(w, http.StatusCreated, toAPIKeyResponse(key)) }