package httpapi import ( "errors" "net/http" "strings" "github.com/descrybe/descrybe-v2/apps/api/internal/auth" "github.com/go-chi/chi/v5" "github.com/google/uuid" "github.com/jackc/pgx/v5" ) 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) handleUpdateAPIKey(w http.ResponseWriter, r *http.Request) { cid, _ := CompanyIDFromContext(r.Context()) if !s.allowCompanyAdminOrPlatform(w, r) { return } if !s.requireFeatures(w, r, "settings.api_keys", "capability.api_access") { return } id, err := uuid.Parse(chi.URLParam(r, "id")) if err != nil { Error(w, http.StatusBadRequest, "invalid id") return } var body struct { Name string `json:"name"` } if err := DecodeJSON(r, &body); err != nil { Error(w, http.StatusBadRequest, "invalid json") return } name := strings.TrimSpace(body.Name) if name == "" { Error(w, http.StatusBadRequest, "name required") return } if len(name) > 120 { Error(w, http.StatusBadRequest, "name too long") return } var ( outID uuid.UUID outName string prefix string lastUsed any created any ) err = s.Pool.QueryRow(r.Context(), ` UPDATE api_keys SET name = $3, updated_at = now() WHERE id = $1 AND company_id = $2 AND revoked_at IS NULL RETURNING id, name, key_prefix, last_used_at, created_at`, id, cid, name).Scan(&outID, &outName, &prefix, &lastUsed, &created) if errors.Is(err, pgx.ErrNoRows) { Error(w, http.StatusNotFound, "not found") return } if err != nil { Error(w, http.StatusInternalServerError, "update failed") return } JSON(w, http.StatusOK, map[string]any{ "id": outID, "name": outName, "key_prefix": prefix, "last_used_at": lastUsed, "created_at": created, }) } 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 }