A community based topic aggregation platform built on atproto
1package aggregator
2
3import (
4 "errors"
5 "log"
6 "net/http"
7
8 "Coves/internal/api/middleware"
9 "Coves/internal/core/aggregators"
10)
11
12// CreateAPIKeyHandler handles API key creation for aggregators
13type CreateAPIKeyHandler struct {
14 apiKeyService aggregators.APIKeyServiceInterface
15 aggregatorService aggregators.Service
16}
17
18// NewCreateAPIKeyHandler creates a new handler for API key creation
19func NewCreateAPIKeyHandler(apiKeyService aggregators.APIKeyServiceInterface, aggregatorService aggregators.Service) *CreateAPIKeyHandler {
20 return &CreateAPIKeyHandler{
21 apiKeyService: apiKeyService,
22 aggregatorService: aggregatorService,
23 }
24}
25
26// CreateAPIKeyResponse represents the response when creating an API key
27type CreateAPIKeyResponse struct {
28 Key string `json:"key"` // The plain-text key (shown ONCE)
29 KeyPrefix string `json:"keyPrefix"` // First 12 chars for identification
30 DID string `json:"did"` // Aggregator DID
31 CreatedAt string `json:"createdAt"` // ISO8601 timestamp
32}
33
34// HandleCreateAPIKey handles POST /xrpc/social.coves.aggregator.createApiKey
35// This endpoint requires OAuth authentication and is only available to registered aggregators.
36// The API key is returned ONCE and cannot be retrieved again.
37//
38// Key Replacement: If an aggregator already has an API key, calling this endpoint will
39// generate a new key and replace the existing one. The old key will be immediately
40// invalidated and all future requests using the old key will fail authentication.
41func (h *CreateAPIKeyHandler) HandleCreateAPIKey(w http.ResponseWriter, r *http.Request) {
42 if r.Method != http.MethodPost {
43 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
44 return
45 }
46
47 // Get authenticated DID from context (set by RequireAuth middleware)
48 userDID := middleware.GetUserDID(r)
49 if userDID == "" {
50 writeError(w, http.StatusUnauthorized, "AuthenticationRequired", "Must be authenticated to create API key")
51 return
52 }
53
54 // Verify the caller is a registered aggregator
55 isAggregator, err := h.aggregatorService.IsAggregator(r.Context(), userDID)
56 if err != nil {
57 log.Printf("ERROR: Failed to check aggregator status: %v", err)
58 writeError(w, http.StatusInternalServerError, "InternalServerError", "Failed to verify aggregator status")
59 return
60 }
61 if !isAggregator {
62 writeError(w, http.StatusForbidden, "AggregatorRequired", "Only registered aggregators can create API keys")
63 return
64 }
65
66 // Get the OAuth session from context
67 oauthSession := middleware.GetOAuthSession(r)
68 if oauthSession == nil {
69 writeError(w, http.StatusUnauthorized, "OAuthSessionRequired", "OAuth session required to create API key")
70 return
71 }
72
73 // Generate the API key
74 plainKey, keyPrefix, err := h.apiKeyService.GenerateKey(r.Context(), userDID, oauthSession)
75 if err != nil {
76 log.Printf("ERROR: Failed to generate API key for %s: %v", userDID, err)
77
78 // Differentiate error types for appropriate HTTP status codes
79 switch {
80 case aggregators.IsNotFound(err):
81 // Aggregator not found in database - should not happen if IsAggregator check passed
82 writeError(w, http.StatusForbidden, "AggregatorRequired", "User is not a registered aggregator")
83 case errors.Is(err, aggregators.ErrOAuthSessionMismatch):
84 // OAuth session DID doesn't match the requested aggregator DID
85 writeError(w, http.StatusBadRequest, "SessionMismatch", "OAuth session does not match the requested aggregator")
86 default:
87 // All other errors are internal server errors
88 writeError(w, http.StatusInternalServerError, "KeyGenerationFailed", "Failed to generate API key")
89 }
90 return
91 }
92
93 // Return the key (shown ONCE only)
94 response := CreateAPIKeyResponse{
95 Key: plainKey,
96 KeyPrefix: keyPrefix,
97 DID: userDID,
98 CreatedAt: formatTimestamp(),
99 }
100
101 writeJSONResponse(w, http.StatusOK, response)
102}