You've already forked agentic-coding-workflow
feat(mcp): admin token CRUD HTTP handler (POST/GET/DELETE) + TokenService.List/GetByID
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,20 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
|
||||
httpx "github.com/yan1h/agent-coding-workflow/internal/transport/http"
|
||||
"github.com/yan1h/agent-coding-workflow/internal/transport/http/middleware"
|
||||
"github.com/yan1h/agent-coding-workflow/internal/user"
|
||||
)
|
||||
|
||||
// MountTransports 在 chi router 上挂载 MCP transport 端点:
|
||||
@@ -56,3 +66,186 @@ func injectTokenIDMiddleware() func(http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Admin Token CRUD HTTP Handler =====
|
||||
|
||||
// AdminHandlerDeps 装配 admin token CRUD HTTP handler 所需依赖。
|
||||
type AdminHandlerDeps struct {
|
||||
Tokens TokenService
|
||||
Users user.Service
|
||||
}
|
||||
|
||||
// MountAdmin 把 admin token CRUD 挂到 /api/v1/mcp/tokens。
|
||||
func MountAdmin(r chi.Router, deps AdminHandlerDeps) {
|
||||
r.Route("/api/v1/mcp/tokens", func(r chi.Router) {
|
||||
r.Post("/", postToken(deps))
|
||||
r.Get("/", listTokens(deps))
|
||||
r.Delete("/{id}", deleteToken(deps))
|
||||
})
|
||||
}
|
||||
|
||||
type postTokenReq struct {
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Scope Scope `json:"scope"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
}
|
||||
type postTokenResp struct {
|
||||
ID string `json:"id"`
|
||||
Token string `json:"token"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
}
|
||||
|
||||
func postToken(deps AdminHandlerDeps) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
rid := middleware.RequestIDFromContext(r.Context())
|
||||
caller, isAdmin, err := resolveSessionUser(r.Context(), deps.Users, r)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
if !isAdmin {
|
||||
httpx.WriteError(w, rid, errs.New(errs.CodeForbidden, "admin only"))
|
||||
return
|
||||
}
|
||||
var req postTokenReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httpx.WriteError(w, rid, errs.Wrap(err, errs.CodeInvalidInput, "decode body"))
|
||||
return
|
||||
}
|
||||
var targetUID uuid.UUID
|
||||
if req.UserID == "" {
|
||||
targetUID = caller
|
||||
} else {
|
||||
tid, err := uuid.Parse(req.UserID)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, rid, errs.Wrap(err, errs.CodeInvalidInput, "user_id parse"))
|
||||
return
|
||||
}
|
||||
targetUID = tid
|
||||
}
|
||||
res, err := deps.Tokens.IssueAdmin(r.Context(), IssueAdminInput{
|
||||
UserID: targetUID,
|
||||
Name: req.Name,
|
||||
Scope: req.Scope,
|
||||
ExpiresAt: req.ExpiresAt,
|
||||
CreatedBy: caller,
|
||||
})
|
||||
if err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusCreated, postTokenResp{
|
||||
ID: res.ID.String(),
|
||||
Token: res.Plaintext,
|
||||
ExpiresAt: derefTime(res.ExpiresAt),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type tokenRow struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Issuer string `json:"issuer"`
|
||||
Scope Scope `json:"scope"`
|
||||
ExpiresAt *string `json:"expires_at,omitempty"`
|
||||
RevokedAt *string `json:"revoked_at,omitempty"`
|
||||
LastUsedAt *string `json:"last_used_at,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
func listTokens(deps AdminHandlerDeps) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
rid := middleware.RequestIDFromContext(r.Context())
|
||||
caller, isAdmin, err := resolveSessionUser(r.Context(), deps.Users, r)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
var callerFilter *uuid.UUID
|
||||
if !isAdmin {
|
||||
callerFilter = &caller
|
||||
}
|
||||
activeOnly := r.URL.Query().Get("active_only") == "true"
|
||||
tokens, err := deps.Tokens.List(r.Context(), callerFilter, activeOnly)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
out := make([]tokenRow, 0, len(tokens))
|
||||
for _, t := range tokens {
|
||||
row := tokenRow{
|
||||
ID: t.ID.String(), Name: t.Name,
|
||||
Issuer: string(t.Issuer), Scope: t.Scope,
|
||||
CreatedAt: t.CreatedAt.Format(time.RFC3339),
|
||||
}
|
||||
if t.ExpiresAt != nil {
|
||||
s := t.ExpiresAt.Format(time.RFC3339)
|
||||
row.ExpiresAt = &s
|
||||
}
|
||||
if t.RevokedAt != nil {
|
||||
s := t.RevokedAt.Format(time.RFC3339)
|
||||
row.RevokedAt = &s
|
||||
}
|
||||
if t.LastUsedAt != nil {
|
||||
s := t.LastUsedAt.Format(time.RFC3339)
|
||||
row.LastUsedAt = &s
|
||||
}
|
||||
out = append(out, row)
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, out)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteToken(deps AdminHandlerDeps) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
rid := middleware.RequestIDFromContext(r.Context())
|
||||
caller, _, err := resolveSessionUser(r.Context(), deps.Users, r)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
idStr := chi.URLParam(r, "id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, rid, errs.Wrap(err, errs.CodeInvalidInput, "id parse"))
|
||||
return
|
||||
}
|
||||
if err := deps.Tokens.Revoke(r.Context(), id, caller); err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
func resolveSessionUser(ctx context.Context, us user.Service, r *http.Request) (uuid.UUID, bool, error) {
|
||||
tok := extractBearer(r)
|
||||
uid, ok, err := us.ResolveSession(ctx, tok)
|
||||
if err != nil {
|
||||
return uuid.Nil, false, errs.Wrap(err, errs.CodeInternal, "resolve session")
|
||||
}
|
||||
if !ok {
|
||||
return uuid.Nil, false, errs.New(errs.CodeUnauthorized, "session invalid")
|
||||
}
|
||||
u, err := us.Get(ctx, uid)
|
||||
if err != nil {
|
||||
return uuid.Nil, false, err
|
||||
}
|
||||
return uid, u.IsAdmin, nil
|
||||
}
|
||||
|
||||
func extractBearer(r *http.Request) string {
|
||||
h := r.Header.Get("Authorization")
|
||||
if strings.HasPrefix(h, "Bearer ") {
|
||||
return strings.TrimPrefix(h, "Bearer ")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func derefTime(t *time.Time) time.Time {
|
||||
if t == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return *t
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user