Files
sub2api-cn-relay-manager/internal/app/key_self_service.go
phamnazage-jpg 596a2a110c
Some checks failed
CI / Build & Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Release (push) Has been cancelled
feat(vnext2): add user key self-service skeleton
- PORTAL_KEY_EXPERIENCE.md: review from pending to approved
- KEY_SELF_SERVICE_API.md: review from pending to approved
- 0015_user_keys.sql: migration for key_records table
- user_keys_repo.go + test: SQLite repo (Create/ListByOwner/GetByID/UpdateStatus)
- key_self_service.go: HTTP handlers (POST/GET /api/keys, pause/resume/delete)
- key_self_service_svc.go: action wiring (buildUserKeyHandler)
- registered in ActionSet + NewAPIHandlerWithAuth

Note: full user auth requires host+CRM co-deployment.
Current skeleton accepts Bearer token for testing.
2026-06-05 11:45:17 +08:00

193 lines
5.4 KiB
Go

package app
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"net/http"
"strings"
)
func generatePlaintextKey() (string, string) {
buf := make([]byte, 32)
rand.Read(buf)
plaintext := "sk-" + hex.EncodeToString(buf)
hash := sha256.Sum256([]byte(plaintext))
return plaintext, "sha256:" + hex.EncodeToString(hash[:])
}
type UserKeyHandler struct {
createFn func(ctx context.Context, req CreateUserKeyRequest) (CreateUserKeyResponse, error)
listFn func(ctx context.Context, subjectID string) ([]UserKeyMeta, error)
getFn func(ctx context.Context, keyID, subjectID string) (UserKeyMeta, error)
pauseFn func(ctx context.Context, keyID, subjectID, reason string) (UserKeyMeta, error)
resumeFn func(ctx context.Context, keyID, subjectID string) (UserKeyMeta, error)
deleteFn func(ctx context.Context, keyID, subjectID string) error
}
type CreateUserKeyRequest struct {
LogicalGroupID string `json:"logical_group_id"`
DisplayName string `json:"display_name"`
AllowedModels []string `json:"allowed_models"`
SubjectID string `json:"-"`
}
type CreateUserKeyResponse struct {
Key UserKeyMeta `json:"key"`
PlaintextKey string `json:"plaintext_key,omitempty"`
}
type UserKeyMeta struct {
KeyID string `json:"key_id"`
MaskedPreview string `json:"masked_preview"`
DisplayName string `json:"display_name"`
LogicalGroupID string `json:"logical_group_id"`
AllowedModels []string `json:"allowed_models"`
AdminStatus string `json:"admin_status"`
QuotaStatus string `json:"quota_status"`
LastUsedAt string `json:"last_used_at,omitempty"`
CreatedAt string `json:"created_at"`
ExpiresAt string `json:"expires_at,omitempty"`
}
func (h *UserKeyHandler) extractSubjectID(r *http.Request) (string, *httpError) {
if hdr := r.Header.Get("Authorization"); strings.HasPrefix(hdr, "Bearer ") {
token := strings.TrimPrefix(hdr, "Bearer ")
if token != "" {
n := 8
if len(token) < n {
n = len(token)
}
return "skeleton_user_" + token[:n], nil
}
}
return "", &httpError{StatusCode: http.StatusUnauthorized, Code: "unauthorized", Message: "user credentials required"}
}
func writeSvcNotImplError(w http.ResponseWriter) {
writeHTTPError(w, &httpError{StatusCode: http.StatusNotImplemented, Code: "not_implemented", Message: "user key service not configured"})
}
func handleCreateUserKey(w http.ResponseWriter, r *http.Request, h *UserKeyHandler) {
if h == nil || h.createFn == nil {
writeSvcNotImplError(w)
return
}
subjectID, httpErr := h.extractSubjectID(r)
if httpErr != nil {
writeHTTPError(w, httpErr)
return
}
var req CreateUserKeyRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeHTTPError(w, &httpError{StatusCode: http.StatusBadRequest, Code: "invalid_json", Message: err.Error()})
return
}
req.SubjectID = subjectID
resp, svcErr := h.createFn(r.Context(), req)
if svcErr != nil {
writeHTTPError(w, classifyError(svcErr))
return
}
writeJSON(w, http.StatusCreated, resp)
}
func handleListUserKeys(w http.ResponseWriter, r *http.Request, h *UserKeyHandler) {
if h == nil || h.listFn == nil {
writeSvcNotImplError(w)
return
}
subjectID, httpErr := h.extractSubjectID(r)
if httpErr != nil {
writeHTTPError(w, httpErr)
return
}
keys, svcErr := h.listFn(r.Context(), subjectID)
if svcErr != nil {
writeHTTPError(w, classifyError(svcErr))
return
}
writeJSON(w, http.StatusOK, map[string]any{"keys": keys})
}
func handleGetUserKey(w http.ResponseWriter, r *http.Request, h *UserKeyHandler) {
if h == nil || h.getFn == nil {
writeSvcNotImplError(w)
return
}
subjectID, httpErr := h.extractSubjectID(r)
if httpErr != nil {
writeHTTPError(w, httpErr)
return
}
keyID := r.PathValue("key_id")
if keyID == "" {
writeHTTPError(w, &httpError{StatusCode: http.StatusBadRequest, Code: "missing_key_id", Message: "key_id required"})
return
}
key, svcErr := h.getFn(r.Context(), keyID, subjectID)
if svcErr != nil {
writeHTTPError(w, classifyError(svcErr))
return
}
writeJSON(w, http.StatusOK, key)
}
func handlePauseUserKey(w http.ResponseWriter, r *http.Request, h *UserKeyHandler) {
if h == nil || h.pauseFn == nil {
writeSvcNotImplError(w)
return
}
subjectID, httpErr := h.extractSubjectID(r)
if httpErr != nil {
writeHTTPError(w, httpErr)
return
}
keyID := r.PathValue("key_id")
key, svcErr := h.pauseFn(r.Context(), keyID, subjectID, "")
if svcErr != nil {
writeHTTPError(w, classifyError(svcErr))
return
}
writeJSON(w, http.StatusOK, key)
}
func handleResumeUserKey(w http.ResponseWriter, r *http.Request, h *UserKeyHandler) {
if h == nil || h.resumeFn == nil {
writeSvcNotImplError(w)
return
}
subjectID, httpErr := h.extractSubjectID(r)
if httpErr != nil {
writeHTTPError(w, httpErr)
return
}
keyID := r.PathValue("key_id")
key, svcErr := h.resumeFn(r.Context(), keyID, subjectID)
if svcErr != nil {
writeHTTPError(w, classifyError(svcErr))
return
}
writeJSON(w, http.StatusOK, key)
}
func handleDeleteUserKey(w http.ResponseWriter, r *http.Request, h *UserKeyHandler) {
if h == nil || h.deleteFn == nil {
writeSvcNotImplError(w)
return
}
subjectID, httpErr := h.extractSubjectID(r)
if httpErr != nil {
writeHTTPError(w, httpErr)
return
}
keyID := r.PathValue("key_id")
if svcErr := h.deleteFn(r.Context(), keyID, subjectID); svcErr != nil {
writeHTTPError(w, classifyError(svcErr))
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
}