ScuttleBot

scuttlebot / internal / api / apikeys.go
Blame History Raw 126 lines
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

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button