feat: add account transfers and refine icons, settings and safety dialogs
This commit is contained in:
1 parent
a4e9d56b8a
commit
da98d02661
33 files changed
+1866
-347
No files matched your search
@@ -0,0 +1,118 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
credentials,
|
||||
positionInput,
|
||||
positionMeta,
|
||||
revisionInput,
|
||||
transferInput,
|
||||
currency,
|
||||
hiddenMenus,
|
||||
} from './validation';
|
||||
export function setupOpenApi(app: INestApplication) {
|
||||
const document = SwaggerModule.createDocument(
|
||||
app,
|
||||
new DocumentBuilder()
|
||||
.setTitle('WorthPath API')
|
||||
.setVersion('1.1')
|
||||
.setDescription(
|
||||
'个人资产管理接口。金额使用十进制字符串;登录会话由 HttpOnly Cookie 传递,数据归属由服务端验证。',
|
||||
)
|
||||
.addCookieAuth('wp_session', { type: 'apiKey', in: 'cookie' }, 'session')
|
||||
.addSecurityRequirements('session')
|
||||
.build(),
|
||||
);
|
||||
const bodies: Record<string, z.ZodType> = {
|
||||
'POST /api/auth/register': credentials,
|
||||
'POST /api/auth/login': credentials,
|
||||
'POST /api/auth/reveal': credentials.pick({ password: true }),
|
||||
'POST /api/positions': positionInput,
|
||||
'PATCH /api/positions/{id}': positionMeta,
|
||||
'POST /api/positions/{id}/revisions': revisionInput,
|
||||
'PUT /api/positions/{id}/revisions/{revisionId}': revisionInput,
|
||||
'PUT /api/positions/{id}/links': z
|
||||
.object({ targetIds: z.array(z.string().uuid()).max(20) })
|
||||
.strict(),
|
||||
'POST /api/transfers': transferInput,
|
||||
'PATCH /api/settings': z
|
||||
.object({
|
||||
baseCurrency: currency.optional(),
|
||||
hiddenMenus: hiddenMenus.optional(),
|
||||
showNotes: z.boolean().optional(),
|
||||
idleMinutes: z.number().int().min(0).max(1440).optional(),
|
||||
})
|
||||
.strict(),
|
||||
'POST /api/backup/clear': z.object({ confirmation: z.literal('确定清空') }),
|
||||
'POST /api/backup/import-file': z.object({
|
||||
token: z.string().uuid(),
|
||||
confirmed: z.literal(true),
|
||||
}),
|
||||
};
|
||||
for (const [path, entry] of Object.entries(document.paths)) {
|
||||
for (const method of ['get', 'post', 'patch', 'put'] as const) {
|
||||
const operation = entry[method];
|
||||
if (!operation) continue;
|
||||
operation.summary = `${method.toUpperCase()} ${path}`;
|
||||
if (path === '/api/health' || ['/api/auth/register', '/api/auth/login'].includes(path))
|
||||
operation.security = [];
|
||||
const schema = bodies[method.toUpperCase() + ' ' + path];
|
||||
if (schema)
|
||||
operation.requestBody = {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: z.toJSONSchema(schema, { target: 'openapi-3.0' }) as any,
|
||||
},
|
||||
},
|
||||
};
|
||||
operation.responses['400'] = {
|
||||
description: '请求格式、金额或业务校验失败,返回 { message }',
|
||||
};
|
||||
operation.responses['401'] = { description: '未登录或会话失效' };
|
||||
operation.responses['403'] = { description: '请求来源或访问权限不受信任' };
|
||||
operation.responses['409'] = { description: '数据冲突或业务记录不可修改' };
|
||||
if (path === '/api/backup' && method === 'get')
|
||||
operation.responses['200'] = {
|
||||
description: '该用户全部数据的 ZIP 文件',
|
||||
content: { 'application/zip': { schema: { type: 'string', format: 'binary' } } },
|
||||
};
|
||||
if (path === '/api/backup/upload' || path === '/api/icons/upload') {
|
||||
const properties = {
|
||||
file: { type: 'string', format: 'binary' },
|
||||
...(path.includes('icons')
|
||||
? {
|
||||
name: { type: 'string', description: '共享图标必须包含中文' },
|
||||
shared: { type: 'string', enum: ['false', 'true'], default: 'false' },
|
||||
confirmed: { type: 'string', enum: ['true'], description: '共享发布必须确认' },
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
operation.requestBody = {
|
||||
required: true,
|
||||
content: {
|
||||
'multipart/form-data': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: path.includes('icons') ? ['file', 'name'] : ['file'],
|
||||
properties: properties as any,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
if (['/api/backup/preview', '/api/backup/import'].includes(path))
|
||||
operation.requestBody = {
|
||||
required: true,
|
||||
description: '旧版 JSON 兼容入口;新版请使用 upload + import-file(支持完整 ZIP 备份)',
|
||||
content: {
|
||||
'application/json': { schema: { type: 'object', additionalProperties: true } },
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
SwaggerModule.setup('api/docs', app, document, {
|
||||
jsonDocumentUrl: 'api/openapi.json',
|
||||
swaggerOptions: { persistAuthorization: false, validatorUrl: null, withCredentials: true },
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user