92 lines
5.1 KiB
TypeScript
92 lines
5.1 KiB
TypeScript
import {
|
|
Body, Controller, Delete, Get, Headers, HttpCode, Inject, Param, Patch, Post, Put, Query, Req,
|
|
} from '@nestjs/common';
|
|
import { ApiBearerAuth, ApiCreatedResponse, ApiNoContentResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
|
|
import {
|
|
bindStoreWatermarkRequestSchema, createWatermarkTemplateRequestSchema,
|
|
listWatermarkTemplatesQuerySchema, updateWatermarkTemplateRequestSchema, type Workspace,
|
|
} from '@atlas/contracts';
|
|
import { z } from 'zod';
|
|
import { ApiError } from '../common/api-error.js';
|
|
import type { AuthenticatedRequest } from '../auth/auth.guard.js';
|
|
import { CurrentWorkspace } from '../workspace/current-workspace.decorator.js';
|
|
import { WatermarksService } from './watermarks.service.js';
|
|
|
|
@ApiTags('Watermarks')
|
|
@ApiBearerAuth('access-token')
|
|
@Controller()
|
|
export class WatermarksController {
|
|
public constructor(@Inject(WatermarksService) private readonly watermarks: WatermarksService) {}
|
|
|
|
@Get('watermark-fonts')
|
|
@ApiOperation({ summary: '读取当前渲染服务可用字体' })
|
|
public async fonts() { return { data: await this.watermarks.fonts() }; }
|
|
|
|
@Get('watermark-templates')
|
|
@ApiOperation({ summary: '分页读取当前工作空间活动水印模板' })
|
|
public async list(@Query() query: unknown, @CurrentWorkspace() workspace: Workspace) {
|
|
return this.watermarks.list(workspace.id, parse(listWatermarkTemplatesQuerySchema, query));
|
|
}
|
|
|
|
@Get('watermark-templates/:templateId')
|
|
@ApiOperation({ summary: '读取水印模板当前版本' })
|
|
public async get(@Param('templateId') templateId: string, @CurrentWorkspace() workspace: Workspace) {
|
|
return { data: await this.watermarks.get(workspace.id, uuid('template_id', templateId)) };
|
|
}
|
|
|
|
@Post('watermark-templates')
|
|
@ApiCreatedResponse({ description: '已创建水印模板及首个渲染版本' })
|
|
public async create(@Body() body: unknown, @Headers('idempotency-key') key: string | undefined,
|
|
@Req() request: AuthenticatedRequest, @CurrentWorkspace() workspace: Workspace) {
|
|
return (await this.watermarks.create(workspace.id, request.auth.userId, idempotencyKey(key),
|
|
parse(createWatermarkTemplateRequestSchema, body))).body;
|
|
}
|
|
|
|
@Patch('watermark-templates/:templateId')
|
|
@ApiOkResponse({ description: '已更新水印模板' })
|
|
public async update(@Param('templateId') templateId: string, @Body() body: unknown,
|
|
@Headers('idempotency-key') key: string | undefined, @Headers('if-match') ifMatch: string | undefined,
|
|
@Req() request: AuthenticatedRequest, @CurrentWorkspace() workspace: Workspace) {
|
|
return (await this.watermarks.update(workspace.id, request.auth.userId, uuid('template_id', templateId),
|
|
idempotencyKey(key), revision(ifMatch), parse(updateWatermarkTemplateRequestSchema, body))).body;
|
|
}
|
|
|
|
@Delete('watermark-templates/:templateId')
|
|
@HttpCode(204)
|
|
@ApiNoContentResponse({ description: '已软删除模板并解除当前店铺绑定' })
|
|
public async remove(@Param('templateId') templateId: string, @Headers('idempotency-key') key: string | undefined,
|
|
@Req() request: AuthenticatedRequest, @CurrentWorkspace() workspace: Workspace) {
|
|
await this.watermarks.remove(workspace.id, request.auth.userId, uuid('template_id', templateId), idempotencyKey(key));
|
|
}
|
|
|
|
@Put('stores/:storeId/watermark-binding')
|
|
@ApiOkResponse({ description: '已更新店铺推荐水印' })
|
|
public async bindStore(@Param('storeId') storeId: string, @Body() body: unknown,
|
|
@Headers('idempotency-key') key: string | undefined, @Req() request: AuthenticatedRequest,
|
|
@CurrentWorkspace() workspace: Workspace) {
|
|
const input = parse(bindStoreWatermarkRequestSchema, body) as { template_id: string | null };
|
|
return (await this.watermarks.bindStore(workspace.id, request.auth.userId, uuid('store_id', storeId),
|
|
idempotencyKey(key), input.template_id)).body;
|
|
}
|
|
}
|
|
|
|
function parse<T extends { safeParse: (input: unknown) => { success: boolean; data?: unknown; error?: { issues: Array<{ path: PropertyKey[]; message: string }> } } }>(schema: T, input: unknown) {
|
|
const result = schema.safeParse(input);
|
|
if (result.success) return result.data as ReturnType<T['safeParse']> extends { data: infer Data } ? Data : never;
|
|
throw new ApiError(422, 'VALIDATION_FAILED', '请求参数不符合要求',
|
|
result.error?.issues.map((issue) => ({ field: issue.path.join('.'), code: 'INVALID_FIELD', message: issue.message })));
|
|
}
|
|
function uuid(field: string, value: string) {
|
|
if (z.uuid().safeParse(value).success) return value;
|
|
throw new ApiError(422, 'VALIDATION_FAILED', '请求参数不符合要求', [{ field, code: 'INVALID_FIELD', message: `${field} 必须是 UUID` }]);
|
|
}
|
|
function idempotencyKey(value: string | undefined) {
|
|
if (!value) throw new ApiError(400, 'IDEMPOTENCY_KEY_REQUIRED', '请求缺少 Idempotency-Key');
|
|
return uuid('Idempotency-Key', value);
|
|
}
|
|
function revision(value: string | undefined) {
|
|
const match = value?.match(/^"([1-9]\d*)"$/);
|
|
if (!match) throw new ApiError(400, 'IF_MATCH_REQUIRED', '请求缺少或格式错误的 If-Match');
|
|
return Number(match[1]);
|
|
}
|