Files
silo-server/internal/api/handlers/api_keys.go

292 lines
8.2 KiB
Go

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))
}