ScuttleBot

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

Keyboard Shortcuts

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