Files
atlas-seller-kit/apps/api/src/watermarks/watermarks.controller.ts
T

107 lines
5.9 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/default')
@ApiOperation({ summary: '读取当前工作空间的默认水印模板' })
public async getDefault(@CurrentWorkspace() workspace: Workspace) {
return { data: await this.watermarks.getDefault(workspace.id) };
}
@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;
}
@Put('watermark-templates/:templateId/default')
@ApiOkResponse({ description: '已设为当前工作空间的默认水印' })
public async setDefault(@Param('templateId') templateId: string,
@Headers('idempotency-key') key: string | undefined,
@Req() request: AuthenticatedRequest, @CurrentWorkspace() workspace: Workspace) {
return (await this.watermarks.setDefault(workspace.id, request.auth.userId,
uuid('template_id', templateId), idempotencyKey(key))).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]);
}