Files
descrybe/apps/api/internal/httpapi/apikey_handlers.go
T
greeneclipse 8580c996c3 Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
2026-08-09 22:47:43 +02:00

113 lines
3.2 KiB
Go

package httpapi
import (
"net/http"
"github.com/descrybe/descrybe-v2/apps/api/internal/auth"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
)
func (s *Server) handleListAPIKeys(w http.ResponseWriter, r *http.Request) {
cid, _ := CompanyIDFromContext(r.Context())
limit, offset := ParseLimitOffset(r)
const where = "company_id = $1 AND revoked_at IS NULL"
var total int64
if err := s.Pool.QueryRow(r.Context(), "SELECT count(*) FROM api_keys WHERE "+where, cid).Scan(&total); err != nil {
Error(w, http.StatusInternalServerError, "list failed")
return
}
rows, err := s.Pool.Query(r.Context(), `
SELECT id, name, key_prefix, last_used_at, created_at
FROM api_keys WHERE `+where+`
ORDER BY created_at DESC LIMIT $2 OFFSET $3`, cid, limit, offset)
if err != nil {
Error(w, http.StatusInternalServerError, "list failed")
return
}
defer rows.Close()
out := make([]map[string]any, 0)
for rows.Next() {
var id uuid.UUID
var name *string
var prefix string
var lastUsed, created any
if err := rows.Scan(&id, &name, &prefix, &lastUsed, &created); err != nil {
Error(w, http.StatusInternalServerError, "scan failed")
return
}
out = append(out, map[string]any{
"id": id, "name": name, "key_prefix": prefix, "last_used_at": lastUsed, "created_at": created,
})
}
JSON(w, http.StatusOK, map[string]any{"api_keys": out, "total": total, "limit": limit, "offset": offset})
}
func (s *Server) handleCreateAPIKey(w http.ResponseWriter, r *http.Request) {
cid, _ := CompanyIDFromContext(r.Context())
uid, _ := UserIDFromContext(r.Context())
if !s.allowCompanyAdminOrPlatform(w, r) {
return
}
if !s.requireFeatures(w, r, "settings.api_keys", "capability.api_access") {
return
}
var body struct {
Name string `json:"name"`
}
if err := DecodeJSON(r, &body); err != nil {
Error(w, http.StatusBadRequest, "invalid json")
return
}
raw, err := auth.RandomToken(24)
if err != nil {
Error(w, http.StatusInternalServerError, "key gen failed")
return
}
full := "dk_" + raw
prefix := full[:10]
var id uuid.UUID
err = s.Pool.QueryRow(r.Context(), `
INSERT INTO api_keys (company_id, user_id, name, key_hash, key_prefix)
VALUES ($1, $2, $3, $4, $5) RETURNING id`,
cid, uid, nullIfEmpty(body.Name), auth.HashAPIKey(full), prefix).Scan(&id)
if err != nil {
Error(w, http.StatusInternalServerError, "create failed")
return
}
JSON(w, http.StatusCreated, map[string]any{
"id": id, "name": body.Name, "key": full, "key_prefix": prefix,
})
}
func (s *Server) handleRevokeAPIKey(w http.ResponseWriter, r *http.Request) {
cid, _ := CompanyIDFromContext(r.Context())
if !s.allowCompanyAdminOrPlatform(w, r) {
return
}
id, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
Error(w, http.StatusBadRequest, "invalid id")
return
}
tag, err := s.Pool.Exec(r.Context(), `
UPDATE api_keys SET revoked_at = now(), updated_at = now()
WHERE id = $1 AND company_id = $2 AND revoked_at IS NULL`, id, cid)
if err != nil {
Error(w, http.StatusInternalServerError, "revoke failed")
return
}
if tag.RowsAffected() == 0 {
Error(w, http.StatusNotFound, "not found")
return
}
JSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func nullIfEmpty(s string) *string {
if s == "" {
return nil
}
return &s
}