|
1
|
package api |
|
2
|
|
|
3
|
import ( |
|
4
|
"encoding/json" |
|
5
|
"net/http" |
|
6
|
"time" |
|
7
|
|
|
8
|
"github.com/conflicthq/scuttlebot/internal/auth" |
|
9
|
) |
|
10
|
|
|
11
|
type createAPIKeyRequest struct { |
|
12
|
Name string `json:"name"` |
|
13
|
Scopes []string `json:"scopes"` |
|
14
|
ExpiresIn string `json:"expires_in,omitempty"` // e.g. "720h" for 30 days, empty = never |
|
15
|
} |
|
16
|
|
|
17
|
type createAPIKeyResponse struct { |
|
18
|
ID string `json:"id"` |
|
19
|
Name string `json:"name"` |
|
20
|
Token string `json:"token"` // plaintext, shown only once |
|
21
|
Scopes []auth.Scope `json:"scopes"` |
|
22
|
CreatedAt time.Time `json:"created_at"` |
|
23
|
ExpiresAt *time.Time `json:"expires_at,omitempty"` |
|
24
|
} |
|
25
|
|
|
26
|
type apiKeyListEntry struct { |
|
27
|
ID string `json:"id"` |
|
28
|
Name string `json:"name"` |
|
29
|
Scopes []auth.Scope `json:"scopes"` |
|
30
|
CreatedAt time.Time `json:"created_at"` |
|
31
|
LastUsed *time.Time `json:"last_used,omitempty"` |
|
32
|
ExpiresAt *time.Time `json:"expires_at,omitempty"` |
|
33
|
Active bool `json:"active"` |
|
34
|
} |
|
35
|
|
|
36
|
// handleListAPIKeys handles GET /v1/api-keys. |
|
37
|
func (s *Server) handleListAPIKeys(w http.ResponseWriter, r *http.Request) { |
|
38
|
keys := s.apiKeys.List() |
|
39
|
out := make([]apiKeyListEntry, len(keys)) |
|
40
|
for i, k := range keys { |
|
41
|
out[i] = apiKeyListEntry{ |
|
42
|
ID: k.ID, |
|
43
|
Name: k.Name, |
|
44
|
Scopes: k.Scopes, |
|
45
|
CreatedAt: k.CreatedAt, |
|
46
|
Active: k.Active, |
|
47
|
} |
|
48
|
if !k.LastUsed.IsZero() { |
|
49
|
t := k.LastUsed |
|
50
|
out[i].LastUsed = &t |
|
51
|
} |
|
52
|
if !k.ExpiresAt.IsZero() { |
|
53
|
t := k.ExpiresAt |
|
54
|
out[i].ExpiresAt = &t |
|
55
|
} |
|
56
|
} |
|
57
|
writeJSON(w, http.StatusOK, out) |
|
58
|
} |
|
59
|
|
|
60
|
// handleCreateAPIKey handles POST /v1/api-keys. |
|
61
|
func (s *Server) handleCreateAPIKey(w http.ResponseWriter, r *http.Request) { |
|
62
|
var req createAPIKeyRequest |
|
63
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
|
64
|
writeError(w, http.StatusBadRequest, "invalid request body") |
|
65
|
return |
|
66
|
} |
|
67
|
if req.Name == "" { |
|
68
|
writeError(w, http.StatusBadRequest, "name is required") |
|
69
|
return |
|
70
|
} |
|
71
|
|
|
72
|
scopes := make([]auth.Scope, len(req.Scopes)) |
|
73
|
for i, s := range req.Scopes { |
|
74
|
scope := auth.Scope(s) |
|
75
|
if !auth.ValidScopes[scope] { |
|
76
|
writeError(w, http.StatusBadRequest, "unknown scope: "+s) |
|
77
|
return |
|
78
|
} |
|
79
|
scopes[i] = scope |
|
80
|
} |
|
81
|
if len(scopes) == 0 { |
|
82
|
writeError(w, http.StatusBadRequest, "at least one scope is required") |
|
83
|
return |
|
84
|
} |
|
85
|
|
|
86
|
var expiresAt time.Time |
|
87
|
if req.ExpiresIn != "" { |
|
88
|
dur, err := time.ParseDuration(req.ExpiresIn) |
|
89
|
if err != nil { |
|
90
|
writeError(w, http.StatusBadRequest, "invalid expires_in duration: "+err.Error()) |
|
91
|
return |
|
92
|
} |
|
93
|
expiresAt = time.Now().Add(dur) |
|
94
|
} |
|
95
|
|
|
96
|
token, key, err := s.apiKeys.Create(req.Name, scopes, expiresAt) |
|
97
|
if err != nil { |
|
98
|
s.log.Error("create api key", "err", err) |
|
99
|
writeError(w, http.StatusInternalServerError, "failed to create API key") |
|
100
|
return |
|
101
|
} |
|
102
|
|
|
103
|
resp := createAPIKeyResponse{ |
|
104
|
ID: key.ID, |
|
105
|
Name: key.Name, |
|
106
|
Token: token, |
|
107
|
Scopes: key.Scopes, |
|
108
|
CreatedAt: key.CreatedAt, |
|
109
|
} |
|
110
|
if !key.ExpiresAt.IsZero() { |
|
111
|
t := key.ExpiresAt |
|
112
|
resp.ExpiresAt = &t |
|
113
|
} |
|
114
|
writeJSON(w, http.StatusCreated, resp) |
|
115
|
} |
|
116
|
|
|
117
|
// handleRevokeAPIKey handles DELETE /v1/api-keys/{id}. |
|
118
|
func (s *Server) handleRevokeAPIKey(w http.ResponseWriter, r *http.Request) { |
|
119
|
id := r.PathValue("id") |
|
120
|
if err := s.apiKeys.Revoke(id); err != nil { |
|
121
|
writeError(w, http.StatusNotFound, err.Error()) |
|
122
|
return |
|
123
|
} |
|
124
|
w.WriteHeader(http.StatusNoContent) |
|
125
|
} |
|
126
|
|