From 78e74c97348d32b1fec95bd789aae6b814f45183 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Sat, 2 May 2026 08:36:45 +0800 Subject: [PATCH] feat(workspace): http handler with workspace/worktree/gitops endpoints --- internal/workspace/dto.go | 117 +++++++ internal/workspace/handler.go | 519 +++++++++++++++++++++++++++++ internal/workspace/handler_test.go | 84 +++++ 3 files changed, 720 insertions(+) create mode 100644 internal/workspace/dto.go create mode 100644 internal/workspace/handler.go create mode 100644 internal/workspace/handler_test.go diff --git a/internal/workspace/dto.go b/internal/workspace/dto.go new file mode 100644 index 0000000..093be2b --- /dev/null +++ b/internal/workspace/dto.go @@ -0,0 +1,117 @@ +// dto.go 定义 workspace 模块 HTTP 层的请求 / 响应结构以及与领域模型的映射函数。 +// 设计取舍: +// - 请求 DTO 用 JSON tag 与前端约定的 snake_case 对齐;可选 patch 字段用 *T 区分"不传"和"显式置空"。 +// - 响应 DTO 不暴露 secret/main_path 等敏感或内部字段,避免泄漏。 +package workspace + +import ( + "time" + + "github.com/google/uuid" + + "github.com/yan1h/agent-coding-workflow/internal/infra/git" +) + +// ===== Request DTO ===== + +type createWorkspaceReq struct { + Slug string `json:"slug"` + Name string `json:"name"` + Description string `json:"description"` + GitRemoteURL string `json:"git_remote_url"` + DefaultBranch string `json:"default_branch"` + Credential credentialBody `json:"credential"` +} + +type updateWorkspaceReq struct { + Name *string `json:"name"` + Description *string `json:"description"` + DefaultBranch *string `json:"default_branch"` +} + +type credentialBody struct { + Kind string `json:"kind"` + Username string `json:"username"` + Secret string `json:"secret"` +} + +type createWorktreeReq struct { + Branch string `json:"branch"` + BaseBranch string `json:"base_branch"` +} + +type commitReq struct { + Message string `json:"message"` + Push *bool `json:"push"` + AddAll *bool `json:"add_all"` +} + +// ===== Response DTO ===== + +type workspaceResp struct { + ID uuid.UUID `json:"id"` + ProjectID uuid.UUID `json:"project_id"` + Slug string `json:"slug"` + Name string `json:"name"` + Description string `json:"description"` + GitRemoteURL string `json:"git_remote_url"` + DefaultBranch string `json:"default_branch"` + SyncStatus string `json:"sync_status"` + LastSyncedAt *time.Time `json:"last_synced_at"` + LastSyncError string `json:"last_sync_error"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func toWorkspaceResp(w *Workspace) workspaceResp { + return workspaceResp{ + ID: w.ID, ProjectID: w.ProjectID, Slug: w.Slug, Name: w.Name, + Description: w.Description, GitRemoteURL: w.GitRemoteURL, DefaultBranch: w.DefaultBranch, + SyncStatus: string(w.SyncStatus), LastSyncedAt: w.LastSyncedAt, LastSyncError: w.LastSyncError, + CreatedAt: w.CreatedAt, UpdatedAt: w.UpdatedAt, + } +} + +type worktreeResp struct { + ID uuid.UUID `json:"id"` + WorkspaceID uuid.UUID `json:"workspace_id"` + Branch string `json:"branch"` + Path string `json:"path"` + Status string `json:"status"` + ActiveHolder string `json:"active_holder"` + AcquiredAt *time.Time `json:"acquired_at"` + LastUsedAt time.Time `json:"last_used_at"` + CreatedAt time.Time `json:"created_at"` +} + +func toWorktreeResp(w *Worktree) worktreeResp { + return worktreeResp{ + ID: w.ID, WorkspaceID: w.WorkspaceID, Branch: w.Branch, Path: w.Path, + Status: string(w.Status), ActiveHolder: w.ActiveHolder, AcquiredAt: w.AcquiredAt, + LastUsedAt: w.LastUsedAt, CreatedAt: w.CreatedAt, + } +} + +type credentialResp struct { + Kind string `json:"kind"` + Username string `json:"username"` + Fingerprint string `json:"fingerprint"` +} + +func toCredentialResp(c *Credential) credentialResp { + return credentialResp{Kind: string(c.Kind), Username: c.Username, Fingerprint: c.Fingerprint} +} + +type fileStatusResp struct { + Path string `json:"path"` + IndexX string `json:"index_x"` + WorktreeY string `json:"worktree_y"` +} + +func toFileStatusResp(f git.FileStatus) fileStatusResp { + return fileStatusResp{Path: f.Path, IndexX: string(f.IndexX), WorktreeY: string(f.WorktreeY)} +} + +type commitResp struct { + SHA string `json:"sha"` +} diff --git a/internal/workspace/handler.go b/internal/workspace/handler.go new file mode 100644 index 0000000..0b8648b --- /dev/null +++ b/internal/workspace/handler.go @@ -0,0 +1,519 @@ +// handler.go 暴露 workspace 模块 HTTP 端点(spec 4.1–4.4 节)。沿 PM 模块(internal/project) +// 的 chi.Router 模式:单个 Handler 持有 3 个 service,Mount 用 sub-router 注册路由。 +// +// 鉴权:复用 transport/http/middleware.Auth + 窄接口 AdminLookup 解析 is_admin。 +// 错误格式:复用 httpx.WriteError 输出 Google API 风格 {code, message, request_id}。 +package workspace + +import ( + "context" + "encoding/json" + "errors" + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + + "github.com/yan1h/agent-coding-workflow/internal/infra/errs" + "github.com/yan1h/agent-coding-workflow/internal/infra/git" + httpx "github.com/yan1h/agent-coding-workflow/internal/transport/http" + "github.com/yan1h/agent-coding-workflow/internal/transport/http/middleware" +) + +// AdminLookup 根据 user id 判 is_admin。 +// 沿 internal/project 同名窄接口,避免对 user 包具体类型的强依赖(也方便测试注入桩)。 +type AdminLookup interface { + IsAdmin(ctx context.Context, id uuid.UUID) (bool, error) +} + +// Handler 同时持有 3 个 service,并在 Mount 时装上 Auth 中间件。 +type Handler struct { + ws WorkspaceService + wt WorktreeService + gop GitOpsService + resolver middleware.SessionResolver + users AdminLookup +} + +// NewHandler 构造 Handler。resolver 与 users 在 Mount 注册的 Auth 中间件中使用。 +func NewHandler(ws WorkspaceService, wt WorktreeService, gop GitOpsService, resolver middleware.SessionResolver, users AdminLookup) *Handler { + return &Handler{ws: ws, wt: wt, gop: gop, resolver: resolver, users: users} +} + +// Mount 把所有 workspace / worktree / gitops 路由挂到 r 上,统一装入 Auth 中间件。 +func (h *Handler) Mount(r chi.Router) { + r.Route("/api/v1", func(r chi.Router) { + r.Use(middleware.Auth(h.resolver, middleware.AuthOptions{})) + r.Route("/projects/{projectSlug}/workspaces", func(r chi.Router) { + r.Post("/", h.createWorkspace) + r.Get("/", h.listWorkspaces) + }) + r.Route("/workspaces/{wsID}", func(r chi.Router) { + r.Get("/", h.getWorkspace) + r.Patch("/", h.updateWorkspace) + r.Delete("/", h.deleteWorkspace) + r.Post("/sync", h.syncWorkspace) + r.Put("/credential", h.upsertCredential) + r.Get("/credential", h.getCredentialMeta) + + r.Get("/main/status", h.statusOnMain) + r.Post("/main/commit", h.commitOnMain) + r.Post("/main/push", h.pushOnMain) + + r.Get("/worktrees", h.listWorktrees) + r.Post("/worktrees", h.createWorktree) + }) + r.Route("/worktrees/{wtID}", func(r chi.Router) { + r.Delete("/", h.deleteWorktree) + r.Post("/acquire", h.acquireWorktree) + r.Post("/release", h.releaseWorktree) + r.Get("/status", h.statusOnWorktree) + r.Post("/commit", h.commitOnWorktree) + r.Post("/push", h.pushOnWorktree) + }) + }) +} + +// caller 从 ctx 取出 uid 并查 is_admin,组装 Caller。 +// Auth 中间件已经把 401 兜在前面;此处的"未认证"检查是双保险。 +func (h *Handler) caller(r *http.Request) (Caller, error) { + uid, ok := middleware.UserIDFromContext(r.Context()) + if !ok { + return Caller{}, errs.New(errs.CodeUnauthorized, "未认证") + } + isAdmin, err := h.users.IsAdmin(r.Context(), uid) + if err != nil { + return Caller{}, err + } + return Caller{UserID: uid, IsAdmin: isAdmin}, nil +} + +func writeJSON(w http.ResponseWriter, status int, body any) { + httpx.WriteJSON(w, status, body) +} + +func writeErr(w http.ResponseWriter, r *http.Request, err error) { + httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()), err) +} + +// parseUUID 解析 URL 中的 uuid path 参数;失败时返回 invalid_input。 +func parseUUID(s string) (uuid.UUID, error) { + id, err := uuid.Parse(s) + if err != nil { + return uuid.Nil, errs.Wrap(err, errs.CodeInvalidInput, "invalid uuid") + } + return id, nil +} + +// ===== workspace CRUD ===== + +func (h *Handler) createWorkspace(w http.ResponseWriter, r *http.Request) { + c, err := h.caller(r) + if err != nil { + writeErr(w, r, err) + return + } + var req createWorkspaceReq + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeErr(w, r, errs.Wrap(err, errs.CodeInvalidInput, "decode body")) + return + } + cred := UpsertCredentialInput{ + Kind: CredentialKind(req.Credential.Kind), + Username: req.Credential.Username, + Secret: []byte(req.Credential.Secret), + } + in := CreateWorkspaceInput{ + Slug: req.Slug, Name: req.Name, Description: req.Description, + GitRemoteURL: req.GitRemoteURL, DefaultBranch: req.DefaultBranch, Credential: cred, + } + ws, err := h.ws.Create(r.Context(), c, chi.URLParam(r, "projectSlug"), in) + if err != nil { + writeErr(w, r, err) + return + } + writeJSON(w, http.StatusCreated, toWorkspaceResp(ws)) +} + +func (h *Handler) listWorkspaces(w http.ResponseWriter, r *http.Request) { + c, err := h.caller(r) + if err != nil { + writeErr(w, r, err) + return + } + out, err := h.ws.List(r.Context(), c, chi.URLParam(r, "projectSlug")) + if err != nil { + writeErr(w, r, err) + return + } + resp := make([]workspaceResp, 0, len(out)) + for _, ws := range out { + resp = append(resp, toWorkspaceResp(ws)) + } + writeJSON(w, http.StatusOK, resp) +} + +func (h *Handler) getWorkspace(w http.ResponseWriter, r *http.Request) { + c, err := h.caller(r) + if err != nil { + writeErr(w, r, err) + return + } + id, err := parseUUID(chi.URLParam(r, "wsID")) + if err != nil { + writeErr(w, r, err) + return + } + ws, err := h.ws.Get(r.Context(), c, id) + if err != nil { + writeErr(w, r, err) + return + } + writeJSON(w, http.StatusOK, toWorkspaceResp(ws)) +} + +func (h *Handler) updateWorkspace(w http.ResponseWriter, r *http.Request) { + c, err := h.caller(r) + if err != nil { + writeErr(w, r, err) + return + } + id, err := parseUUID(chi.URLParam(r, "wsID")) + if err != nil { + writeErr(w, r, err) + return + } + var req updateWorkspaceReq + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeErr(w, r, errs.Wrap(err, errs.CodeInvalidInput, "decode")) + return + } + ws, err := h.ws.Update(r.Context(), c, id, UpdateWorkspaceInput{ + Name: req.Name, Description: req.Description, DefaultBranch: req.DefaultBranch, + }) + if err != nil { + writeErr(w, r, err) + return + } + writeJSON(w, http.StatusOK, toWorkspaceResp(ws)) +} + +func (h *Handler) deleteWorkspace(w http.ResponseWriter, r *http.Request) { + c, err := h.caller(r) + if err != nil { + writeErr(w, r, err) + return + } + id, err := parseUUID(chi.URLParam(r, "wsID")) + if err != nil { + writeErr(w, r, err) + return + } + if err := h.ws.Delete(r.Context(), c, id); err != nil { + writeErr(w, r, err) + return + } + w.WriteHeader(http.StatusNoContent) +} + +func (h *Handler) syncWorkspace(w http.ResponseWriter, r *http.Request) { + c, err := h.caller(r) + if err != nil { + writeErr(w, r, err) + return + } + id, err := parseUUID(chi.URLParam(r, "wsID")) + if err != nil { + writeErr(w, r, err) + return + } + ws, err := h.ws.Sync(r.Context(), c, id) + if err != nil { + writeErr(w, r, err) + return + } + writeJSON(w, http.StatusOK, toWorkspaceResp(ws)) +} + +// ===== credential ===== + +func (h *Handler) upsertCredential(w http.ResponseWriter, r *http.Request) { + c, err := h.caller(r) + if err != nil { + writeErr(w, r, err) + return + } + id, err := parseUUID(chi.URLParam(r, "wsID")) + if err != nil { + writeErr(w, r, err) + return + } + var body credentialBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + writeErr(w, r, errs.Wrap(err, errs.CodeInvalidInput, "decode")) + return + } + cred, err := h.ws.UpsertCredential(r.Context(), c, id, UpsertCredentialInput{ + Kind: CredentialKind(body.Kind), Username: body.Username, Secret: []byte(body.Secret), + }) + if err != nil { + writeErr(w, r, err) + return + } + writeJSON(w, http.StatusOK, toCredentialResp(cred)) +} + +func (h *Handler) getCredentialMeta(w http.ResponseWriter, r *http.Request) { + c, err := h.caller(r) + if err != nil { + writeErr(w, r, err) + return + } + id, err := parseUUID(chi.URLParam(r, "wsID")) + if err != nil { + writeErr(w, r, err) + return + } + cred, err := h.ws.GetCredentialMeta(r.Context(), c, id) + if err != nil { + // "凭据未配置" 视为返回 kind=none + 空 fingerprint + if isNotFound(err) { + writeJSON(w, http.StatusOK, credentialResp{Kind: string(CredKindNone)}) + return + } + writeErr(w, r, err) + return + } + writeJSON(w, http.StatusOK, toCredentialResp(cred)) +} + +func isNotFound(err error) bool { + var ae *errs.AppError + return errors.As(err, &ae) && ae.Code == errs.CodeNotFound +} + +// ===== worktree ===== + +func (h *Handler) listWorktrees(w http.ResponseWriter, r *http.Request) { + c, err := h.caller(r) + if err != nil { + writeErr(w, r, err) + return + } + id, err := parseUUID(chi.URLParam(r, "wsID")) + if err != nil { + writeErr(w, r, err) + return + } + out, err := h.wt.List(r.Context(), c, id) + if err != nil { + writeErr(w, r, err) + return + } + resp := make([]worktreeResp, 0, len(out)) + for _, x := range out { + resp = append(resp, toWorktreeResp(x)) + } + writeJSON(w, http.StatusOK, resp) +} + +func (h *Handler) createWorktree(w http.ResponseWriter, r *http.Request) { + c, err := h.caller(r) + if err != nil { + writeErr(w, r, err) + return + } + id, err := parseUUID(chi.URLParam(r, "wsID")) + if err != nil { + writeErr(w, r, err) + return + } + var req createWorktreeReq + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeErr(w, r, errs.Wrap(err, errs.CodeInvalidInput, "decode")) + return + } + wt, err := h.wt.Create(r.Context(), c, id, CreateWorktreeInput{Branch: req.Branch, BaseBranch: req.BaseBranch}) + if err != nil { + writeErr(w, r, err) + return + } + writeJSON(w, http.StatusCreated, toWorktreeResp(wt)) +} + +func (h *Handler) deleteWorktree(w http.ResponseWriter, r *http.Request) { + c, err := h.caller(r) + if err != nil { + writeErr(w, r, err) + return + } + id, err := parseUUID(chi.URLParam(r, "wtID")) + if err != nil { + writeErr(w, r, err) + return + } + if err := h.wt.Delete(r.Context(), c, id); err != nil { + writeErr(w, r, err) + return + } + w.WriteHeader(http.StatusNoContent) +} + +func (h *Handler) acquireWorktree(w http.ResponseWriter, r *http.Request) { + c, err := h.caller(r) + if err != nil { + writeErr(w, r, err) + return + } + id, err := parseUUID(chi.URLParam(r, "wtID")) + if err != nil { + writeErr(w, r, err) + return + } + wt, err := h.wt.Acquire(r.Context(), c, id) + if err != nil { + writeErr(w, r, err) + return + } + writeJSON(w, http.StatusOK, toWorktreeResp(wt)) +} + +func (h *Handler) releaseWorktree(w http.ResponseWriter, r *http.Request) { + c, err := h.caller(r) + if err != nil { + writeErr(w, r, err) + return + } + id, err := parseUUID(chi.URLParam(r, "wtID")) + if err != nil { + writeErr(w, r, err) + return + } + wt, err := h.wt.Release(r.Context(), c, id) + if err != nil { + writeErr(w, r, err) + return + } + writeJSON(w, http.StatusOK, toWorktreeResp(wt)) +} + +// ===== git ops ===== + +func (h *Handler) statusOnMain(w http.ResponseWriter, r *http.Request) { + h.statusGeneric(w, r, true) +} + +func (h *Handler) statusOnWorktree(w http.ResponseWriter, r *http.Request) { + h.statusGeneric(w, r, false) +} + +func (h *Handler) statusGeneric(w http.ResponseWriter, r *http.Request, onMain bool) { + c, err := h.caller(r) + if err != nil { + writeErr(w, r, err) + return + } + param := "wsID" + if !onMain { + param = "wtID" + } + id, err := parseUUID(chi.URLParam(r, param)) + if err != nil { + writeErr(w, r, err) + return + } + var st []git.FileStatus + if onMain { + st, err = h.gop.StatusOnMain(r.Context(), c, id) + } else { + st, err = h.gop.StatusOnWorktree(r.Context(), c, id) + } + if err != nil { + writeErr(w, r, err) + return + } + out := make([]fileStatusResp, 0, len(st)) + for _, f := range st { + out = append(out, toFileStatusResp(f)) + } + writeJSON(w, http.StatusOK, out) +} + +func (h *Handler) commitOnMain(w http.ResponseWriter, r *http.Request) { + h.commitGeneric(w, r, true) +} + +func (h *Handler) commitOnWorktree(w http.ResponseWriter, r *http.Request) { + h.commitGeneric(w, r, false) +} + +func (h *Handler) commitGeneric(w http.ResponseWriter, r *http.Request, onMain bool) { + c, err := h.caller(r) + if err != nil { + writeErr(w, r, err) + return + } + param := "wsID" + if !onMain { + param = "wtID" + } + id, err := parseUUID(chi.URLParam(r, param)) + if err != nil { + writeErr(w, r, err) + return + } + var req commitReq + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeErr(w, r, errs.Wrap(err, errs.CodeInvalidInput, "decode")) + return + } + push := req.Push == nil || *req.Push + addAll := req.AddAll == nil || *req.AddAll + in := CommitInput{Message: req.Message, Push: push, AddAll: addAll} + var sha string + if onMain { + sha, err = h.gop.CommitOnMain(r.Context(), c, id, in) + } else { + sha, err = h.gop.CommitOnWorktree(r.Context(), c, id, in) + } + if err != nil { + writeErr(w, r, err) + return + } + writeJSON(w, http.StatusOK, commitResp{SHA: sha}) +} + +func (h *Handler) pushOnMain(w http.ResponseWriter, r *http.Request) { + h.pushGeneric(w, r, true) +} + +func (h *Handler) pushOnWorktree(w http.ResponseWriter, r *http.Request) { + h.pushGeneric(w, r, false) +} + +func (h *Handler) pushGeneric(w http.ResponseWriter, r *http.Request, onMain bool) { + c, err := h.caller(r) + if err != nil { + writeErr(w, r, err) + return + } + param := "wsID" + if !onMain { + param = "wtID" + } + id, err := parseUUID(chi.URLParam(r, param)) + if err != nil { + writeErr(w, r, err) + return + } + if onMain { + err = h.gop.PushOnMain(r.Context(), c, id) + } else { + err = h.gop.PushOnWorktree(r.Context(), c, id) + } + if err != nil { + writeErr(w, r, err) + return + } + w.WriteHeader(http.StatusNoContent) +} diff --git a/internal/workspace/handler_test.go b/internal/workspace/handler_test.go new file mode 100644 index 0000000..cc53476 --- /dev/null +++ b/internal/workspace/handler_test.go @@ -0,0 +1,84 @@ +package workspace + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/yan1h/agent-coding-workflow/internal/transport/http/middleware" +) + +// fakeWS / fakeWT / fakeGOP 是 handler 单测用的桩(只覆盖必要方法)。 +type fakeWS struct{ created *Workspace } + +func (f *fakeWS) Create(_ context.Context, _ Caller, _ string, _ CreateWorkspaceInput) (*Workspace, error) { + w := &Workspace{ID: uuid.New(), Slug: "ws1", Name: "W1", SyncStatus: SyncStatusCloning} + f.created = w + return w, nil +} + +func (f *fakeWS) Get(_ context.Context, _ Caller, id uuid.UUID) (*Workspace, error) { + return &Workspace{ID: id, Slug: "ws1"}, nil +} + +func (f *fakeWS) List(_ context.Context, _ Caller, _ string) ([]*Workspace, error) { return nil, nil } + +func (f *fakeWS) Update(_ context.Context, _ Caller, id uuid.UUID, _ UpdateWorkspaceInput) (*Workspace, error) { + return &Workspace{ID: id}, nil +} + +func (f *fakeWS) Delete(_ context.Context, _ Caller, _ uuid.UUID) error { return nil } + +func (f *fakeWS) Sync(_ context.Context, _ Caller, id uuid.UUID) (*Workspace, error) { + return &Workspace{ID: id, SyncStatus: SyncStatusIdle}, nil +} + +func (f *fakeWS) UpsertCredential(_ context.Context, _ Caller, _ uuid.UUID, _ UpsertCredentialInput) (*Credential, error) { + return &Credential{Kind: CredKindPAT, Fingerprint: "****abcd"}, nil +} + +func (f *fakeWS) GetCredentialMeta(_ context.Context, _ Caller, _ uuid.UUID) (*Credential, error) { + return &Credential{Kind: CredKindNone}, nil +} + +// fakeResolver 把固定 token 解码为 uuid(沿 internal/project handler_test.go 模式)。 +type fakeResolver struct{ valid map[string]uuid.UUID } + +func (f *fakeResolver) ResolveSession(_ context.Context, token string) (uuid.UUID, bool, error) { + uid, ok := f.valid[token] + return uid, ok, nil +} + +// fakeAdmin 始终返回 false。 +type fakeAdmin struct{} + +func (fakeAdmin) IsAdmin(_ context.Context, _ uuid.UUID) (bool, error) { return false, nil } + +func TestHandler_CreateWorkspace_Smoke(t *testing.T) { + t.Parallel() + ws := &fakeWS{} + uid := uuid.New() + tok := "tok-1" + resolver := &fakeResolver{valid: map[string]uuid.UUID{tok: uid}} + h := NewHandler(ws, nil, nil, resolver, fakeAdmin{}) + r := chi.NewRouter() + r.Use(middleware.RequestID) + h.Mount(r) + + body, _ := json.Marshal(createWorkspaceReq{ + Slug: "ws1", Name: "W1", GitRemoteURL: "https://x", + Credential: credentialBody{Kind: "none"}, + }) + req := httptest.NewRequest("POST", "/api/v1/projects/p/workspaces", bytes.NewReader(body)) + req.Header.Set("Authorization", "Bearer "+tok) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + require.Equal(t, http.StatusCreated, w.Code) +}