import { randomBytes } from 'node:crypto'; import { BadRequestException, Injectable, NotFoundException, } from '@nestjs/common'; import { maskWecomPluginApiKey, parseWecomPluginPermissions, type CreateWecomApiPluginRequest, type UpdateWecomApiPluginRequest, type WecomApiPluginDto, type WecomApiPluginSecretDto, } from '@dukang/shared-types'; import { Prisma } from '@prisma/client'; import { PrismaService } from '../../common/prisma/prisma.module'; import { serializeBigInt } from '../../common/decorators/current-user.decorator'; const MIN_KEY_LEN = 16; @Injectable() export class AdminWecomApiPluginsService { constructor(private readonly prisma: PrismaService) {} async list(query: { name?: string; enabled?: string; page?: number; pageSize?: number }) { const page = query.page ?? 1; const pageSize = query.pageSize ?? 20; const where: { name?: { contains: string }; enabled?: boolean } = {}; if (query.name?.trim()) where.name = { contains: query.name.trim() }; if (query.enabled === 'true' || query.enabled === 'false') { where.enabled = query.enabled === 'true'; } const [items, total] = await Promise.all([ this.prisma.wecomApiPlugin.findMany({ where, orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }], skip: (page - 1) * pageSize, take: pageSize, }), this.prisma.wecomApiPlugin.count({ where }), ]); return serializeBigInt({ items: items.map((row) => this.toDto(row)), total, page, pageSize, }); } async detail(id: bigint): Promise { return this.toDto(await this.mustGet(id)); } async create(dto: CreateWecomApiPluginRequest): Promise { const name = dto.name?.trim(); if (!name) throw new BadRequestException('请填写名称'); const permissions = parseWecomPluginPermissions(dto.permissions); if (!permissions.length) throw new BadRequestException('请至少勾选一项工具权限'); const apiKey = this.normalizeApiKey(dto.apiKey, true); try { const row = await this.prisma.wecomApiPlugin.create({ data: { name, apiKey, permissions: JSON.stringify(permissions), remark: dto.remark?.trim() || null, enabled: dto.enabled !== false, sortOrder: dto.sortOrder ?? 0, }, }); return { ...this.toDto(row), apiKey }; } catch (e) { this.rethrowUniqueKey(e); throw e; } } async update(id: bigint, dto: UpdateWecomApiPluginRequest): Promise { const existing = await this.mustGet(id); const data: Prisma.WecomApiPluginUpdateInput = {}; if (dto.name !== undefined) { const name = dto.name.trim(); if (!name) throw new BadRequestException('请填写名称'); data.name = name; } if (dto.permissions !== undefined) { const permissions = parseWecomPluginPermissions(dto.permissions); if (!permissions.length) throw new BadRequestException('请至少勾选一项工具权限'); data.permissions = JSON.stringify(permissions); } if (dto.remark !== undefined) data.remark = dto.remark?.trim() || null; if (dto.enabled !== undefined) data.enabled = dto.enabled; if (dto.sortOrder !== undefined) data.sortOrder = dto.sortOrder; if (dto.apiKey !== undefined && String(dto.apiKey).trim()) { data.apiKey = this.normalizeApiKey(dto.apiKey, false); } try { const row = await this.prisma.wecomApiPlugin.update({ where: { id: existing.id }, data, }); return this.toDto(row); } catch (e) { this.rethrowUniqueKey(e); throw e; } } async remove(id: bigint) { await this.mustGet(id); await this.prisma.wecomApiPlugin.delete({ where: { id } }); return { ok: true }; } async rotateKey(id: bigint): Promise { await this.mustGet(id); const apiKey = generateApiKey(); try { const row = await this.prisma.wecomApiPlugin.update({ where: { id }, data: { apiKey }, }); return { ...this.toDto(row), apiKey }; } catch (e) { this.rethrowUniqueKey(e); throw e; } } private async mustGet(id: bigint) { const row = await this.prisma.wecomApiPlugin.findUnique({ where: { id } }); if (!row) throw new NotFoundException('API 插件不存在'); return row; } private normalizeApiKey(raw: string | undefined, generateIfEmpty: boolean): string { const v = String(raw ?? '').trim(); if (!v) { if (generateIfEmpty) return generateApiKey(); throw new BadRequestException('请填写 API Key'); } if (v.length < MIN_KEY_LEN) { throw new BadRequestException(`API Key 至少 ${MIN_KEY_LEN} 位`); } if (v.length > 128) throw new BadRequestException('API Key 过长'); return v; } private rethrowUniqueKey(e: unknown): void { if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') { throw new BadRequestException('API Key 已存在,请更换'); } } private toDto(row: { id: bigint; name: string; apiKey: string; permissions: string; remark: string | null; enabled: boolean; sortOrder: number; createdAt: Date; updatedAt: Date; }): WecomApiPluginDto { return { id: row.id.toString(), name: row.name, apiKeyConfigured: Boolean(row.apiKey), apiKeyMasked: maskWecomPluginApiKey(row.apiKey), permissions: parseWecomPluginPermissions(row.permissions), remark: row.remark, enabled: row.enabled, sortOrder: row.sortOrder, createdAt: row.createdAt.toISOString(), updatedAt: row.updatedAt.toISOString(), }; } } function generateApiKey(): string { return `dkp_${randomBytes(24).toString('hex')}`; }