Enable auto default sync channels on agent activate, bind-code/phone confirm flows, publish ALTER, and align admin/yuheng docs with the production bind path.
1173 lines
49 KiB
Go
1173 lines
49 KiB
Go
package handler
|
||
|
||
import (
|
||
"encoding/json"
|
||
"io"
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"aijianzhan/platform/internal/apidef"
|
||
"aijianzhan/platform/internal/audit"
|
||
"aijianzhan/platform/internal/authx"
|
||
"aijianzhan/platform/internal/agentcap"
|
||
"aijianzhan/platform/internal/logic/applogic"
|
||
"aijianzhan/platform/internal/meta"
|
||
"aijianzhan/platform/internal/svc"
|
||
"aijianzhan/platform/internal/types"
|
||
|
||
"github.com/zeromicro/go-zero/rest"
|
||
"github.com/zeromicro/go-zero/rest/httpx"
|
||
"github.com/zeromicro/go-zero/rest/pathvar"
|
||
)
|
||
|
||
func chain(h http.HandlerFunc, mws ...func(http.HandlerFunc) http.HandlerFunc) http.HandlerFunc {
|
||
for i := len(mws) - 1; i >= 0; i-- {
|
||
h = mws[i](h)
|
||
}
|
||
return h
|
||
}
|
||
|
||
func RegisterHandlers(server *rest.Server, svcCtx *svc.ServiceContext) {
|
||
authMW := authx.Middleware(svcCtx.JWT, svcCtx.Config.DevAuth)
|
||
rl := svcCtx.Limiter.Middleware
|
||
perm := authx.RequirePermission
|
||
tenant := authx.RequireTenant()
|
||
platformAdmin := authx.RequirePlatformAdmin()
|
||
syncPush := authx.RequireSyncPushAccess()
|
||
appGrant := requireAgentAppGrant(svcCtx)
|
||
|
||
// —— 公开 ——
|
||
server.AddRoutes([]rest.Route{
|
||
{Method: http.MethodPost, Path: "/api/v1/auth/token", Handler: rl(tokenHandler(svcCtx))},
|
||
{Method: http.MethodPost, Path: "/api/v1/auth/agent/register", Handler: rl(agentSelfRegisterHandler(svcCtx))},
|
||
{Method: http.MethodPost, Path: "/api/v1/auth/bind-code/redeem", Handler: rl(bindCodeRedeemHandler(svcCtx))},
|
||
{Method: http.MethodPost, Path: "/api/v1/auth/bind/phone-lookup", Handler: rl(bindPhoneLookupHandler(svcCtx))},
|
||
{Method: http.MethodPost, Path: "/api/v1/auth/bind/phone-confirm", Handler: rl(bindPhoneConfirmHandler(svcCtx))},
|
||
{Method: http.MethodPost, Path: "/api/v1/auth/register", Handler: rl(registerHandler(svcCtx))},
|
||
{Method: http.MethodPost, Path: "/api/v1/auth/login", Handler: rl(loginHandler(svcCtx))},
|
||
{Method: http.MethodPost, Path: "/api/v1/auth/sms/send", Handler: rl(sendLoginSMSHandler(svcCtx))},
|
||
{Method: http.MethodGet, Path: "/api/v1/meta/apis", Handler: rl(apiCatalogHandler())},
|
||
{Method: http.MethodGet, Path: "/api/v1/meta/openapi.yaml", Handler: rl(openapiHandler())},
|
||
// 授权租约:过期后仍可调用(中间件放行),供远端续费 / 安全狗延期
|
||
{Method: http.MethodGet, Path: "/api/v1/license/status", Handler: rl(licenseStatusHandler(svcCtx))},
|
||
{Method: http.MethodPost, Path: "/api/v1/license/renew", Handler: rl(licenseRenewHandler(svcCtx))},
|
||
{Method: http.MethodPost, Path: "/api/v1/license/extend", Handler: rl(licenseExtendHandler(svcCtx))},
|
||
{Method: http.MethodPut, Path: "/api/v1/license/lease", Handler: rl(licensePutLeaseHandler(svcCtx))},
|
||
{Method: http.MethodPost, Path: "/api/v1/license/import", Handler: rl(licenseImportHandler(svcCtx))},
|
||
// 已发布模块公开展示(看板预览 / 真页面截图,无需登录)
|
||
{Method: http.MethodGet, Path: "/api/v1/public/apps/:slug/blueprint", Handler: rl(publicBlueprintHandler(svcCtx))},
|
||
{Method: http.MethodGet, Path: "/api/v1/public/apps/:slug/:resource", Handler: rl(publicListHandler(svcCtx))},
|
||
// 按用户 ID 加密后的模块路径访问(宿主「访问地址」)
|
||
{Method: http.MethodGet, Path: "/api/v1/public/m/:token/blueprint", Handler: rl(publicModulePathBlueprintHandler(svcCtx))},
|
||
})
|
||
|
||
// —— 鉴权:pending 可调用(入驻) ——
|
||
server.AddRoutes([]rest.Route{
|
||
{Method: http.MethodPost, Path: "/api/v1/auth/invites/accept", Handler: chain(inviteAcceptHandler(svcCtx), rl, authMW)},
|
||
{Method: http.MethodPost, Path: "/api/v1/auth/password", Handler: chain(changePasswordHandler(svcCtx), rl, authMW)},
|
||
{Method: http.MethodGet, Path: "/api/v1/auth/me", Handler: chain(meHandler(svcCtx), rl, authMW)},
|
||
{Method: http.MethodPut, Path: "/api/v1/auth/phone", Handler: chain(bindPhoneHandler(svcCtx), rl, authMW)},
|
||
{Method: http.MethodPost, Path: "/api/v1/tenants", Handler: chain(tenantCreateHandler(svcCtx), rl, authMW)},
|
||
|
||
// —— 平台超级管理员(仅公司一级:列表/新建)——
|
||
// —— 平台超级管理员(公司一级 + 权限额度)——
|
||
{Method: http.MethodGet, Path: "/api/v1/platform/tenants", Handler: chain(platformListTenantsHandler(svcCtx), rl, authMW, platformAdmin)},
|
||
{Method: http.MethodPost, Path: "/api/v1/platform/tenants", Handler: chain(platformCreateTenantHandler(svcCtx), rl, authMW, platformAdmin)},
|
||
{Method: http.MethodPut, Path: "/api/v1/platform/tenants/:id", Handler: chain(platformUpdateTenantHandler(svcCtx), rl, authMW, platformAdmin)},
|
||
{Method: http.MethodPost, Path: "/api/v1/platform/tenants/:id/enter", Handler: chain(platformEnterTenantHandler(svcCtx), rl, authMW, platformAdmin)},
|
||
{Method: http.MethodPost, Path: "/api/v1/platform/exit", Handler: chain(platformExitTenantHandler(svcCtx), rl, authMW, platformAdmin)},
|
||
{Method: http.MethodPost, Path: "/api/v1/platform/tenants/:id/admin-invite", Handler: chain(platformAdminInviteHandler(svcCtx), rl, authMW, platformAdmin)},
|
||
{Method: http.MethodPost, Path: "/api/v1/platform/tenants/:id/admin-account", Handler: chain(platformAdminAccountHandler(svcCtx), rl, authMW, platformAdmin)},
|
||
{Method: http.MethodGet, Path: "/api/v1/platform/tenants/:id/admins", Handler: chain(platformListAdminsHandler(svcCtx), rl, authMW, platformAdmin)},
|
||
{Method: http.MethodPut, Path: "/api/v1/platform/tenants/:id/admins/:uid", Handler: chain(platformUpdateAdminHandler(svcCtx), rl, authMW, platformAdmin)},
|
||
{Method: http.MethodGet, Path: "/api/v1/platform/perm-modules", Handler: chain(platformPermModulesHandler(svcCtx), rl, authMW, platformAdmin)},
|
||
{Method: http.MethodGet, Path: "/api/v1/platform/tenants/:id/permissions", Handler: chain(platformGetTenantPermsHandler(svcCtx), rl, authMW, platformAdmin)},
|
||
{Method: http.MethodPut, Path: "/api/v1/platform/tenants/:id/permissions", Handler: chain(platformSetTenantPermsHandler(svcCtx), rl, authMW, platformAdmin)},
|
||
{Method: http.MethodGet, Path: "/api/v1/platform/dbsync/lww-overrides", Handler: chain(platformLwwOverridesHandler(svcCtx), rl, authMW, platformAdmin)},
|
||
{Method: http.MethodPost, Path: "/api/v1/platform/dbsync/lww-overrides/:id/rollback", Handler: chain(platformLwwRollbackHandler(svcCtx), rl, authMW, platformAdmin)},
|
||
{Method: http.MethodPost, Path: "/api/v1/platform/apps/ensure-import", Handler: chain(platformEnsureImportHandler(svcCtx), rl, authMW, platformAdmin)},
|
||
})
|
||
|
||
// —— 鉴权:需已加入租户 ——
|
||
server.AddRoutes([]rest.Route{
|
||
{Method: http.MethodGet, Path: "/api/v1/apps", Handler: chain(listAppsHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm读取模块))},
|
||
{Method: http.MethodPost, Path: "/api/v1/admin/apps/ensure-import", Handler: chain(adminEnsureImportHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm发布模块))},
|
||
{Method: http.MethodPut, Path: "/api/v1/apps/:slug/draft", Handler: chain(saveDraftHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm写入模块), appGrant)},
|
||
{Method: http.MethodPost, Path: "/api/v1/apps/:slug/publish", Handler: chain(publishHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm发布模块), appGrant)},
|
||
{Method: http.MethodGet, Path: "/api/v1/apps/:slug/blueprint", Handler: chain(getBlueprintHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm读取模块), appGrant)},
|
||
{Method: http.MethodGet, Path: "/api/v1/apps/:slug/agent-capsule", Handler: chain(capsuleHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm读取模块), appGrant)},
|
||
{Method: http.MethodGet, Path: "/api/v1/audit/logs", Handler: chain(auditListHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm查看审计))},
|
||
|
||
{Method: http.MethodGet, Path: "/api/v1/admin/agents", Handler: chain(agentListHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
|
||
{Method: http.MethodPost, Path: "/api/v1/admin/agents", Handler: chain(agentCreateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
|
||
{Method: http.MethodGet, Path: "/api/v1/admin/agents/:id", Handler: chain(agentGetHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
|
||
{Method: http.MethodPut, Path: "/api/v1/admin/agents/:id", Handler: chain(agentUpdateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
|
||
{Method: http.MethodPost, Path: "/api/v1/admin/agents/:id/rotate-secret", Handler: chain(agentRotateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
|
||
{Method: http.MethodDelete, Path: "/api/v1/admin/agents/:id", Handler: chain(agentDeleteHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
|
||
// Z12b:智能体自查(无需管理智能体)
|
||
{Method: http.MethodGet, Path: "/api/v1/agents/me", Handler: chain(agentMeHandler(svcCtx), rl, authMW)},
|
||
|
||
{Method: http.MethodGet, Path: "/api/v1/admin/bind-codes", Handler: chain(bindCodeListHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||
{Method: http.MethodPost, Path: "/api/v1/admin/bind-codes", Handler: chain(bindCodeCreateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||
{Method: http.MethodDelete, Path: "/api/v1/admin/bind-codes/:code", Handler: chain(bindCodeRevokeHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||
|
||
{Method: http.MethodGet, Path: "/api/v1/admin/roles", Handler: chain(roleListHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
|
||
{Method: http.MethodGet, Path: "/api/v1/admin/entitlements", Handler: chain(companyEntitlementsHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm读取模块))},
|
||
{Method: http.MethodGet, Path: "/api/v1/admin/members", Handler: chain(memberListHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm邀请成员))},
|
||
{Method: http.MethodPost, Path: "/api/v1/admin/members", Handler: chain(memberCreateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm邀请成员))},
|
||
{Method: http.MethodPut, Path: "/api/v1/admin/members/:id", Handler: chain(memberUpdateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm邀请成员))},
|
||
{Method: http.MethodPost, Path: "/api/v1/admin/roles", Handler: chain(roleCreateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
|
||
{Method: http.MethodGet, Path: "/api/v1/admin/roles/:id", Handler: chain(roleGetHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
|
||
{Method: http.MethodPut, Path: "/api/v1/admin/roles/:id", Handler: chain(roleUpdateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
|
||
{Method: http.MethodDelete, Path: "/api/v1/admin/roles/:id", Handler: chain(roleDeleteHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理智能体))},
|
||
|
||
{Method: http.MethodGet, Path: "/api/v1/admin/invites", Handler: chain(inviteListHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm邀请成员))},
|
||
{Method: http.MethodPost, Path: "/api/v1/admin/invites", Handler: chain(inviteCreateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm邀请成员))},
|
||
{Method: http.MethodDelete, Path: "/api/v1/admin/invites/:id", Handler: chain(inviteRevokeHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm邀请成员))},
|
||
|
||
{Method: http.MethodGet, Path: "/api/v1/admin/org-units", Handler: chain(orgUnitListHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理组织))},
|
||
{Method: http.MethodPost, Path: "/api/v1/admin/org-units", Handler: chain(orgUnitCreateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理组织))},
|
||
{Method: http.MethodPut, Path: "/api/v1/admin/org-units/:id", Handler: chain(orgUnitUpdateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理组织))},
|
||
{Method: http.MethodDelete, Path: "/api/v1/admin/org-units/:id", Handler: chain(orgUnitDeleteHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm管理组织))},
|
||
|
||
// 跨库同步中间件:本地 SQLite ↔ 线上 MySQL/Postgres,后台可配线上地址
|
||
{Method: http.MethodGet, Path: "/api/v1/admin/sync/channels", Handler: chain(syncListHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||
{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels", Handler: chain(syncSaveHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||
{Method: http.MethodGet, Path: "/api/v1/admin/sync/channels/:id", Handler: chain(syncGetHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||
{Method: http.MethodPut, Path: "/api/v1/admin/sync/channels/:id", Handler: chain(syncSaveHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||
{Method: http.MethodDelete, Path: "/api/v1/admin/sync/channels/:id", Handler: chain(syncDeleteHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||
{Method: http.MethodPost, Path: "/api/v1/admin/sync/test", Handler: chain(syncTestHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||
{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels/:id/prepare", Handler: chain(syncPrepareHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||
{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels/:id/start", Handler: chain(syncStartHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||
{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels/:id/stop", Handler: chain(syncStopHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||
{Method: http.MethodGet, Path: "/api/v1/admin/sync/conflicts", Handler: chain(syncConflictsHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||
{Method: http.MethodPost, Path: "/api/v1/admin/sync/conflicts/:id/resolve", Handler: chain(syncResolveConflictHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||
{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels/:id/reconcile", Handler: chain(syncReconcileHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||
{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels/:id/ingest", Handler: chain(syncIngestHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||
{Method: http.MethodGet, Path: "/api/v1/admin/sync/channels/:id/inspect", Handler: chain(syncInspectHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||
{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels/:id/preview", Handler: chain(syncPreviewHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||
{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels/:id/drop-table", Handler: chain(syncDropTableHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||
{Method: http.MethodGet, Path: "/api/v1/admin/sync/bindings", Handler: chain(syncBindingsListHandler(svcCtx), rl, authMW, tenant, syncPush)},
|
||
{Method: http.MethodPost, Path: "/api/v1/admin/sync/bindings", Handler: chain(syncBindingsEnsureHandler(svcCtx), rl, authMW, tenant, syncPush)},
|
||
|
||
// —— 本机 sync agent(形态 B):白名单 + push/pull;「数据同步」租户级,或登录用户按 Binding 自助 ——
|
||
{Method: http.MethodGet, Path: "/api/v1/agent/sync/channels/:id/whitelist", Handler: chain(agentSyncWhitelistHandler(svcCtx), rl, authMW, tenant, syncPush)},
|
||
{Method: http.MethodPost, Path: "/api/v1/agent/sync/channels/:id/push", Handler: chain(agentSyncPushHandler(svcCtx), rl, authMW, tenant, syncPush)},
|
||
{Method: http.MethodPost, Path: "/api/v1/agent/sync/channels/:id/push/batch", Handler: chain(agentSyncPushBatchHandler(svcCtx), rl, authMW, tenant, syncPush)},
|
||
{Method: http.MethodPost, Path: "/api/v1/agent/sync/channels/:id/pull", Handler: chain(agentSyncPullHandler(svcCtx), rl, authMW, tenant, syncPush)},
|
||
{Method: http.MethodPost, Path: "/api/v1/agent/sync/channels/:id/bootstrap", Handler: chain(agentSyncBootstrapHandler(svcCtx), rl, authMW, tenant, syncPush)},
|
||
{Method: http.MethodPost, Path: "/api/v1/agent/sync/channels/:id/schema", Handler: chain(agentSyncSchemaDescribeHandler(svcCtx), rl, authMW, tenant, syncPush)},
|
||
{Method: http.MethodPost, Path: "/api/v1/agent/sync/channels/:id/schema/ensure", Handler: chain(agentSyncSchemaEnsureHandler(svcCtx), rl, authMW, tenant, syncPush)},
|
||
|
||
// 存储:POST 创建对象,GET 读取(无 /upload 动词路径;旧路径保留别名防断裂)
|
||
{Method: http.MethodPost, Path: "/api/v1/storage", Handler: chain(uploadHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm上传文件))},
|
||
{Method: http.MethodPost, Path: "/api/v1/storage/upload", Handler: chain(uploadHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm上传文件))},
|
||
{Method: http.MethodGet, Path: "/api/v1/storage/:1/:2/:3", Handler: chain(downloadHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm下载文件))},
|
||
|
||
// 动态 CRUD:仅通用行数据传输
|
||
{Method: http.MethodPost, Path: "/api/v1/apps/:slug/:resource/import", Handler: chain(importHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm导入数据), appGrant)},
|
||
{Method: http.MethodGet, Path: "/api/v1/apps/:slug/:resource/export", Handler: chain(exportHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm导出数据), appGrant)},
|
||
{Method: http.MethodGet, Path: "/api/v1/apps/:slug/:resource/aggregate", Handler: chain(aggregateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm查询数据), appGrant)},
|
||
{Method: http.MethodGet, Path: "/api/v1/apps/:slug/:resource", Handler: chain(listHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm查询数据), appGrant)},
|
||
{Method: http.MethodPost, Path: "/api/v1/apps/:slug/:resource", Handler: chain(createHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm新增数据), appGrant)},
|
||
{Method: http.MethodGet, Path: "/api/v1/apps/:slug/:resource/:id", Handler: chain(getHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm查询数据), appGrant)},
|
||
{Method: http.MethodPut, Path: "/api/v1/apps/:slug/:resource/:id", Handler: chain(updateHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm更新数据), appGrant)},
|
||
{Method: http.MethodDelete, Path: "/api/v1/apps/:slug/:resource/:id", Handler: chain(deleteHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm删除数据), appGrant)},
|
||
})
|
||
}
|
||
|
||
func requireAgentAppGrant(svcCtx *svc.ServiceContext) func(http.HandlerFunc) http.HandlerFunc {
|
||
return func(next http.HandlerFunc) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
if authx.Role(r.Context()) != authx.RoleAgent {
|
||
next(w, r)
|
||
return
|
||
}
|
||
vars := pathvar.Vars(r)
|
||
slug := vars["slug"]
|
||
if slug == "" {
|
||
next(w, r)
|
||
return
|
||
}
|
||
if svcCtx.Agents == nil {
|
||
authx.WriteError(w, http.StatusForbidden, "agent store unavailable")
|
||
return
|
||
}
|
||
ok, err := svcCtx.Agents.HasAppAccess(r.Context(), authx.AgentID(r.Context()), slug)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusForbidden, err.Error())
|
||
return
|
||
}
|
||
if ok {
|
||
next(w, r)
|
||
return
|
||
}
|
||
// 新建:目标模块尚不存在时,允许已启用智能体直接 publish/draft(发布成功后自动写入 app_slugs)
|
||
if svcCtx.Meta != nil && (r.Method == http.MethodPost || r.Method == http.MethodPut) {
|
||
existing, gerr := svcCtx.Meta.GetBySlug(r.Context(), authx.TenantID(r.Context()), slug)
|
||
if gerr != nil || existing == nil {
|
||
next(w, r)
|
||
return
|
||
}
|
||
}
|
||
authx.WriteError(w, http.StatusForbidden, "app not granted to agent: "+slug)
|
||
}
|
||
}
|
||
}
|
||
|
||
func agentListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
items, err := applogic.NewAgentAdminLogic(r.Context(), svcCtx).List()
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, map[string]any{"items": items})
|
||
}
|
||
}
|
||
|
||
func agentGetHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
|
||
acc, err := applogic.NewAgentAdminLogic(r.Context(), svcCtx).Get(id)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusNotFound, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, acc)
|
||
}
|
||
}
|
||
|
||
func agentCreateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
var req types.AgentCreateReq
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
resp, err := applogic.NewAgentAdminLogic(r.Context(), svcCtx).Create(&req)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, resp)
|
||
}
|
||
}
|
||
|
||
func agentUpdateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
|
||
var req types.AgentUpdateReq
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
acc, err := applogic.NewAgentAdminLogic(r.Context(), svcCtx).Update(id, &req)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, acc)
|
||
}
|
||
}
|
||
|
||
func agentRotateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
|
||
resp, err := applogic.NewAgentAdminLogic(r.Context(), svcCtx).Rotate(id)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, resp)
|
||
}
|
||
}
|
||
|
||
func agentDeleteHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
|
||
if err := applogic.NewAgentAdminLogic(r.Context(), svcCtx).Delete(id); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, map[string]any{"ok": true})
|
||
}
|
||
}
|
||
|
||
func agentMeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
acc, err := applogic.NewAuthLogic(r.Context(), svcCtx).AgentMe()
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusForbidden, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, map[string]any{
|
||
"agent_id": acc.AgentID,
|
||
"tenant_id": acc.TenantID,
|
||
"name": acc.Name,
|
||
"client_id": acc.ClientID,
|
||
"status": acc.Status,
|
||
"channel_id": acc.ChannelID,
|
||
"online_db_id": acc.OnlineDBID,
|
||
"database_name": acc.DatabaseName,
|
||
"sync_bound": strings.TrimSpace(acc.ChannelID) != "" && strings.TrimSpace(acc.OnlineDBID) != "",
|
||
"app_slugs": acc.AppSlugs,
|
||
"permissions": acc.Perms,
|
||
})
|
||
}
|
||
}
|
||
|
||
func bindCodeListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
items, err := applogic.NewAuthLogic(r.Context(), svcCtx).ListBindCodes()
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, map[string]any{"items": items})
|
||
}
|
||
}
|
||
|
||
func bindCodeCreateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
var req applogic.BindCodeCreateReq
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil && err != io.EOF {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
bc, err := applogic.NewAuthLogic(r.Context(), svcCtx).CreateBindCode(req)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, bc)
|
||
}
|
||
}
|
||
|
||
func bindCodeRevokeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
code := pathvar.Vars(r)["code"]
|
||
if err := applogic.NewAuthLogic(r.Context(), svcCtx).RevokeBindCode(code); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, map[string]any{"ok": true})
|
||
}
|
||
}
|
||
|
||
func bindCodeRedeemHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
var req applogic.BindCodeRedeemReq
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).RedeemBindCode(req)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, resp)
|
||
}
|
||
}
|
||
|
||
func bindPhoneLookupHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
var req applogic.PhoneLookupReq
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).PhoneLookup(req)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, resp)
|
||
}
|
||
}
|
||
|
||
func bindPhoneConfirmHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
var req applogic.PhoneConfirmReq
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).PhoneConfirm(req)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, resp)
|
||
}
|
||
}
|
||
|
||
func roleListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
items, err := applogic.NewRoleAdminLogic(r.Context(), svcCtx).List()
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, map[string]any{"items": items})
|
||
}
|
||
}
|
||
|
||
func roleGetHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
|
||
role, err := applogic.NewRoleAdminLogic(r.Context(), svcCtx).Get(id)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusNotFound, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, role)
|
||
}
|
||
}
|
||
|
||
func roleCreateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
var req types.RoleCreateReq
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
role, err := applogic.NewRoleAdminLogic(r.Context(), svcCtx).Create(&req)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, role)
|
||
}
|
||
}
|
||
|
||
func roleUpdateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
|
||
var req types.RoleUpdateReq
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
role, err := applogic.NewRoleAdminLogic(r.Context(), svcCtx).Update(id, &req)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, role)
|
||
}
|
||
}
|
||
|
||
func roleDeleteHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
|
||
if err := applogic.NewRoleAdminLogic(r.Context(), svcCtx).Delete(id); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, map[string]any{"ok": true})
|
||
}
|
||
}
|
||
|
||
func apiCatalogHandler() http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
httpx.OkJson(w, map[string]any{
|
||
"platform": apidef.Catalog,
|
||
"ai": apidef.AICatalog,
|
||
"verbs": []string{"GET", "POST", "PUT", "DELETE"},
|
||
"note": "业务数据只走 apps/{slug}/{resource} CRUD;行业字段由蓝图定义,不在此增删路由。",
|
||
})
|
||
}
|
||
}
|
||
|
||
func tokenHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
var req types.TokenReq
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).IssueToken(&req)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusUnauthorized, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, resp)
|
||
}
|
||
}
|
||
|
||
func agentSelfRegisterHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
var req types.AgentSelfRegisterReq
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).SelfRegisterAgent(&req)
|
||
if err != nil {
|
||
msg := err.Error()
|
||
code := http.StatusBadRequest
|
||
if strings.Contains(msg, "invalid register secret") {
|
||
code = http.StatusUnauthorized
|
||
}
|
||
if strings.Contains(msg, "already registered") {
|
||
code = http.StatusConflict
|
||
}
|
||
authx.WriteError(w, code, msg)
|
||
return
|
||
}
|
||
httpx.OkJson(w, resp)
|
||
}
|
||
}
|
||
|
||
func registerHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
var req types.RegisterReq
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).Register(&req)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
_ = svcCtx.Audit.Log(r.Context(), resp.TenantID, resp.UserID, "auth.register", req.Username)
|
||
httpx.WriteJson(w, http.StatusCreated, resp)
|
||
}
|
||
}
|
||
|
||
func loginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
var req types.LoginReq
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).Login(&req)
|
||
if err != nil {
|
||
msg := err.Error()
|
||
code := http.StatusUnauthorized
|
||
if strings.Contains(msg, "请填写") || strings.Contains(msg, "格式") || strings.Contains(msg, "未开启") || strings.Contains(msg, "未启用") {
|
||
code = http.StatusBadRequest
|
||
}
|
||
authx.WriteError(w, code, msg)
|
||
return
|
||
}
|
||
label := req.Username
|
||
if label == "" {
|
||
label = req.Phone
|
||
}
|
||
_ = svcCtx.Audit.Log(r.Context(), resp.TenantID, resp.UserID, "auth.login", label)
|
||
httpx.OkJson(w, resp)
|
||
}
|
||
}
|
||
|
||
func sendLoginSMSHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
var req types.SMSSendReq
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
res, err := applogic.NewAuthLogic(r.Context(), svcCtx).SendLoginSMS(req.Phone)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, types.SMSSendResp{
|
||
OK: true,
|
||
ExpiresIn: res.ExpiresIn,
|
||
RetryAfter: res.RetryAfter,
|
||
Message: res.Message,
|
||
DebugCode: res.DebugCode,
|
||
})
|
||
}
|
||
}
|
||
|
||
func changePasswordHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
var body struct {
|
||
OldPassword string `json:"old_password"`
|
||
NewPassword string `json:"new_password"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
if err := applogic.NewAuthLogic(r.Context(), svcCtx).ChangePassword(body.OldPassword, body.NewPassword); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
_ = svcCtx.Audit.Log(r.Context(), authx.TenantID(r.Context()), authx.UserID(r.Context()), "auth.change_password", "ok")
|
||
httpx.OkJson(w, map[string]any{"ok": true})
|
||
}
|
||
}
|
||
|
||
func meHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
u, err := applogic.NewAuthLogic(r.Context(), svcCtx).Me()
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusUnauthorized, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, map[string]any{
|
||
"user_id": u.UserID,
|
||
"username": u.Username,
|
||
"phone": u.Phone,
|
||
"username_login_disabled": u.UsernameLoginDisabled,
|
||
"display_name": u.DisplayName,
|
||
"role": u.Role,
|
||
"tenant_id": u.TenantID,
|
||
"org_unit_id": u.OrgUnitID,
|
||
"status": u.Status,
|
||
"policy": map[string]any{
|
||
"phone_login_only": applogic.NewAuthLogic(r.Context(), svcCtx).PhoneLoginOnlyPolicy(),
|
||
"license_enabled": svcCtx.Config.License.Enabled,
|
||
"disable_username_login_if_phone_bound": svcCtx.Config.Auth.DisableUsernameLoginIfPhoneBound,
|
||
"require_phone_bound": svcCtx.Config.Auth.RequirePhoneBound,
|
||
},
|
||
})
|
||
}
|
||
}
|
||
|
||
func bindPhoneHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
var body struct {
|
||
Phone string `json:"phone"`
|
||
DisableUsernameLogin *bool `json:"disable_username_login"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
l := applogic.NewAuthLogic(r.Context(), svcCtx)
|
||
u, err := l.BindPhone(body.Phone)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
if body.DisableUsernameLogin != nil {
|
||
u, err = l.SetUsernameLoginDisabled(*body.DisableUsernameLogin)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
}
|
||
_ = svcCtx.Audit.Log(r.Context(), authx.TenantID(r.Context()), authx.UserID(r.Context()),
|
||
"auth.bind_phone", u.Phone)
|
||
httpx.OkJson(w, map[string]any{
|
||
"user_id": u.UserID,
|
||
"username": u.Username,
|
||
"phone": u.Phone,
|
||
"username_login_disabled": u.UsernameLoginDisabled,
|
||
"display_name": u.DisplayName,
|
||
})
|
||
}
|
||
}
|
||
|
||
func inviteAcceptHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
var req types.InviteAcceptReq
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).AcceptInvite(&req)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
_ = svcCtx.Audit.Log(r.Context(), resp.TenantID, resp.UserID, "auth.invite.accept", req.Code)
|
||
httpx.OkJson(w, resp)
|
||
}
|
||
}
|
||
|
||
func tenantCreateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
var req types.TenantCreateReq
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).CreateTenant(&req)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
_ = svcCtx.Audit.Log(r.Context(), resp.TenantID, resp.UserID, "tenant.create", req.Name)
|
||
httpx.WriteJson(w, http.StatusCreated, resp)
|
||
}
|
||
}
|
||
|
||
func inviteListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
items, err := applogic.NewInviteAdminLogic(r.Context(), svcCtx).List()
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, map[string]any{"items": items})
|
||
}
|
||
}
|
||
|
||
func inviteCreateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
var req types.InviteCreateReq
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
inv, err := applogic.NewInviteAdminLogic(r.Context(), svcCtx).Create(&req)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.WriteJson(w, http.StatusCreated, inv)
|
||
}
|
||
}
|
||
|
||
func inviteRevokeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
|
||
if err := applogic.NewInviteAdminLogic(r.Context(), svcCtx).Revoke(id); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, map[string]string{"status": "ok"})
|
||
}
|
||
}
|
||
|
||
func orgUnitListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
items, err := applogic.NewOrgUnitLogic(r.Context(), svcCtx).List()
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, map[string]any{"items": items, "max_depth": 5})
|
||
}
|
||
}
|
||
|
||
func orgUnitCreateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
var req types.OrgUnitCreateReq
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
ou, err := applogic.NewOrgUnitLogic(r.Context(), svcCtx).Create(&req)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.WriteJson(w, http.StatusCreated, ou)
|
||
}
|
||
}
|
||
|
||
func orgUnitUpdateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
|
||
var req types.OrgUnitUpdateReq
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
ou, err := applogic.NewOrgUnitLogic(r.Context(), svcCtx).Update(id, &req)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, ou)
|
||
}
|
||
}
|
||
|
||
func orgUnitDeleteHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
id, _ := strconv.ParseInt(pathvar.Vars(r)["id"], 10, 64)
|
||
if err := applogic.NewOrgUnitLogic(r.Context(), svcCtx).Delete(id); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, map[string]string{"status": "ok"})
|
||
}
|
||
}
|
||
|
||
func auditListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||
size, _ := strconv.Atoi(r.URL.Query().Get("page_size"))
|
||
if page <= 0 {
|
||
page = 1
|
||
}
|
||
if size <= 0 {
|
||
size = 50
|
||
}
|
||
items, total, err := svcCtx.Audit.List(r.Context(), authx.TenantID(r.Context()), size, (page-1)*size)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, map[string]any{"items": items, "total": total, "page": page, "page_size": size})
|
||
}
|
||
}
|
||
|
||
func uploadHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
if err := r.ParseMultipartForm(32 << 20); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
file, hdr, err := r.FormFile("file")
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, "file required")
|
||
return
|
||
}
|
||
defer file.Close()
|
||
ct := hdr.Header.Get("Content-Type")
|
||
if ct == "" {
|
||
ct = "application/octet-stream"
|
||
}
|
||
meta, err := svcCtx.Objects.Put(r.Context(), authx.TenantID(r.Context()), hdr.Filename, ct, file, hdr.Size)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
_ = svcCtx.Audit.Log(r.Context(), authx.TenantID(r.Context()), authx.UserID(r.Context()), "storage.upload", audit.DetailJSON(meta))
|
||
httpx.OkJson(w, types.UploadResp{
|
||
Key: meta.Key, URL: meta.URL, Filename: meta.Filename, ContentType: meta.ContentType, Size: meta.Size,
|
||
})
|
||
}
|
||
}
|
||
|
||
func downloadHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
// path: /api/v1/storage/t1/20260101/xxx.png → key from URL after /storage/
|
||
idx := strings.Index(r.URL.Path, "/storage/")
|
||
if idx < 0 {
|
||
authx.WriteError(w, http.StatusNotFound, "not found")
|
||
return
|
||
}
|
||
key := r.URL.Path[idx+len("/storage/"):]
|
||
rc, meta, err := svcCtx.Objects.Open(r.Context(), key)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusNotFound, err.Error())
|
||
return
|
||
}
|
||
defer rc.Close()
|
||
if meta.ContentType != "" {
|
||
w.Header().Set("Content-Type", meta.ContentType)
|
||
}
|
||
if meta.Filename != "" {
|
||
w.Header().Set("Content-Disposition", "inline; filename="+meta.Filename)
|
||
}
|
||
_, _ = io.Copy(w, rc)
|
||
}
|
||
}
|
||
|
||
func capsuleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
slug := pathvar.Vars(r)["slug"]
|
||
resp, err := applogic.NewCapsuleLogic(r.Context(), svcCtx).Build(slug)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, resp)
|
||
}
|
||
}
|
||
|
||
func importHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
vars := pathvar.Vars(r)
|
||
if err := r.ParseMultipartForm(8 << 20); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
file, hdr, err := r.FormFile("file")
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, "file required")
|
||
return
|
||
}
|
||
defer file.Close()
|
||
filename := "import.xlsx"
|
||
if hdr != nil && hdr.Filename != "" {
|
||
filename = hdr.Filename
|
||
}
|
||
resp, err := applogic.NewCrudLogic(r.Context(), svcCtx).ImportRows(vars["slug"], vars["resource"], filename, file)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, resp)
|
||
}
|
||
}
|
||
|
||
func exportHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
vars := pathvar.Vars(r)
|
||
format := strings.ToLower(r.URL.Query().Get("format"))
|
||
if format == "csv" {
|
||
raw, err := applogic.NewCrudLogic(r.Context(), svcCtx).ExportCSV(vars["slug"], vars["resource"])
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
|
||
w.Header().Set("Content-Disposition", "attachment; filename="+vars["resource"]+".csv")
|
||
_, _ = w.Write(raw)
|
||
return
|
||
}
|
||
raw, name, err := applogic.NewCrudLogic(r.Context(), svcCtx).ExportExcel(vars["slug"], vars["resource"])
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||
w.Header().Set("Content-Disposition", "attachment; filename="+name)
|
||
_, _ = w.Write(raw)
|
||
}
|
||
}
|
||
|
||
func aggregateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
vars := pathvar.Vars(r)
|
||
q := r.URL.Query()
|
||
resp, err := applogic.NewCrudLogic(r.Context(), svcCtx).Aggregate(
|
||
vars["slug"], vars["resource"], q.Get("group_by"), q.Get("sum"),
|
||
)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, resp)
|
||
}
|
||
}
|
||
|
||
func listAppsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
resp, err := applogic.NewPublishLogic(r.Context(), svcCtx).ListApps()
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, resp)
|
||
}
|
||
}
|
||
|
||
func saveDraftHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
slug := pathvar.Vars(r)["slug"]
|
||
var req types.DraftReq
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
resp, err := applogic.NewPublishLogic(r.Context(), svcCtx).SaveDraft(slug, &req)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, resp)
|
||
}
|
||
}
|
||
|
||
func publishHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
slug := pathvar.Vars(r)["slug"]
|
||
var req types.PublishReq
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
resp, err := applogic.NewPublishLogic(r.Context(), svcCtx).Publish(slug, &req)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, resp)
|
||
}
|
||
}
|
||
|
||
func getBlueprintHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
slug := pathvar.Vars(r)["slug"]
|
||
bp, err := applogic.NewCrudLogic(r.Context(), svcCtx).GetBlueprint(slug)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusNotFound, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, bp)
|
||
}
|
||
}
|
||
|
||
func publicBlueprintHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
slug := pathvar.Vars(r)["slug"]
|
||
app, err := svcCtx.Meta.FindPublishedBySlug(r.Context(), slug)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusNotFound, err.Error())
|
||
return
|
||
}
|
||
if app.Blueprint == nil {
|
||
authx.WriteError(w, http.StatusNotFound, "blueprint missing")
|
||
return
|
||
}
|
||
httpx.OkJson(w, app.Blueprint)
|
||
}
|
||
}
|
||
|
||
func publicModulePathBlueprintHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
token := pathvar.Vars(r)["token"]
|
||
secret := svcCtx.Config.Agent.CapsuleSecret
|
||
if secret == "" {
|
||
secret = svcCtx.JWT.AccessSecret
|
||
}
|
||
claim, err := agentcap.OpenModulePath(secret, token)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusNotFound, "invalid access path")
|
||
return
|
||
}
|
||
app, err := svcCtx.Meta.GetBySlug(r.Context(), claim.TenantID, claim.Slug)
|
||
if err != nil || app == nil || app.Blueprint == nil || app.Status != meta.StatusPublished {
|
||
authx.WriteError(w, http.StatusNotFound, "module not found")
|
||
return
|
||
}
|
||
httpx.OkJson(w, app.Blueprint)
|
||
}
|
||
}
|
||
|
||
func publicListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
vars := pathvar.Vars(r)
|
||
slug, resource := vars["slug"], vars["resource"]
|
||
if apidef.IsReservedResource(resource) {
|
||
authx.WriteError(w, http.StatusNotFound, "not found")
|
||
return
|
||
}
|
||
app, err := svcCtx.Meta.FindPublishedBySlug(r.Context(), slug)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusNotFound, err.Error())
|
||
return
|
||
}
|
||
q := r.URL.Query()
|
||
page, size := applogic.ParsePage(q.Get("page"), q.Get("page_size"))
|
||
filters := map[string]string{}
|
||
for k, vs := range q {
|
||
if strings.HasPrefix(k, "filter.") && len(vs) > 0 {
|
||
filters[strings.TrimPrefix(k, "filter.")] = vs[0]
|
||
}
|
||
}
|
||
// 以应用所属租户读取数据,无需登录态
|
||
ctx := authx.WithClaims(r.Context(), app.TenantID, 0, authx.Role只读)
|
||
resp, err := applogic.NewCrudLogic(ctx, svcCtx).List(slug, resource, page, size, filters, q.Get("sort"))
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, resp)
|
||
}
|
||
}
|
||
|
||
func listHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
vars := pathvar.Vars(r)
|
||
slug, resource := vars["slug"], vars["resource"]
|
||
if apidef.IsReservedResource(resource) {
|
||
authx.WriteError(w, http.StatusNotFound, "not found")
|
||
return
|
||
}
|
||
q := r.URL.Query()
|
||
page, size := applogic.ParsePage(q.Get("page"), q.Get("page_size"))
|
||
filters := map[string]string{}
|
||
for k, vs := range q {
|
||
if strings.HasPrefix(k, "filter.") && len(vs) > 0 {
|
||
filters[strings.TrimPrefix(k, "filter.")] = vs[0]
|
||
}
|
||
}
|
||
resp, err := applogic.NewCrudLogic(r.Context(), svcCtx).List(slug, resource, page, size, filters, q.Get("sort"))
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, resp)
|
||
}
|
||
}
|
||
|
||
func createHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
vars := pathvar.Vars(r)
|
||
if apidef.IsReservedResource(vars["resource"]) {
|
||
authx.WriteError(w, http.StatusBadRequest, "reserved resource name")
|
||
return
|
||
}
|
||
body, err := readJSONObject(r)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
row, err := applogic.NewCrudLogic(r.Context(), svcCtx).Create(vars["slug"], vars["resource"], body)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.WriteJson(w, http.StatusCreated, row)
|
||
}
|
||
}
|
||
|
||
func getHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
vars := pathvar.Vars(r)
|
||
row, err := applogic.NewCrudLogic(r.Context(), svcCtx).Get(vars["slug"], vars["resource"], vars["id"])
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusNotFound, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, row)
|
||
}
|
||
}
|
||
|
||
func updateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
vars := pathvar.Vars(r)
|
||
body, err := readJSONObject(r)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
row, err := applogic.NewCrudLogic(r.Context(), svcCtx).Update(vars["slug"], vars["resource"], vars["id"], body)
|
||
if err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
httpx.OkJson(w, row)
|
||
}
|
||
}
|
||
|
||
func deleteHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
vars := pathvar.Vars(r)
|
||
if err := applogic.NewCrudLogic(r.Context(), svcCtx).Delete(vars["slug"], vars["resource"], vars["id"]); err != nil {
|
||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
w.WriteHeader(http.StatusNoContent)
|
||
}
|
||
}
|
||
|
||
func readJSONObject(r *http.Request) (map[string]any, error) {
|
||
defer r.Body.Close()
|
||
raw, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var body map[string]any
|
||
if err := json.Unmarshal(raw, &body); err != nil {
|
||
return nil, err
|
||
}
|
||
return body, nil
|
||
}
|