A community based topic aggregation platform built on atproto
1package aggregator
2
3import (
4 "log"
5 "net/http"
6 "time"
7
8 "Coves/internal/api/middleware"
9 "Coves/internal/core/aggregators"
10)
11
12// RevokeAPIKeyHandler handles API key revocation for aggregators
13type RevokeAPIKeyHandler struct {
14 apiKeyService aggregators.APIKeyServiceInterface
15 aggregatorService aggregators.Service
16}
17
18// NewRevokeAPIKeyHandler creates a new handler for API key revocation
19func NewRevokeAPIKeyHandler(apiKeyService aggregators.APIKeyServiceInterface, aggregatorService aggregators.Service) *RevokeAPIKeyHandler {
20 return &RevokeAPIKeyHandler{
21 apiKeyService: apiKeyService,
22 aggregatorService: aggregatorService,
23 }
24}
25
26// RevokeAPIKeyResponse represents the response when revoking an API key
27type RevokeAPIKeyResponse struct {
28 RevokedAt string `json:"revokedAt"` // ISO8601 timestamp when key was revoked
29}
30
31// HandleRevokeAPIKey handles POST /xrpc/social.coves.aggregator.revokeApiKey
32// This endpoint requires OAuth authentication and revokes the aggregator's current API key.
33// After revocation, the aggregator must complete OAuth flow again to get a new key.
34func (h *RevokeAPIKeyHandler) HandleRevokeAPIKey(w http.ResponseWriter, r *http.Request) {
35 if r.Method != http.MethodPost {
36 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
37 return
38 }
39
40 // Get authenticated DID from context (set by RequireAuth middleware)
41 userDID := middleware.GetUserDID(r)
42 if userDID == "" {
43 writeError(w, http.StatusUnauthorized, "AuthenticationRequired", "Must be authenticated to revoke API key")
44 return
45 }
46
47 // Verify the caller is a registered aggregator
48 isAggregator, err := h.aggregatorService.IsAggregator(r.Context(), userDID)
49 if err != nil {
50 log.Printf("ERROR: Failed to check aggregator status: %v", err)
51 writeError(w, http.StatusInternalServerError, "InternalServerError", "Failed to verify aggregator status")
52 return
53 }
54 if !isAggregator {
55 writeError(w, http.StatusForbidden, "AggregatorRequired", "Only registered aggregators can revoke API keys")
56 return
57 }
58
59 // Check if the aggregator has an API key to revoke
60 keyInfo, err := h.apiKeyService.GetAPIKeyInfo(r.Context(), userDID)
61 if err != nil {
62 if aggregators.IsNotFound(err) {
63 writeError(w, http.StatusNotFound, "AggregatorNotFound", "Aggregator not found")
64 return
65 }
66 log.Printf("ERROR: Failed to get API key info for %s: %v", userDID, err)
67 writeError(w, http.StatusInternalServerError, "InternalServerError", "Failed to get API key info")
68 return
69 }
70
71 if !keyInfo.HasKey {
72 writeError(w, http.StatusBadRequest, "ApiKeyNotFound", "No API key exists to revoke")
73 return
74 }
75
76 if keyInfo.IsRevoked {
77 writeError(w, http.StatusBadRequest, "ApiKeyAlreadyRevoked", "API key has already been revoked")
78 return
79 }
80
81 // Revoke the API key
82 if err := h.apiKeyService.RevokeKey(r.Context(), userDID); err != nil {
83 log.Printf("ERROR: Failed to revoke API key for %s: %v", userDID, err)
84 writeError(w, http.StatusInternalServerError, "RevocationFailed", "Failed to revoke API key")
85 return
86 }
87
88 // Return success
89 response := RevokeAPIKeyResponse{
90 RevokedAt: time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
91 }
92
93 writeJSONResponse(w, http.StatusOK, response)
94}
95
96// formatTimestamp returns current time in ISO8601 format
97func formatTimestamp() string {
98 return time.Now().UTC().Format("2006-01-02T15:04:05.000Z")
99}