import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'; import { z } from 'zod'; import type { AuthRepository } from '../auth/auth-repository.js'; import { authenticateAccessToken } from '../auth/authenticate.js'; import type { AccessProfile } from '../auth/rbac-repository.js'; import type { ManagementActor } from '../auth/user-management-repository.js'; import { PlatformAdminError, type PlatformAdminRepository } from '../tenancy/platform-admin-repository.js'; const id = z.string().regex(/^[1-9]\d{0,19}$/); const configSchema = z.object({ brandName: z.string().trim().min(1).max(128), logoUrl: z.string().trim().max(512).default(''), themeColor: z.string().regex(/^#[0-9A-Fa-f]{6}$/), servicePhone: z.string().trim().max(32).default(''), franchisePhone: z.string().trim().max(32).default(''), shareTitle: z.string().trim().max(128).default(''), shareImageUrl: z.string().trim().max(512).default(''), defaultStoreId: id.nullable().default(null), extraConfig: z.record(z.unknown()).default({}), bindingStatus: z.enum(['ACTIVE', 'DISABLED']), isDefault: z.boolean() }); const bindSchema = z.object({ appId: z.string().regex(/^[A-Za-z0-9_-]{6,64}$/), appName: z.string().trim().min(1).max(128), appStatus: z.enum(['ACTIVE', 'DISABLED']), config: configSchema }); export interface PlatformManagementRouteOptions { repository: Pick; authRepository: Pick; accessControl: { getAccessProfile(tenantId: string, userId: string): Promise }; jwtSecret: string; } export async function registerPlatformManagementRoutes(app: FastifyInstance, options: PlatformManagementRouteOptions) { app.get('/admin-api/platform-apps', async (request, reply) => { const actor = await requireActor(request, reply, options, false); if (!actor) return; return handle(reply, request.traceId, async () => ({ code: 0, data: await options.repository.listTenantApps(actor.tenantId), traceId: request.traceId })); }); app.put('/admin-api/platform-apps/:id/config', async (request, reply) => { const actor = await requireActor(request, reply, options, false); const params = z.object({ id }).safeParse(request.params); const body = configSchema.safeParse(request.body); if (!actor) return; if (!params.success || !body.success) return invalid(reply, request.traceId); return handle(reply, request.traceId, async () => ({ code: 0, data: await options.repository.updateTenantConfig(actor, params.data.id, body.data), traceId: request.traceId })); }); app.post('/admin-api/platform-apps/bind', async (request, reply) => { const actor = await requireActor(request, reply, options, true); const body = bindSchema.safeParse(request.body); if (!actor) return; if (!body.success) return invalid(reply, request.traceId); return handle(reply, request.traceId, async () => reply.status(201).send({ code: 0, data: await options.repository.bindApplication(actor, body.data), traceId: request.traceId })); }); } async function requireActor(request: FastifyRequest, reply: FastifyReply, options: PlatformManagementRouteOptions, platformOnly: boolean): Promise { const auth = await authenticateAccessToken(request.headers.authorization, options.authRepository, options.jwtSecret); if (!auth) { reply.status(401).send({ code: 'AUTH_SESSION_INVALID', message: 'Authentication required.', traceId: request.traceId }); return null; } const access = await options.accessControl.getAccessProfile(auth.session.tenantId, auth.session.user.id); const platform = access.roles.includes('PLATFORM_ADMIN') || access.capabilities.includes('platform.manage'); if ((platformOnly && !platform) || (!platformOnly && !platform && !access.capabilities.includes('tenant.manage'))) { reply.status(403).send({ code: platformOnly ? 'PLATFORM_MANAGEMENT_FORBIDDEN' : 'TENANT_MANAGEMENT_FORBIDDEN', message: 'Management permission is required.', traceId: request.traceId }); return null; } return { tenantId: auth.session.tenantId, userId: auth.session.user.id, access, traceId: request.traceId, ip: request.ip, userAgent: request.headers['user-agent'] ?? '' }; } async function handle(reply: FastifyReply, traceId: string, work: () => Promise) { try { return await work(); } catch (error) { if (!(error instanceof PlatformAdminError)) throw error; const missing = error.code.endsWith('_NOT_FOUND'); return reply.status(missing ? 404 : 400).send({ code: error.code, message: 'The platform application operation is invalid.', traceId }); } } function invalid(reply: FastifyReply, traceId: string) { return reply.status(400).send({ code: 'INVALID_PLATFORM_APP_REQUEST', message: 'The platform application request is invalid.', traceId }); }