diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d3d743b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,32 @@ +.git +.gitea +.github +.gitignore + +# IDE +.idea +.vscode +*.swp +*.swo + +# Build output +build/ +dist/ + +# Environment +.env +.env.* +*.local + +# Docs +coverage.out +coverage.html +*.log + +# OS +.DS_Store +Thumbs.db + +# Uploads content +uploads/* +!uploads/.gitkeep diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..2130d48 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,34 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + name: Build & Test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + + - name: Check go.mod + run: | + go mod tidy + git diff --exit-code go.mod go.sum + + - name: Vet + run: go vet ./... + + - name: Test + run: go test ./... -v -race -coverprofile=coverage.out + + - name: Build + run: go build -o build/mengstack ./cmd/server diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d8e20a2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +# Build stage +FROM golang:1.23-alpine AS builder + +WORKDIR /app + +RUN apk add --no-cache git + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/mengstack ./cmd/server + +# Runtime stage +FROM alpine:3.20 + +RUN apk add --no-cache ca-certificates tzdata + +RUN addgroup -S mengstack && adduser -S mengstack -G mengstack + +WORKDIR /app + +COPY --from=builder /app/mengstack . +COPY --from=builder /app/configs ./configs + +RUN mkdir -p uploads logs && chown -R mengstack:mengstack /app + +USER mengstack + +EXPOSE 2222 + +CMD ["./mengstack"] diff --git a/Makefile b/Makefile index 719d17c..ae20d79 100644 --- a/Makefile +++ b/Makefile @@ -31,6 +31,9 @@ lint: tidy: go mod tidy +docker-build: + docker build -t mengstack:latest . + docker-up: docker-compose up -d diff --git a/docker-compose.yml b/docker-compose.yml index edecb0b..ade3f81 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,7 @@ services: api: build: . + restart: unless-stopped ports: - "${SERVER_PORT:-2222}:2222" environment: diff --git a/docs/docs.go b/docs/docs.go index 22e550f..0a8b565 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -15,6 +15,1716 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/api/v1/audit/logs": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "分页查询审计日志,支持按用户、操作、资源筛选", + "produces": [ + "application/json" + ], + "tags": [ + "Audit" + ], + "summary": "查询审计日志", + "parameters": [ + { + "type": "integer", + "description": "按用户 ID 筛选", + "name": "user_id", + "in": "query" + }, + { + "type": "string", + "description": "按操作筛选", + "name": "action", + "in": "query" + }, + { + "type": "string", + "description": "按资源类型筛选", + "name": "resource", + "in": "query" + }, + { + "type": "integer", + "description": "页码(默认 1)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页条数(默认 20)", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/notifications": { + "post": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "向指定用户发送通知", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Notification" + ], + "summary": "创建通知", + "parameters": [ + { + "description": "通知内容", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.CreateNotificationRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.NotificationDTO" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/notifications/read-all": { + "put": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "将当前用户的所有通知标记为已读", + "produces": [ + "application/json" + ], + "tags": [ + "Notification" + ], + "summary": "全部标记已读", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/notifications/unread-count": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "获取当前用户的未读通知数量", + "produces": [ + "application/json" + ], + "tags": [ + "Notification" + ], + "summary": "获取未读通知数", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/notifications/user/{userId}": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "分页获取指定用户的通知列表", + "produces": [ + "application/json" + ], + "tags": [ + "Notification" + ], + "summary": "获取用户通知列表", + "parameters": [ + { + "type": "integer", + "description": "用户 ID", + "name": "userId", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "页码(默认 1)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页条数(默认 20)", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/notifications/{id}": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "根据 ID 获取指定通知的详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "Notification" + ], + "summary": "获取通知详情", + "parameters": [ + { + "type": "integer", + "description": "通知 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.NotificationDTO" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "删除指定通知", + "tags": [ + "Notification" + ], + "summary": "删除通知", + "parameters": [ + { + "type": "integer", + "description": "通知 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/notifications/{id}/read": { + "put": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "将指定通知标记为已读", + "tags": [ + "Notification" + ], + "summary": "标记通知已读", + "parameters": [ + { + "type": "integer", + "description": "通知 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/org/tenants": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "获取系统中的所有租户列表", + "produces": [ + "application/json" + ], + "tags": [ + "Org" + ], + "summary": "列出所有租户", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.TenantDTO" + } + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "创建新的租户(组织),需提供名称和 slug", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Org" + ], + "summary": "创建租户", + "parameters": [ + { + "description": "租户信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.CreateTenantRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.TenantDTO" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/org/tenants/{id}": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "根据 ID 获取指定租户的详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "Org" + ], + "summary": "获取租户详情", + "parameters": [ + { + "type": "string", + "description": "租户 ID (UUID)", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.TenantDTO" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "更新租户的名称、slug 或状态", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Org" + ], + "summary": "更新租户", + "parameters": [ + { + "type": "string", + "description": "租户 ID (UUID)", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新内容", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.UpdateTenantRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.TenantDTO" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "删除指定租户及其所有关联数据", + "tags": [ + "Org" + ], + "summary": "删除租户", + "parameters": [ + { + "type": "string", + "description": "租户 ID (UUID)", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/rbac/permissions": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "获取所有权限列表,可按模块筛选", + "produces": [ + "application/json" + ], + "tags": [ + "RBAC" + ], + "summary": "列出权限", + "parameters": [ + { + "type": "string", + "description": "按模块筛选", + "name": "module", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.PermissionDTO" + } + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/rbac/roles": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "获取当前租户下的所有角色", + "produces": [ + "application/json" + ], + "tags": [ + "RBAC" + ], + "summary": "列出角色", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.RoleDTO" + } + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "在当前租户下创建新角色", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "RBAC" + ], + "summary": "创建角色", + "parameters": [ + { + "description": "角色信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.CreateRoleRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.RoleDTO" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/rbac/roles/{id}": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "根据 ID 获取角色信息,包含权限列表", + "produces": [ + "application/json" + ], + "tags": [ + "RBAC" + ], + "summary": "获取角色详情", + "parameters": [ + { + "type": "integer", + "description": "角色 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.RoleDTO" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "更新角色名称或描述", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "RBAC" + ], + "summary": "更新角色", + "parameters": [ + { + "type": "integer", + "description": "角色 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新内容", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.UpdateRoleRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.RoleDTO" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "删除指定角色(系统角色不可删除)", + "tags": [ + "RBAC" + ], + "summary": "删除角色", + "parameters": [ + { + "type": "integer", + "description": "角色 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/rbac/roles/{id}/permissions": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "获取指定角色已分配的权限列表", + "produces": [ + "application/json" + ], + "tags": [ + "RBAC" + ], + "summary": "获取角色权限列表", + "parameters": [ + { + "type": "integer", + "description": "角色 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.PermissionDTO" + } + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "为角色批量设置权限(覆盖原有权限)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "RBAC" + ], + "summary": "设置角色权限", + "parameters": [ + { + "type": "integer", + "description": "角色 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "权限 ID 列表", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.SetRolePermissionsRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/rbac/users/{userId}/roles": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "获取指定用户的所有角色及权限", + "produces": [ + "application/json" + ], + "tags": [ + "RBAC" + ], + "summary": "获取用户角色", + "parameters": [ + { + "type": "integer", + "description": "用户 ID", + "name": "userId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.RoleDTO" + } + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "为指定用户分配一个角色", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "RBAC" + ], + "summary": "分配角色给用户", + "parameters": [ + { + "type": "integer", + "description": "用户 ID", + "name": "userId", + "in": "path", + "required": true + }, + { + "description": "角色 ID", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.AssignRoleRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/rbac/users/{userId}/roles/{roleId}": { + "delete": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "从指定用户移除一个角色", + "tags": [ + "RBAC" + ], + "summary": "移除用户角色", + "parameters": [ + { + "type": "integer", + "description": "用户 ID", + "name": "userId", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "角色 ID", + "name": "roleId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/settings": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "获取所有全局配置项", + "produces": [ + "application/json" + ], + "tags": [ + "Settings" + ], + "summary": "列出全局配置", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.SettingDTO" + } + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/settings/global/{key}": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "根据 key 获取全局(非租户)配置项", + "produces": [ + "application/json" + ], + "tags": [ + "Settings" + ], + "summary": "获取全局配置", + "parameters": [ + { + "type": "string", + "description": "配置键名", + "name": "key", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.SettingDTO" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "创建或更新全局配置项", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Settings" + ], + "summary": "更新全局配置", + "parameters": [ + { + "type": "string", + "description": "配置键名", + "name": "key", + "in": "path", + "required": true + }, + { + "description": "配置值", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.UpdateSettingRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/settings/tenant": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "获取当前租户的所有配置项", + "produces": [ + "application/json" + ], + "tags": [ + "Settings" + ], + "summary": "列出租户配置", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.SettingDTO" + } + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/settings/tenant/{key}": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "根据 key 获取当前租户的配置项", + "produces": [ + "application/json" + ], + "tags": [ + "Settings" + ], + "summary": "获取租户配置", + "parameters": [ + { + "type": "string", + "description": "配置键名", + "name": "key", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.SettingDTO" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "创建或更新当前租户的配置项", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Settings" + ], + "summary": "更新租户配置", + "parameters": [ + { + "type": "string", + "description": "配置键名", + "name": "key", + "in": "path", + "required": true + }, + { + "description": "配置值", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.UpdateSettingRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "删除当前租户的指定配置项", + "tags": [ + "Settings" + ], + "summary": "删除租户配置", + "parameters": [ + { + "type": "string", + "description": "配置键名", + "name": "key", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, "/auth/login": { "post": { "description": "使用邮箱和密码登录,返回 JWT token", @@ -35,7 +1745,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/mengstack_internal_modules_auth_domain.LoginRequest" + "$ref": "#/definitions/domain.LoginRequest" } } ], @@ -45,13 +1755,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/mengstack_internal_modules_auth_domain.TokenPair" + "$ref": "#/definitions/domain.TokenPair" } } } @@ -61,13 +1771,13 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" } } } @@ -103,13 +1813,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/mengstack_internal_modules_auth_domain.TokenPair" + "$ref": "#/definitions/domain.TokenPair" } } } @@ -119,13 +1829,13 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" } } } @@ -151,7 +1861,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/mengstack_internal_modules_auth_domain.RegisterRequest" + "$ref": "#/definitions/domain.RegisterRequest" } } ], @@ -161,13 +1871,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/mengstack_internal_modules_auth_domain.TokenPair" + "$ref": "#/definitions/domain.TokenPair" } } } @@ -177,13 +1887,13 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" } } } @@ -257,7 +1967,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/mengstack_internal_modules_auth_domain.ChangePasswordRequest" + "$ref": "#/definitions/domain.ChangePasswordRequest" } } ], @@ -265,19 +1975,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" } } } @@ -305,7 +2015,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" } } } @@ -335,13 +2045,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/mengstack_internal_modules_auth_domain.UserDTO" + "$ref": "#/definitions/domain.UserDTO" } } } @@ -351,13 +2061,13 @@ const docTemplate = `{ "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" } } } @@ -365,22 +2075,18 @@ const docTemplate = `{ } }, "definitions": { - "mengstack_internal_kernel_response.Response": { + "domain.AssignRoleRequest": { "type": "object", + "required": [ + "role_id" + ], "properties": { - "code": { + "role_id": { "type": "integer" - }, - "data": {}, - "message": { - "type": "string" - }, - "trace_id": { - "type": "string" } } }, - "mengstack_internal_modules_auth_domain.ChangePasswordRequest": { + "domain.ChangePasswordRequest": { "type": "object", "required": [ "new_password", @@ -397,7 +2103,57 @@ const docTemplate = `{ } } }, - "mengstack_internal_modules_auth_domain.LoginRequest": { + "domain.CreateNotificationRequest": { + "type": "object", + "required": [ + "title", + "user_id" + ], + "properties": { + "content": { + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + }, + "user_id": { + "type": "integer" + } + } + }, + "domain.CreateRoleRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "domain.CreateTenantRequest": { + "type": "object", + "required": [ + "name", + "slug" + ], + "properties": { + "name": { + "type": "string" + }, + "slug": { + "type": "string" + } + } + }, + "domain.LoginRequest": { "type": "object", "required": [ "email", @@ -412,7 +2168,56 @@ const docTemplate = `{ } } }, - "mengstack_internal_modules_auth_domain.RegisterRequest": { + "domain.NotificationDTO": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_read": { + "type": "boolean" + }, + "tenant_id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + }, + "user_id": { + "type": "integer" + } + } + }, + "domain.PermissionDTO": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "module": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "domain.RegisterRequest": { "type": "object", "required": [ "email", @@ -438,7 +2243,99 @@ const docTemplate = `{ } } }, - "mengstack_internal_modules_auth_domain.TokenPair": { + "domain.RoleDTO": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_system": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "permissions": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.PermissionDTO" + } + }, + "tenant_id": { + "type": "string" + } + } + }, + "domain.SetRolePermissionsRequest": { + "type": "object", + "required": [ + "permission_ids" + ], + "properties": { + "permission_ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "domain.SettingDTO": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "key": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "tenant_id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "updated_by": { + "type": "integer" + }, + "value": { + "type": "string" + } + } + }, + "domain.TenantDTO": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "updated_at": { + "type": "string" + } + } + }, + "domain.TokenPair": { "type": "object", "properties": { "access_token": { @@ -452,7 +2349,46 @@ const docTemplate = `{ } } }, - "mengstack_internal_modules_auth_domain.UserDTO": { + "domain.UpdateRoleRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "domain.UpdateSettingRequest": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "type": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "domain.UpdateTenantRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "status": { + "type": "integer" + } + } + }, + "domain.UserDTO": { "type": "object", "properties": { "avatar": { @@ -477,6 +2413,21 @@ const docTemplate = `{ "type": "string" } } + }, + "response.Response": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "message": { + "type": "string" + }, + "trace_id": { + "type": "string" + } + } } }, "securityDefinitions": { diff --git a/docs/swagger.json b/docs/swagger.json index b3fd63f..a5a4a0b 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -9,6 +9,1716 @@ "host": "localhost:2222", "basePath": "/api/v1", "paths": { + "/api/v1/audit/logs": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "分页查询审计日志,支持按用户、操作、资源筛选", + "produces": [ + "application/json" + ], + "tags": [ + "Audit" + ], + "summary": "查询审计日志", + "parameters": [ + { + "type": "integer", + "description": "按用户 ID 筛选", + "name": "user_id", + "in": "query" + }, + { + "type": "string", + "description": "按操作筛选", + "name": "action", + "in": "query" + }, + { + "type": "string", + "description": "按资源类型筛选", + "name": "resource", + "in": "query" + }, + { + "type": "integer", + "description": "页码(默认 1)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页条数(默认 20)", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/notifications": { + "post": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "向指定用户发送通知", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Notification" + ], + "summary": "创建通知", + "parameters": [ + { + "description": "通知内容", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.CreateNotificationRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.NotificationDTO" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/notifications/read-all": { + "put": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "将当前用户的所有通知标记为已读", + "produces": [ + "application/json" + ], + "tags": [ + "Notification" + ], + "summary": "全部标记已读", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/notifications/unread-count": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "获取当前用户的未读通知数量", + "produces": [ + "application/json" + ], + "tags": [ + "Notification" + ], + "summary": "获取未读通知数", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/notifications/user/{userId}": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "分页获取指定用户的通知列表", + "produces": [ + "application/json" + ], + "tags": [ + "Notification" + ], + "summary": "获取用户通知列表", + "parameters": [ + { + "type": "integer", + "description": "用户 ID", + "name": "userId", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "页码(默认 1)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页条数(默认 20)", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/notifications/{id}": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "根据 ID 获取指定通知的详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "Notification" + ], + "summary": "获取通知详情", + "parameters": [ + { + "type": "integer", + "description": "通知 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.NotificationDTO" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "删除指定通知", + "tags": [ + "Notification" + ], + "summary": "删除通知", + "parameters": [ + { + "type": "integer", + "description": "通知 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/notifications/{id}/read": { + "put": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "将指定通知标记为已读", + "tags": [ + "Notification" + ], + "summary": "标记通知已读", + "parameters": [ + { + "type": "integer", + "description": "通知 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/org/tenants": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "获取系统中的所有租户列表", + "produces": [ + "application/json" + ], + "tags": [ + "Org" + ], + "summary": "列出所有租户", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.TenantDTO" + } + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "创建新的租户(组织),需提供名称和 slug", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Org" + ], + "summary": "创建租户", + "parameters": [ + { + "description": "租户信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.CreateTenantRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.TenantDTO" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/org/tenants/{id}": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "根据 ID 获取指定租户的详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "Org" + ], + "summary": "获取租户详情", + "parameters": [ + { + "type": "string", + "description": "租户 ID (UUID)", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.TenantDTO" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "更新租户的名称、slug 或状态", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Org" + ], + "summary": "更新租户", + "parameters": [ + { + "type": "string", + "description": "租户 ID (UUID)", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新内容", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.UpdateTenantRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.TenantDTO" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "删除指定租户及其所有关联数据", + "tags": [ + "Org" + ], + "summary": "删除租户", + "parameters": [ + { + "type": "string", + "description": "租户 ID (UUID)", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/rbac/permissions": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "获取所有权限列表,可按模块筛选", + "produces": [ + "application/json" + ], + "tags": [ + "RBAC" + ], + "summary": "列出权限", + "parameters": [ + { + "type": "string", + "description": "按模块筛选", + "name": "module", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.PermissionDTO" + } + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/rbac/roles": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "获取当前租户下的所有角色", + "produces": [ + "application/json" + ], + "tags": [ + "RBAC" + ], + "summary": "列出角色", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.RoleDTO" + } + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "在当前租户下创建新角色", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "RBAC" + ], + "summary": "创建角色", + "parameters": [ + { + "description": "角色信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.CreateRoleRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.RoleDTO" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/rbac/roles/{id}": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "根据 ID 获取角色信息,包含权限列表", + "produces": [ + "application/json" + ], + "tags": [ + "RBAC" + ], + "summary": "获取角色详情", + "parameters": [ + { + "type": "integer", + "description": "角色 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.RoleDTO" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "更新角色名称或描述", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "RBAC" + ], + "summary": "更新角色", + "parameters": [ + { + "type": "integer", + "description": "角色 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新内容", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.UpdateRoleRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.RoleDTO" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "删除指定角色(系统角色不可删除)", + "tags": [ + "RBAC" + ], + "summary": "删除角色", + "parameters": [ + { + "type": "integer", + "description": "角色 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/rbac/roles/{id}/permissions": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "获取指定角色已分配的权限列表", + "produces": [ + "application/json" + ], + "tags": [ + "RBAC" + ], + "summary": "获取角色权限列表", + "parameters": [ + { + "type": "integer", + "description": "角色 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.PermissionDTO" + } + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "为角色批量设置权限(覆盖原有权限)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "RBAC" + ], + "summary": "设置角色权限", + "parameters": [ + { + "type": "integer", + "description": "角色 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "权限 ID 列表", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.SetRolePermissionsRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/rbac/users/{userId}/roles": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "获取指定用户的所有角色及权限", + "produces": [ + "application/json" + ], + "tags": [ + "RBAC" + ], + "summary": "获取用户角色", + "parameters": [ + { + "type": "integer", + "description": "用户 ID", + "name": "userId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.RoleDTO" + } + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "为指定用户分配一个角色", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "RBAC" + ], + "summary": "分配角色给用户", + "parameters": [ + { + "type": "integer", + "description": "用户 ID", + "name": "userId", + "in": "path", + "required": true + }, + { + "description": "角色 ID", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.AssignRoleRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/rbac/users/{userId}/roles/{roleId}": { + "delete": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "从指定用户移除一个角色", + "tags": [ + "RBAC" + ], + "summary": "移除用户角色", + "parameters": [ + { + "type": "integer", + "description": "用户 ID", + "name": "userId", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "角色 ID", + "name": "roleId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/settings": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "获取所有全局配置项", + "produces": [ + "application/json" + ], + "tags": [ + "Settings" + ], + "summary": "列出全局配置", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.SettingDTO" + } + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/settings/global/{key}": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "根据 key 获取全局(非租户)配置项", + "produces": [ + "application/json" + ], + "tags": [ + "Settings" + ], + "summary": "获取全局配置", + "parameters": [ + { + "type": "string", + "description": "配置键名", + "name": "key", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.SettingDTO" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "创建或更新全局配置项", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Settings" + ], + "summary": "更新全局配置", + "parameters": [ + { + "type": "string", + "description": "配置键名", + "name": "key", + "in": "path", + "required": true + }, + { + "description": "配置值", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.UpdateSettingRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/settings/tenant": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "获取当前租户的所有配置项", + "produces": [ + "application/json" + ], + "tags": [ + "Settings" + ], + "summary": "列出租户配置", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.SettingDTO" + } + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/api/v1/settings/tenant/{key}": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "根据 key 获取当前租户的配置项", + "produces": [ + "application/json" + ], + "tags": [ + "Settings" + ], + "summary": "获取租户配置", + "parameters": [ + { + "type": "string", + "description": "配置键名", + "name": "key", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.SettingDTO" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "创建或更新当前租户的配置项", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Settings" + ], + "summary": "更新租户配置", + "parameters": [ + { + "type": "string", + "description": "配置键名", + "name": "key", + "in": "path", + "required": true + }, + { + "description": "配置值", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.UpdateSettingRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + }, + { + "TenantID": [] + } + ], + "description": "删除当前租户的指定配置项", + "tags": [ + "Settings" + ], + "summary": "删除租户配置", + "parameters": [ + { + "type": "string", + "description": "配置键名", + "name": "key", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, "/auth/login": { "post": { "description": "使用邮箱和密码登录,返回 JWT token", @@ -29,7 +1739,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/mengstack_internal_modules_auth_domain.LoginRequest" + "$ref": "#/definitions/domain.LoginRequest" } } ], @@ -39,13 +1749,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/mengstack_internal_modules_auth_domain.TokenPair" + "$ref": "#/definitions/domain.TokenPair" } } } @@ -55,13 +1765,13 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" } } } @@ -97,13 +1807,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/mengstack_internal_modules_auth_domain.TokenPair" + "$ref": "#/definitions/domain.TokenPair" } } } @@ -113,13 +1823,13 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" } } } @@ -145,7 +1855,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/mengstack_internal_modules_auth_domain.RegisterRequest" + "$ref": "#/definitions/domain.RegisterRequest" } } ], @@ -155,13 +1865,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/mengstack_internal_modules_auth_domain.TokenPair" + "$ref": "#/definitions/domain.TokenPair" } } } @@ -171,13 +1881,13 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" } } } @@ -251,7 +1961,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/mengstack_internal_modules_auth_domain.ChangePasswordRequest" + "$ref": "#/definitions/domain.ChangePasswordRequest" } } ], @@ -259,19 +1969,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" } } } @@ -299,7 +2009,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" } } } @@ -329,13 +2039,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/mengstack_internal_modules_auth_domain.UserDTO" + "$ref": "#/definitions/domain.UserDTO" } } } @@ -345,13 +2055,13 @@ "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/mengstack_internal_kernel_response.Response" + "$ref": "#/definitions/response.Response" } } } @@ -359,22 +2069,18 @@ } }, "definitions": { - "mengstack_internal_kernel_response.Response": { + "domain.AssignRoleRequest": { "type": "object", + "required": [ + "role_id" + ], "properties": { - "code": { + "role_id": { "type": "integer" - }, - "data": {}, - "message": { - "type": "string" - }, - "trace_id": { - "type": "string" } } }, - "mengstack_internal_modules_auth_domain.ChangePasswordRequest": { + "domain.ChangePasswordRequest": { "type": "object", "required": [ "new_password", @@ -391,7 +2097,57 @@ } } }, - "mengstack_internal_modules_auth_domain.LoginRequest": { + "domain.CreateNotificationRequest": { + "type": "object", + "required": [ + "title", + "user_id" + ], + "properties": { + "content": { + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + }, + "user_id": { + "type": "integer" + } + } + }, + "domain.CreateRoleRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "domain.CreateTenantRequest": { + "type": "object", + "required": [ + "name", + "slug" + ], + "properties": { + "name": { + "type": "string" + }, + "slug": { + "type": "string" + } + } + }, + "domain.LoginRequest": { "type": "object", "required": [ "email", @@ -406,7 +2162,56 @@ } } }, - "mengstack_internal_modules_auth_domain.RegisterRequest": { + "domain.NotificationDTO": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_read": { + "type": "boolean" + }, + "tenant_id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + }, + "user_id": { + "type": "integer" + } + } + }, + "domain.PermissionDTO": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "module": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "domain.RegisterRequest": { "type": "object", "required": [ "email", @@ -432,7 +2237,99 @@ } } }, - "mengstack_internal_modules_auth_domain.TokenPair": { + "domain.RoleDTO": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_system": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "permissions": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.PermissionDTO" + } + }, + "tenant_id": { + "type": "string" + } + } + }, + "domain.SetRolePermissionsRequest": { + "type": "object", + "required": [ + "permission_ids" + ], + "properties": { + "permission_ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "domain.SettingDTO": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "key": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "tenant_id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "updated_by": { + "type": "integer" + }, + "value": { + "type": "string" + } + } + }, + "domain.TenantDTO": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "updated_at": { + "type": "string" + } + } + }, + "domain.TokenPair": { "type": "object", "properties": { "access_token": { @@ -446,7 +2343,46 @@ } } }, - "mengstack_internal_modules_auth_domain.UserDTO": { + "domain.UpdateRoleRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "domain.UpdateSettingRequest": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "type": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "domain.UpdateTenantRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "status": { + "type": "integer" + } + } + }, + "domain.UserDTO": { "type": "object", "properties": { "avatar": { @@ -471,6 +2407,21 @@ "type": "string" } } + }, + "response.Response": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "message": { + "type": "string" + }, + "trace_id": { + "type": "string" + } + } } }, "securityDefinitions": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 10a5447..b9b16d3 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,16 +1,13 @@ basePath: /api/v1 definitions: - mengstack_internal_kernel_response.Response: + domain.AssignRoleRequest: properties: - code: + role_id: type: integer - data: {} - message: - type: string - trace_id: - type: string + required: + - role_id type: object - mengstack_internal_modules_auth_domain.ChangePasswordRequest: + domain.ChangePasswordRequest: properties: new_password: maxLength: 128 @@ -22,7 +19,40 @@ definitions: - new_password - old_password type: object - mengstack_internal_modules_auth_domain.LoginRequest: + domain.CreateNotificationRequest: + properties: + content: + type: string + title: + type: string + type: + type: string + user_id: + type: integer + required: + - title + - user_id + type: object + domain.CreateRoleRequest: + properties: + description: + type: string + name: + type: string + required: + - name + type: object + domain.CreateTenantRequest: + properties: + name: + type: string + slug: + type: string + required: + - name + - slug + type: object + domain.LoginRequest: properties: email: type: string @@ -32,7 +62,39 @@ definitions: - email - password type: object - mengstack_internal_modules_auth_domain.RegisterRequest: + domain.NotificationDTO: + properties: + content: + type: string + created_at: + type: string + id: + type: integer + is_read: + type: boolean + tenant_id: + type: string + title: + type: string + type: + type: string + user_id: + type: integer + type: object + domain.PermissionDTO: + properties: + code: + type: string + description: + type: string + id: + type: integer + module: + type: string + name: + type: string + type: object + domain.RegisterRequest: properties: email: type: string @@ -51,7 +113,67 @@ definitions: - password - username type: object - mengstack_internal_modules_auth_domain.TokenPair: + domain.RoleDTO: + properties: + description: + type: string + id: + type: integer + is_system: + type: boolean + name: + type: string + permissions: + items: + $ref: '#/definitions/domain.PermissionDTO' + type: array + tenant_id: + type: string + type: object + domain.SetRolePermissionsRequest: + properties: + permission_ids: + items: + type: integer + type: array + required: + - permission_ids + type: object + domain.SettingDTO: + properties: + id: + type: integer + key: + type: string + scope: + type: string + tenant_id: + type: string + type: + type: string + updated_at: + type: string + updated_by: + type: integer + value: + type: string + type: object + domain.TenantDTO: + properties: + created_at: + type: string + id: + type: string + name: + type: string + slug: + type: string + status: + type: integer + updated_at: + type: string + type: object + domain.TokenPair: properties: access_token: type: string @@ -60,7 +182,32 @@ definitions: refresh_token: type: string type: object - mengstack_internal_modules_auth_domain.UserDTO: + domain.UpdateRoleRequest: + properties: + description: + type: string + name: + type: string + type: object + domain.UpdateSettingRequest: + properties: + type: + type: string + value: + type: string + required: + - value + type: object + domain.UpdateTenantRequest: + properties: + name: + type: string + slug: + type: string + status: + type: integer + type: object + domain.UserDTO: properties: avatar: type: string @@ -77,6 +224,16 @@ definitions: username: type: string type: object + response.Response: + properties: + code: + type: integer + data: {} + message: + type: string + trace_id: + type: string + type: object host: localhost:2222 info: contact: {} @@ -84,6 +241,1015 @@ info: title: MengStack API version: 0.1.0 paths: + /api/v1/audit/logs: + get: + description: 分页查询审计日志,支持按用户、操作、资源筛选 + parameters: + - description: 按用户 ID 筛选 + in: query + name: user_id + type: integer + - description: 按操作筛选 + in: query + name: action + type: string + - description: 按资源类型筛选 + in: query + name: resource + type: string + - description: 页码(默认 1) + in: query + name: page + type: integer + - description: 每页条数(默认 20) + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 查询审计日志 + tags: + - Audit + /api/v1/notifications: + post: + consumes: + - application/json + description: 向指定用户发送通知 + parameters: + - description: 通知内容 + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.CreateNotificationRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/domain.NotificationDTO' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 创建通知 + tags: + - Notification + /api/v1/notifications/{id}: + delete: + description: 删除指定通知 + parameters: + - description: 通知 ID + in: path + name: id + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.Response' + "404": + description: Not Found + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 删除通知 + tags: + - Notification + get: + description: 根据 ID 获取指定通知的详细信息 + parameters: + - description: 通知 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/domain.NotificationDTO' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.Response' + "404": + description: Not Found + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 获取通知详情 + tags: + - Notification + /api/v1/notifications/{id}/read: + put: + description: 将指定通知标记为已读 + parameters: + - description: 通知 ID + in: path + name: id + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.Response' + "404": + description: Not Found + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 标记通知已读 + tags: + - Notification + /api/v1/notifications/read-all: + put: + description: 将当前用户的所有通知标记为已读 + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 全部标记已读 + tags: + - Notification + /api/v1/notifications/unread-count: + get: + description: 获取当前用户的未读通知数量 + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 获取未读通知数 + tags: + - Notification + /api/v1/notifications/user/{userId}: + get: + description: 分页获取指定用户的通知列表 + parameters: + - description: 用户 ID + in: path + name: userId + required: true + type: integer + - description: 页码(默认 1) + in: query + name: page + type: integer + - description: 每页条数(默认 20) + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.Response' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 获取用户通知列表 + tags: + - Notification + /api/v1/org/tenants: + get: + description: 获取系统中的所有租户列表 + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + items: + $ref: '#/definitions/domain.TenantDTO' + type: array + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 列出所有租户 + tags: + - Org + post: + consumes: + - application/json + description: 创建新的租户(组织),需提供名称和 slug + parameters: + - description: 租户信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.CreateTenantRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/domain.TenantDTO' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.Response' + "409": + description: Conflict + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 创建租户 + tags: + - Org + /api/v1/org/tenants/{id}: + delete: + description: 删除指定租户及其所有关联数据 + parameters: + - description: 租户 ID (UUID) + in: path + name: id + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.Response' + "404": + description: Not Found + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 删除租户 + tags: + - Org + get: + description: 根据 ID 获取指定租户的详细信息 + parameters: + - description: 租户 ID (UUID) + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/domain.TenantDTO' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.Response' + "404": + description: Not Found + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 获取租户详情 + tags: + - Org + put: + consumes: + - application/json + description: 更新租户的名称、slug 或状态 + parameters: + - description: 租户 ID (UUID) + in: path + name: id + required: true + type: string + - description: 更新内容 + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.UpdateTenantRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/domain.TenantDTO' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.Response' + "404": + description: Not Found + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 更新租户 + tags: + - Org + /api/v1/rbac/permissions: + get: + description: 获取所有权限列表,可按模块筛选 + parameters: + - description: 按模块筛选 + in: query + name: module + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + items: + $ref: '#/definitions/domain.PermissionDTO' + type: array + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 列出权限 + tags: + - RBAC + /api/v1/rbac/roles: + get: + description: 获取当前租户下的所有角色 + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + items: + $ref: '#/definitions/domain.RoleDTO' + type: array + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 列出角色 + tags: + - RBAC + post: + consumes: + - application/json + description: 在当前租户下创建新角色 + parameters: + - description: 角色信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.CreateRoleRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/domain.RoleDTO' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.Response' + "409": + description: Conflict + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 创建角色 + tags: + - RBAC + /api/v1/rbac/roles/{id}: + delete: + description: 删除指定角色(系统角色不可删除) + parameters: + - description: 角色 ID + in: path + name: id + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.Response' + "403": + description: Forbidden + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 删除角色 + tags: + - RBAC + get: + description: 根据 ID 获取角色信息,包含权限列表 + parameters: + - description: 角色 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/domain.RoleDTO' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.Response' + "404": + description: Not Found + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 获取角色详情 + tags: + - RBAC + put: + consumes: + - application/json + description: 更新角色名称或描述 + parameters: + - description: 角色 ID + in: path + name: id + required: true + type: integer + - description: 更新内容 + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.UpdateRoleRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/domain.RoleDTO' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.Response' + "404": + description: Not Found + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 更新角色 + tags: + - RBAC + /api/v1/rbac/roles/{id}/permissions: + get: + description: 获取指定角色已分配的权限列表 + parameters: + - description: 角色 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + items: + $ref: '#/definitions/domain.PermissionDTO' + type: array + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.Response' + "404": + description: Not Found + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 获取角色权限列表 + tags: + - RBAC + put: + consumes: + - application/json + description: 为角色批量设置权限(覆盖原有权限) + parameters: + - description: 角色 ID + in: path + name: id + required: true + type: integer + - description: 权限 ID 列表 + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.SetRolePermissionsRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.Response' + "404": + description: Not Found + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 设置角色权限 + tags: + - RBAC + /api/v1/rbac/users/{userId}/roles: + get: + description: 获取指定用户的所有角色及权限 + parameters: + - description: 用户 ID + in: path + name: userId + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + items: + $ref: '#/definitions/domain.RoleDTO' + type: array + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.Response' + "404": + description: Not Found + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 获取用户角色 + tags: + - RBAC + post: + consumes: + - application/json + description: 为指定用户分配一个角色 + parameters: + - description: 用户 ID + in: path + name: userId + required: true + type: integer + - description: 角色 ID + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.AssignRoleRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.Response' + "409": + description: Conflict + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 分配角色给用户 + tags: + - RBAC + /api/v1/rbac/users/{userId}/roles/{roleId}: + delete: + description: 从指定用户移除一个角色 + parameters: + - description: 用户 ID + in: path + name: userId + required: true + type: integer + - description: 角色 ID + in: path + name: roleId + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.Response' + "404": + description: Not Found + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 移除用户角色 + tags: + - RBAC + /api/v1/settings: + get: + description: 获取所有全局配置项 + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + items: + $ref: '#/definitions/domain.SettingDTO' + type: array + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 列出全局配置 + tags: + - Settings + /api/v1/settings/global/{key}: + get: + description: 根据 key 获取全局(非租户)配置项 + parameters: + - description: 配置键名 + in: path + name: key + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/domain.SettingDTO' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.Response' + "404": + description: Not Found + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 获取全局配置 + tags: + - Settings + put: + consumes: + - application/json + description: 创建或更新全局配置项 + parameters: + - description: 配置键名 + in: path + name: key + required: true + type: string + - description: 配置值 + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.UpdateSettingRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 更新全局配置 + tags: + - Settings + /api/v1/settings/tenant: + get: + description: 获取当前租户的所有配置项 + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + items: + $ref: '#/definitions/domain.SettingDTO' + type: array + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 列出租户配置 + tags: + - Settings + /api/v1/settings/tenant/{key}: + delete: + description: 删除当前租户的指定配置项 + parameters: + - description: 配置键名 + in: path + name: key + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.Response' + "404": + description: Not Found + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 删除租户配置 + tags: + - Settings + get: + description: 根据 key 获取当前租户的配置项 + parameters: + - description: 配置键名 + in: path + name: key + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/domain.SettingDTO' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.Response' + "404": + description: Not Found + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 获取租户配置 + tags: + - Settings + put: + consumes: + - application/json + description: 创建或更新当前租户的配置项 + parameters: + - description: 配置键名 + in: path + name: key + required: true + type: string + - description: 配置值 + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.UpdateSettingRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.Response' + security: + - Bearer: [] + - TenantID: [] + summary: 更新租户配置 + tags: + - Settings /auth/login: post: consumes: @@ -95,7 +1261,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/mengstack_internal_modules_auth_domain.LoginRequest' + $ref: '#/definitions/domain.LoginRequest' produces: - application/json responses: @@ -103,19 +1269,19 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/mengstack_internal_kernel_response.Response' + - $ref: '#/definitions/response.Response' - properties: data: - $ref: '#/definitions/mengstack_internal_modules_auth_domain.TokenPair' + $ref: '#/definitions/domain.TokenPair' type: object "400": description: Bad Request schema: - $ref: '#/definitions/mengstack_internal_kernel_response.Response' + $ref: '#/definitions/response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/mengstack_internal_kernel_response.Response' + $ref: '#/definitions/response.Response' summary: 用户登录 tags: - Auth @@ -138,19 +1304,19 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/mengstack_internal_kernel_response.Response' + - $ref: '#/definitions/response.Response' - properties: data: - $ref: '#/definitions/mengstack_internal_modules_auth_domain.TokenPair' + $ref: '#/definitions/domain.TokenPair' type: object "400": description: Bad Request schema: - $ref: '#/definitions/mengstack_internal_kernel_response.Response' + $ref: '#/definitions/response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/mengstack_internal_kernel_response.Response' + $ref: '#/definitions/response.Response' summary: 刷新 Token tags: - Auth @@ -165,7 +1331,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/mengstack_internal_modules_auth_domain.RegisterRequest' + $ref: '#/definitions/domain.RegisterRequest' produces: - application/json responses: @@ -173,19 +1339,19 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/mengstack_internal_kernel_response.Response' + - $ref: '#/definitions/response.Response' - properties: data: - $ref: '#/definitions/mengstack_internal_modules_auth_domain.TokenPair' + $ref: '#/definitions/domain.TokenPair' type: object "400": description: Bad Request schema: - $ref: '#/definitions/mengstack_internal_kernel_response.Response' + $ref: '#/definitions/response.Response' "409": description: Conflict schema: - $ref: '#/definitions/mengstack_internal_kernel_response.Response' + $ref: '#/definitions/response.Response' summary: 用户注册 tags: - Auth @@ -226,22 +1392,22 @@ paths: name: request required: true schema: - $ref: '#/definitions/mengstack_internal_modules_auth_domain.ChangePasswordRequest' + $ref: '#/definitions/domain.ChangePasswordRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/mengstack_internal_kernel_response.Response' + $ref: '#/definitions/response.Response' "400": description: Bad Request schema: - $ref: '#/definitions/mengstack_internal_kernel_response.Response' + $ref: '#/definitions/response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/mengstack_internal_kernel_response.Response' + $ref: '#/definitions/response.Response' security: - Bearer: [] - TenantID: [] @@ -257,7 +1423,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/mengstack_internal_kernel_response.Response' + $ref: '#/definitions/response.Response' security: - Bearer: [] - TenantID: [] @@ -274,19 +1440,19 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/mengstack_internal_kernel_response.Response' + - $ref: '#/definitions/response.Response' - properties: data: - $ref: '#/definitions/mengstack_internal_modules_auth_domain.UserDTO' + $ref: '#/definitions/domain.UserDTO' type: object "401": description: Unauthorized schema: - $ref: '#/definitions/mengstack_internal_kernel_response.Response' + $ref: '#/definitions/response.Response' "404": description: Not Found schema: - $ref: '#/definitions/mengstack_internal_kernel_response.Response' + $ref: '#/definitions/response.Response' security: - Bearer: [] - TenantID: [] diff --git a/internal/app/app.go b/internal/app/app.go index 063756d..c04ccde 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -56,9 +56,12 @@ func newEngine( r := gin.New() r.Use(middleware.RequestID()) - r.Use(middleware.RequestLogger()) + r.Use(middleware.RequestLogger(log)) + r.Use(middleware.SecurityHeaders()) r.Use(middleware.CORS()) - r.Use(gin.Recovery()) + r.Use(middleware.RateLimit(100, time.Minute)) + r.Use(middleware.BodyLimit(10 << 20)) + r.Use(middleware.Recovery(log)) healthHandler := health.NewHandler(db, rdb, log) r.GET("/health", healthHandler.Handle) @@ -97,8 +100,10 @@ func registerLifecycle(lc fx.Lifecycle, srv *http.Server, log *zap.Logger) { return nil }, OnStop: func(ctx context.Context) error { + shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() log.Info("server shutting down") - return srv.Shutdown(ctx) + return srv.Shutdown(shutdownCtx) }, }) } diff --git a/internal/app/database/postgres.go b/internal/app/database/postgres.go index 1c95b1b..3996fbc 100644 --- a/internal/app/database/postgres.go +++ b/internal/app/database/postgres.go @@ -13,7 +13,7 @@ import ( func NewPostgres(cfg config.DatabaseConfig) (*gorm.DB, error) { dsn := fmt.Sprintf( - "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", + "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s TimeZone=Asia/Shanghai", cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName, cfg.SSLMode, ) diff --git a/internal/app/middleware/bodylimit.go b/internal/app/middleware/bodylimit.go new file mode 100644 index 0000000..a7a18c3 --- /dev/null +++ b/internal/app/middleware/bodylimit.go @@ -0,0 +1,23 @@ +package middleware + +import ( + "net/http" + + "mengstack/internal/kernel/errors" + "mengstack/internal/kernel/response" + + "github.com/gin-gonic/gin" +) + +// BodyLimit restricts the maximum request body size. +func BodyLimit(maxBytes int64) gin.HandlerFunc { + return func(c *gin.Context) { + if c.Request.ContentLength > maxBytes { + response.Fail(c, errors.New("BODY_TOO_LARGE", "request body too large", http.StatusRequestEntityTooLarge)) + c.Abort() + return + } + c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxBytes) + c.Next() + } +} diff --git a/internal/app/middleware/middleware_test.go b/internal/app/middleware/middleware_test.go new file mode 100644 index 0000000..82da822 --- /dev/null +++ b/internal/app/middleware/middleware_test.go @@ -0,0 +1,187 @@ +package middleware + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/gin-gonic/gin" + "go.uber.org/zap/zaptest" +) + +func init() { + gin.SetMode(gin.TestMode) +} + +func TestRequestID_GeneratesNew(t *testing.T) { + r := gin.New() + r.Use(RequestID()) + r.GET("/test", func(c *gin.Context) { + c.String(200, c.GetString("trace_id")) + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/test", nil) + r.ServeHTTP(w, req) + + if w.Header().Get("X-Request-ID") == "" { + t.Error("expected X-Request-ID header to be set") + } + if w.Body.String() == "" { + t.Error("expected trace_id in context") + } +} + +func TestRequestID_PreservesExisting(t *testing.T) { + r := gin.New() + r.Use(RequestID()) + r.GET("/test", func(c *gin.Context) { + c.String(200, c.GetString("trace_id")) + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/test", nil) + req.Header.Set("X-Request-ID", "existing-id") + r.ServeHTTP(w, req) + + if w.Header().Get("X-Request-ID") != "existing-id" { + t.Errorf("expected existing-id, got %s", w.Header().Get("X-Request-ID")) + } +} + +func TestSecurityHeaders(t *testing.T) { + r := gin.New() + r.Use(SecurityHeaders()) + r.GET("/test", func(c *gin.Context) { + c.String(200, "ok") + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/test", nil) + r.ServeHTTP(w, req) + + headers := map[string]string{ + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "DENY", + "X-XSS-Protection": "1; mode=block", + "Referrer-Policy": "strict-origin-when-cross-origin", + } + + for key, expected := range headers { + if got := w.Header().Get(key); got != expected { + t.Errorf("header %s: expected %q, got %q", key, expected, got) + } + } +} + +func TestRateLimit_AllowsWithinLimit(t *testing.T) { + r := gin.New() + r.Use(RateLimit(5, time.Minute)) + r.GET("/test", func(c *gin.Context) { + c.String(200, "ok") + }) + + for i := 0; i < 5; i++ { + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/test", nil) + r.ServeHTTP(w, req) + if w.Code != 200 { + t.Errorf("request %d: expected 200, got %d", i+1, w.Code) + } + } +} + +func TestRateLimit_BlocksOverLimit(t *testing.T) { + r := gin.New() + r.Use(RateLimit(2, time.Minute)) + r.GET("/test", func(c *gin.Context) { + c.String(200, "ok") + }) + + for i := 0; i < 2; i++ { + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/test", nil) + r.ServeHTTP(w, req) + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/test", nil) + r.ServeHTTP(w, req) + + if w.Code != http.StatusTooManyRequests { + t.Errorf("expected 429, got %d", w.Code) + } +} + +func TestRequestLogger_DoesNotPanic(t *testing.T) { + log := zaptest.NewLogger(t) + r := gin.New() + r.Use(RequestID()) + r.Use(RequestLogger(log)) + r.GET("/test", func(c *gin.Context) { + c.String(200, "ok") + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/test", nil) + r.ServeHTTP(w, req) + + if w.Code != 200 { + t.Errorf("expected 200, got %d", w.Code) + } +} + +func TestRecovery_HandlesPanic(t *testing.T) { + log := zaptest.NewLogger(t) + r := gin.New() + r.Use(RequestID()) + r.Use(Recovery(log)) + r.GET("/panic", func(c *gin.Context) { + panic("test panic") + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/panic", nil) + r.ServeHTTP(w, req) + + if w.Code != 500 { + t.Errorf("expected 500, got %d", w.Code) + } +} + +func TestBodyLimit_RejectsLargeBody(t *testing.T) { + r := gin.New() + r.Use(BodyLimit(10)) + r.POST("/test", func(c *gin.Context) { + c.String(200, "ok") + }) + + w := httptest.NewRecorder() + body := strings.NewReader(strings.Repeat("x", 100)) + req, _ := http.NewRequest("POST", "/test", body) + req.Header.Set("Content-Length", "100") + r.ServeHTTP(w, req) + + if w.Code != http.StatusRequestEntityTooLarge { + t.Errorf("expected 413, got %d", w.Code) + } +} + +func TestBodyLimit_AllowsSmallBody(t *testing.T) { + r := gin.New() + r.Use(BodyLimit(1024)) + r.POST("/test", func(c *gin.Context) { + c.String(200, "ok") + }) + + w := httptest.NewRecorder() + body := strings.NewReader("hello") + req, _ := http.NewRequest("POST", "/test", body) + r.ServeHTTP(w, req) + + if w.Code != 200 { + t.Errorf("expected 200, got %d", w.Code) + } +} diff --git a/internal/app/middleware/ratelimit.go b/internal/app/middleware/ratelimit.go new file mode 100644 index 0000000..f05e1dd --- /dev/null +++ b/internal/app/middleware/ratelimit.go @@ -0,0 +1,89 @@ +package middleware + +import ( + "net/http" + "sync" + "time" + + "mengstack/internal/kernel/response" + "mengstack/internal/kernel/errors" + + "github.com/gin-gonic/gin" +) + +type visitor struct { + count int + lastSeen time.Time +} + +type rateLimiter struct { + mu sync.Mutex + visitors map[string]*visitor + limit int + window time.Duration +} + +func newRateLimiter(limit int, window time.Duration) *rateLimiter { + rl := &rateLimiter{ + visitors: make(map[string]*visitor), + limit: limit, + window: window, + } + go rl.cleanup() + return rl +} + +func (rl *rateLimiter) cleanup() { + ticker := time.NewTicker(rl.window) + for range ticker.C { + rl.mu.Lock() + now := time.Now() + for key, v := range rl.visitors { + if now.Sub(v.lastSeen) > rl.window { + delete(rl.visitors, key) + } + } + rl.mu.Unlock() + } +} + +func (rl *rateLimiter) allow(key string) bool { + rl.mu.Lock() + defer rl.mu.Unlock() + + v, exists := rl.visitors[key] + if !exists { + rl.visitors[key] = &visitor{count: 1, lastSeen: time.Now()} + return true + } + + if time.Since(v.lastSeen) > rl.window { + v.count = 1 + v.lastSeen = time.Now() + return true + } + + if v.count >= rl.limit { + return false + } + + v.count++ + v.lastSeen = time.Now() + return true +} + +// RateLimit godoc +// Limits requests per IP address. +func RateLimit(limit int, window time.Duration) gin.HandlerFunc { + rl := newRateLimiter(limit, window) + + return func(c *gin.Context) { + ip := c.ClientIP() + if !rl.allow(ip) { + response.Fail(c, errors.New("RATE_LIMITED", "too many requests", http.StatusTooManyRequests)) + c.Abort() + return + } + c.Next() + } +} diff --git a/internal/app/middleware/requestid.go b/internal/app/middleware/requestid.go index ff9396a..c926ad7 100644 --- a/internal/app/middleware/requestid.go +++ b/internal/app/middleware/requestid.go @@ -1,11 +1,11 @@ package middleware import ( - "strconv" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" + "go.uber.org/zap" ) func RequestID() gin.HandlerFunc { @@ -20,19 +20,53 @@ func RequestID() gin.HandlerFunc { } } -func RequestLogger() gin.HandlerFunc { +func RequestLogger(log *zap.Logger) gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() path := c.Request.URL.Path + query := c.Request.URL.RawQuery c.Next() latency := time.Since(start) status := c.Writer.Status() - gin.DefaultWriter.Write([]byte( - "[" + c.GetString("trace_id") + "] " + - c.Request.Method + " " + path + " " + - strconv.Itoa(status) + " " + latency.String() + "\n", - )) + + fields := []zap.Field{ + zap.Int("status", status), + zap.String("method", c.Request.Method), + zap.String("path", path), + zap.String("query", query), + zap.String("ip", c.ClientIP()), + zap.String("user-agent", c.Request.UserAgent()), + zap.Duration("latency", latency), + zap.String("trace_id", c.GetString("trace_id")), + } + + if c.Writer.Status() >= 500 { + log.Error("request completed with server error", fields...) + } else if c.Writer.Status() >= 400 { + log.Warn("request completed with client error", fields...) + } else { + log.Info("request", fields...) + } + } +} + +func Recovery(log *zap.Logger) gin.HandlerFunc { + return func(c *gin.Context) { + defer func() { + if r := recover(); r != nil { + log.Error("panic recovered", + zap.Any("error", r), + zap.String("path", c.Request.URL.Path), + zap.String("trace_id", c.GetString("trace_id")), + ) + c.AbortWithStatusJSON(500, gin.H{ + "error": "INTERNAL_ERROR", + "message": "internal server error", + }) + } + }() + c.Next() } } diff --git a/internal/app/middleware/security.go b/internal/app/middleware/security.go new file mode 100644 index 0000000..018ca50 --- /dev/null +++ b/internal/app/middleware/security.go @@ -0,0 +1,15 @@ +package middleware + +import "github.com/gin-gonic/gin" + +// SecurityHeaders adds common security headers to responses. +func SecurityHeaders() gin.HandlerFunc { + return func(c *gin.Context) { + c.Header("X-Content-Type-Options", "nosniff") + c.Header("X-Frame-Options", "DENY") + c.Header("X-XSS-Protection", "1; mode=block") + c.Header("Referrer-Policy", "strict-origin-when-cross-origin") + c.Header("Content-Security-Policy", "default-src 'self'") + c.Next() + } +} diff --git a/internal/kernel/testutil/testutil.go b/internal/kernel/testutil/testutil.go new file mode 100644 index 0000000..6edfb68 --- /dev/null +++ b/internal/kernel/testutil/testutil.go @@ -0,0 +1,45 @@ +package testutil + +import ( + "mengstack/internal/config" + "testing" + + "go.uber.org/zap" + "go.uber.org/zap/zaptest" +) + +// TestLogger returns a zap logger suitable for use in tests. +func TestLogger(t *testing.T) *zap.Logger { + return zaptest.NewLogger(t) +} + +// TestConfig returns a minimal config for unit tests. +func TestConfig() *config.Config { + return &config.Config{ + Server: config.ServerConfig{ + Port: 0, + Mode: "test", + }, + Database: config.DatabaseConfig{ + Host: "localhost", + Port: 5432, + User: "test", + Password: "test", + DBName: "mengstack_test", + SSLMode: "disable", + }, + Redis: config.RedisConfig{ + Addr: "localhost:6379", + }, + JWT: config.JWTConfig{ + Secret: "test-secret", + AccessExpiryMinutes: 30, + RefreshExpiryDays: 7, + Issuer: "test", + }, + Log: config.LogConfig{ + Level: "debug", + Format: "console", + }, + } +} diff --git a/internal/modules/audit/interfaces/handler.go b/internal/modules/audit/interfaces/handler.go index 82ae353..e6ad239 100644 --- a/internal/modules/audit/interfaces/handler.go +++ b/internal/modules/audit/interfaces/handler.go @@ -19,6 +19,21 @@ func NewHandler(svc *application.Service) *Handler { return &Handler{svc: svc} } +// ListLogs godoc +// @Summary 查询审计日志 +// @Description 分页查询审计日志,支持按用户、操作、资源筛选 +// @Tags Audit +// @Produce json +// @Param user_id query int false "按用户 ID 筛选" +// @Param action query string false "按操作筛选" +// @Param resource query string false "按资源类型筛选" +// @Param page query int false "页码(默认 1)" +// @Param page_size query int false "每页条数(默认 20)" +// @Success 200 {object} response.Response +// @Failure 500 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/audit/logs [get] func (h *Handler) ListLogs(c *gin.Context) { tenantID, _ := tenant.FromContext(c.Request.Context()) diff --git a/internal/modules/notification/interfaces/handler.go b/internal/modules/notification/interfaces/handler.go index 5feeeb4..59ce141 100644 --- a/internal/modules/notification/interfaces/handler.go +++ b/internal/modules/notification/interfaces/handler.go @@ -19,6 +19,18 @@ func NewHandler(svc *application.Service) *Handler { return &Handler{svc: svc} } +// Create godoc +// @Summary 创建通知 +// @Description 向指定用户发送通知 +// @Tags Notification +// @Accept json +// @Produce json +// @Param request body domain.CreateNotificationRequest true "通知内容" +// @Success 200 {object} response.Response{data=domain.NotificationDTO} +// @Failure 400 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/notifications [post] func (h *Handler) Create(c *gin.Context) { var req domain.CreateNotificationRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -34,6 +46,18 @@ func (h *Handler) Create(c *gin.Context) { response.Success(c, n) } +// Get godoc +// @Summary 获取通知详情 +// @Description 根据 ID 获取指定通知的详细信息 +// @Tags Notification +// @Produce json +// @Param id path int true "通知 ID" +// @Success 200 {object} response.Response{data=domain.NotificationDTO} +// @Failure 400 {object} response.Response +// @Failure 404 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/notifications/{id} [get] func (h *Handler) Get(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { @@ -53,6 +77,20 @@ func (h *Handler) Get(c *gin.Context) { response.Success(c, n) } +// ListByUser godoc +// @Summary 获取用户通知列表 +// @Description 分页获取指定用户的通知列表 +// @Tags Notification +// @Produce json +// @Param userId path int true "用户 ID" +// @Param page query int false "页码(默认 1)" +// @Param page_size query int false "每页条数(默认 20)" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.Response +// @Failure 500 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/notifications/user/{userId} [get] func (h *Handler) ListByUser(c *gin.Context) { userID, err := strconv.ParseUint(c.Param("userId"), 10, 64) if err != nil { @@ -74,6 +112,17 @@ func (h *Handler) ListByUser(c *gin.Context) { }) } +// MarkAsRead godoc +// @Summary 标记通知已读 +// @Description 将指定通知标记为已读 +// @Tags Notification +// @Param id path int true "通知 ID" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.Response +// @Failure 404 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/notifications/{id}/read [put] func (h *Handler) MarkAsRead(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { @@ -88,6 +137,16 @@ func (h *Handler) MarkAsRead(c *gin.Context) { response.Success(c, nil) } +// MarkAllAsRead godoc +// @Summary 全部标记已读 +// @Description 将当前用户的所有通知标记为已读 +// @Tags Notification +// @Produce json +// @Success 200 {object} response.Response +// @Failure 500 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/notifications/read-all [put] func (h *Handler) MarkAllAsRead(c *gin.Context) { userID := c.GetUint("user_id") tenantID := c.GetString("tenant_id") @@ -98,6 +157,16 @@ func (h *Handler) MarkAllAsRead(c *gin.Context) { response.Success(c, nil) } +// CountUnread godoc +// @Summary 获取未读通知数 +// @Description 获取当前用户的未读通知数量 +// @Tags Notification +// @Produce json +// @Success 200 {object} response.Response +// @Failure 500 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/notifications/unread-count [get] func (h *Handler) CountUnread(c *gin.Context) { userID := c.GetUint("user_id") tenantID := c.GetString("tenant_id") @@ -109,6 +178,17 @@ func (h *Handler) CountUnread(c *gin.Context) { response.Success(c, gin.H{"count": count}) } +// Delete godoc +// @Summary 删除通知 +// @Description 删除指定通知 +// @Tags Notification +// @Param id path int true "通知 ID" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.Response +// @Failure 404 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/notifications/{id} [delete] func (h *Handler) Delete(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { diff --git a/internal/modules/org/interfaces/handler.go b/internal/modules/org/interfaces/handler.go index ffc8acd..36a49ff 100644 --- a/internal/modules/org/interfaces/handler.go +++ b/internal/modules/org/interfaces/handler.go @@ -16,6 +16,19 @@ func NewHandler(svc *application.Service) *Handler { return &Handler{svc: svc} } +// CreateTenant godoc +// @Summary 创建租户 +// @Description 创建新的租户(组织),需提供名称和 slug +// @Tags Org +// @Accept json +// @Produce json +// @Param request body domain.CreateTenantRequest true "租户信息" +// @Success 200 {object} response.Response{data=domain.TenantDTO} +// @Failure 400 {object} response.Response +// @Failure 409 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/org/tenants [post] func (h *Handler) CreateTenant(c *gin.Context) { var req domain.CreateTenantRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -32,6 +45,16 @@ func (h *Handler) CreateTenant(c *gin.Context) { response.Success(c, dto) } +// ListTenants godoc +// @Summary 列出所有租户 +// @Description 获取系统中的所有租户列表 +// @Tags Org +// @Produce json +// @Success 200 {object} response.Response{data=[]domain.TenantDTO} +// @Failure 500 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/org/tenants [get] func (h *Handler) ListTenants(c *gin.Context) { dtos, err := h.svc.ListTenants(c.Request.Context()) if err != nil { @@ -42,6 +65,18 @@ func (h *Handler) ListTenants(c *gin.Context) { response.Success(c, dtos) } +// GetTenant godoc +// @Summary 获取租户详情 +// @Description 根据 ID 获取指定租户的详细信息 +// @Tags Org +// @Produce json +// @Param id path string true "租户 ID (UUID)" +// @Success 200 {object} response.Response{data=domain.TenantDTO} +// @Failure 400 {object} response.Response +// @Failure 404 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/org/tenants/{id} [get] func (h *Handler) GetTenant(c *gin.Context) { id := c.Param("id") @@ -54,6 +89,20 @@ func (h *Handler) GetTenant(c *gin.Context) { response.Success(c, dto) } +// UpdateTenant godoc +// @Summary 更新租户 +// @Description 更新租户的名称、slug 或状态 +// @Tags Org +// @Accept json +// @Produce json +// @Param id path string true "租户 ID (UUID)" +// @Param request body domain.UpdateTenantRequest true "更新内容" +// @Success 200 {object} response.Response{data=domain.TenantDTO} +// @Failure 400 {object} response.Response +// @Failure 404 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/org/tenants/{id} [put] func (h *Handler) UpdateTenant(c *gin.Context) { id := c.Param("id") @@ -72,6 +121,17 @@ func (h *Handler) UpdateTenant(c *gin.Context) { response.Success(c, dto) } +// DeleteTenant godoc +// @Summary 删除租户 +// @Description 删除指定租户及其所有关联数据 +// @Tags Org +// @Param id path string true "租户 ID (UUID)" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.Response +// @Failure 404 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/org/tenants/{id} [delete] func (h *Handler) DeleteTenant(c *gin.Context) { id := c.Param("id") diff --git a/internal/modules/rbac/interfaces/handler.go b/internal/modules/rbac/interfaces/handler.go index 588eec0..d800888 100644 --- a/internal/modules/rbac/interfaces/handler.go +++ b/internal/modules/rbac/interfaces/handler.go @@ -18,6 +18,17 @@ func NewHandler(svc *application.Service) *Handler { return &Handler{svc: svc} } +// ListPermissions godoc +// @Summary 列出权限 +// @Description 获取所有权限列表,可按模块筛选 +// @Tags RBAC +// @Produce json +// @Param module query string false "按模块筛选" +// @Success 200 {object} response.Response{data=[]domain.PermissionDTO} +// @Failure 500 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/rbac/permissions [get] func (h *Handler) ListPermissions(c *gin.Context) { module := c.Query("module") @@ -40,6 +51,19 @@ func (h *Handler) ListPermissions(c *gin.Context) { response.Success(c, dtos) } +// CreateRole godoc +// @Summary 创建角色 +// @Description 在当前租户下创建新角色 +// @Tags RBAC +// @Accept json +// @Produce json +// @Param request body domain.CreateRoleRequest true "角色信息" +// @Success 200 {object} response.Response{data=domain.RoleDTO} +// @Failure 400 {object} response.Response +// @Failure 409 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/rbac/roles [post] func (h *Handler) CreateRole(c *gin.Context) { var req domain.CreateRoleRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -56,6 +80,16 @@ func (h *Handler) CreateRole(c *gin.Context) { response.Success(c, dto) } +// ListRoles godoc +// @Summary 列出角色 +// @Description 获取当前租户下的所有角色 +// @Tags RBAC +// @Produce json +// @Success 200 {object} response.Response{data=[]domain.RoleDTO} +// @Failure 500 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/rbac/roles [get] func (h *Handler) ListRoles(c *gin.Context) { dtos, err := h.svc.ListRoles(c.Request.Context()) if err != nil { @@ -66,6 +100,18 @@ func (h *Handler) ListRoles(c *gin.Context) { response.Success(c, dtos) } +// GetRole godoc +// @Summary 获取角色详情 +// @Description 根据 ID 获取角色信息,包含权限列表 +// @Tags RBAC +// @Produce json +// @Param id path int true "角色 ID" +// @Success 200 {object} response.Response{data=domain.RoleDTO} +// @Failure 400 {object} response.Response +// @Failure 404 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/rbac/roles/{id} [get] func (h *Handler) GetRole(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { @@ -82,6 +128,20 @@ func (h *Handler) GetRole(c *gin.Context) { response.Success(c, dto) } +// UpdateRole godoc +// @Summary 更新角色 +// @Description 更新角色名称或描述 +// @Tags RBAC +// @Accept json +// @Produce json +// @Param id path int true "角色 ID" +// @Param request body domain.UpdateRoleRequest true "更新内容" +// @Success 200 {object} response.Response{data=domain.RoleDTO} +// @Failure 400 {object} response.Response +// @Failure 404 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/rbac/roles/{id} [put] func (h *Handler) UpdateRole(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { @@ -104,6 +164,17 @@ func (h *Handler) UpdateRole(c *gin.Context) { response.Success(c, dto) } +// DeleteRole godoc +// @Summary 删除角色 +// @Description 删除指定角色(系统角色不可删除) +// @Tags RBAC +// @Param id path int true "角色 ID" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.Response +// @Failure 403 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/rbac/roles/{id} [delete] func (h *Handler) DeleteRole(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { @@ -119,6 +190,20 @@ func (h *Handler) DeleteRole(c *gin.Context) { response.Success(c, nil) } +// SetRolePermissions godoc +// @Summary 设置角色权限 +// @Description 为角色批量设置权限(覆盖原有权限) +// @Tags RBAC +// @Accept json +// @Produce json +// @Param id path int true "角色 ID" +// @Param request body domain.SetRolePermissionsRequest true "权限 ID 列表" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.Response +// @Failure 404 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/rbac/roles/{id}/permissions [put] func (h *Handler) SetRolePermissions(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { @@ -140,6 +225,18 @@ func (h *Handler) SetRolePermissions(c *gin.Context) { response.Success(c, nil) } +// GetRolePermissions godoc +// @Summary 获取角色权限列表 +// @Description 获取指定角色已分配的权限列表 +// @Tags RBAC +// @Produce json +// @Param id path int true "角色 ID" +// @Success 200 {object} response.Response{data=[]domain.PermissionDTO} +// @Failure 400 {object} response.Response +// @Failure 404 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/rbac/roles/{id}/permissions [get] func (h *Handler) GetRolePermissions(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { @@ -156,6 +253,20 @@ func (h *Handler) GetRolePermissions(c *gin.Context) { response.Success(c, dto.Permissions) } +// AssignRole godoc +// @Summary 分配角色给用户 +// @Description 为指定用户分配一个角色 +// @Tags RBAC +// @Accept json +// @Produce json +// @Param userId path int true "用户 ID" +// @Param request body domain.AssignRoleRequest true "角色 ID" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.Response +// @Failure 409 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/rbac/users/{userId}/roles [post] func (h *Handler) AssignRole(c *gin.Context) { userID, err := strconv.ParseUint(c.Param("userId"), 10, 64) if err != nil { @@ -177,6 +288,18 @@ func (h *Handler) AssignRole(c *gin.Context) { response.Success(c, nil) } +// RemoveRole godoc +// @Summary 移除用户角色 +// @Description 从指定用户移除一个角色 +// @Tags RBAC +// @Param userId path int true "用户 ID" +// @Param roleId path int true "角色 ID" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.Response +// @Failure 404 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/rbac/users/{userId}/roles/{roleId} [delete] func (h *Handler) RemoveRole(c *gin.Context) { userID, err := strconv.ParseUint(c.Param("userId"), 10, 64) if err != nil { @@ -198,6 +321,18 @@ func (h *Handler) RemoveRole(c *gin.Context) { response.Success(c, nil) } +// GetUserRoles godoc +// @Summary 获取用户角色 +// @Description 获取指定用户的所有角色及权限 +// @Tags RBAC +// @Produce json +// @Param userId path int true "用户 ID" +// @Success 200 {object} response.Response{data=[]domain.RoleDTO} +// @Failure 400 {object} response.Response +// @Failure 404 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/rbac/users/{userId}/roles [get] func (h *Handler) GetUserRoles(c *gin.Context) { userID, err := strconv.ParseUint(c.Param("userId"), 10, 64) if err != nil { diff --git a/internal/modules/settings/interfaces/handler.go b/internal/modules/settings/interfaces/handler.go index 27e8910..074e5ba 100644 --- a/internal/modules/settings/interfaces/handler.go +++ b/internal/modules/settings/interfaces/handler.go @@ -18,6 +18,18 @@ func NewHandler(svc *application.Service) *Handler { return &Handler{svc: svc} } +// GetGlobalSetting godoc +// @Summary 获取全局配置 +// @Description 根据 key 获取全局(非租户)配置项 +// @Tags Settings +// @Produce json +// @Param key path string true "配置键名" +// @Success 200 {object} response.Response{data=domain.SettingDTO} +// @Failure 400 {object} response.Response +// @Failure 404 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/settings/global/{key} [get] func (h *Handler) GetGlobalSetting(c *gin.Context) { key := c.Param("key") setting, err := h.svc.GetGlobalSetting(c.Request.Context(), key) @@ -32,6 +44,19 @@ func (h *Handler) GetGlobalSetting(c *gin.Context) { response.Success(c, setting) } +// UpdateGlobalSetting godoc +// @Summary 更新全局配置 +// @Description 创建或更新全局配置项 +// @Tags Settings +// @Accept json +// @Produce json +// @Param key path string true "配置键名" +// @Param request body domain.UpdateSettingRequest true "配置值" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/settings/global/{key} [put] func (h *Handler) UpdateGlobalSetting(c *gin.Context) { key := c.Param("key") var req domain.UpdateSettingRequest @@ -47,6 +72,16 @@ func (h *Handler) UpdateGlobalSetting(c *gin.Context) { response.Success(c, nil) } +// ListGlobalSettings godoc +// @Summary 列出全局配置 +// @Description 获取所有全局配置项 +// @Tags Settings +// @Produce json +// @Success 200 {object} response.Response{data=[]domain.SettingDTO} +// @Failure 500 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/settings [get] func (h *Handler) ListGlobalSettings(c *gin.Context) { settings, err := h.svc.ListGlobalSettings(c.Request.Context()) if err != nil { @@ -56,6 +91,18 @@ func (h *Handler) ListGlobalSettings(c *gin.Context) { response.Success(c, settings) } +// GetTenantSetting godoc +// @Summary 获取租户配置 +// @Description 根据 key 获取当前租户的配置项 +// @Tags Settings +// @Produce json +// @Param key path string true "配置键名" +// @Success 200 {object} response.Response{data=domain.SettingDTO} +// @Failure 400 {object} response.Response +// @Failure 404 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/settings/tenant/{key} [get] func (h *Handler) GetTenantSetting(c *gin.Context) { key := c.Param("key") tenantID := c.GetString("tenant_id") @@ -71,6 +118,19 @@ func (h *Handler) GetTenantSetting(c *gin.Context) { response.Success(c, setting) } +// UpdateTenantSetting godoc +// @Summary 更新租户配置 +// @Description 创建或更新当前租户的配置项 +// @Tags Settings +// @Accept json +// @Produce json +// @Param key path string true "配置键名" +// @Param request body domain.UpdateSettingRequest true "配置值" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/settings/tenant/{key} [put] func (h *Handler) UpdateTenantSetting(c *gin.Context) { key := c.Param("key") tenantID := c.GetString("tenant_id") @@ -87,6 +147,16 @@ func (h *Handler) UpdateTenantSetting(c *gin.Context) { response.Success(c, nil) } +// ListTenantSettings godoc +// @Summary 列出租户配置 +// @Description 获取当前租户的所有配置项 +// @Tags Settings +// @Produce json +// @Success 200 {object} response.Response{data=[]domain.SettingDTO} +// @Failure 500 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/settings/tenant [get] func (h *Handler) ListTenantSettings(c *gin.Context) { tenantID := c.GetString("tenant_id") settings, err := h.svc.ListTenantSettings(c.Request.Context(), tenantID) @@ -97,6 +167,17 @@ func (h *Handler) ListTenantSettings(c *gin.Context) { response.Success(c, settings) } +// DeleteTenantSetting godoc +// @Summary 删除租户配置 +// @Description 删除当前租户的指定配置项 +// @Tags Settings +// @Param key path string true "配置键名" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.Response +// @Failure 404 {object} response.Response +// @Security Bearer +// @Security TenantID +// @Router /api/v1/settings/tenant/{key} [delete] func (h *Handler) DeleteTenantSetting(c *gin.Context) { key := c.Param("key") tenantID := c.GetString("tenant_id")