import { Router } from 'express'; import { z } from 'zod'; import type { PlatformBrowseHistoryBatchSyncRequest, PlatformBrowseHistoryResponse, PlatformBrowseHistoryWriteEntry, ProfileDashboardSummary, ProfilePlayStatsResponse, ProfileWalletLedgerResponse, RuntimeSettings, } from '../../../../packages/shared/src/contracts/runtime.js'; import { PLATFORM_THEMES } from '../../../../packages/shared/src/contracts/runtime.js'; import type { AppContext } from '../../context.js'; import { asyncHandler, sendApiResponse } from '../../http.js'; import { requireJwtAuth } from '../../middleware/auth.js'; import { routeMeta } from '../../middleware/routeMeta.js'; const platformBrowseHistoryEntrySchema = z.object({ ownerUserId: z.string().trim().min(1), profileId: z.string().trim().min(1), worldName: z.string().trim().min(1), subtitle: z.string().trim().optional().default(''), summaryText: z.string().trim().optional().default(''), coverImageSrc: z.string().trim().nullable().optional().default(null), themeMode: z.string().trim().optional().default('mythic'), authorDisplayName: z.string().trim().optional().default('玩家'), visitedAt: z.string().trim().optional().default(''), }); const platformBrowseHistoryBatchSchema = z.object({ entries: z.array(platformBrowseHistoryEntrySchema).max(100), }); const settingsSchema = z.object({ musicVolume: z.number().min(0).max(1), platformTheme: z.enum(PLATFORM_THEMES), }); export const RPG_PROFILE_ROUTE_BASE_PATH = '/api/runtime/profile'; export const RPG_PROFILE_LEGACY_ROUTE_BASE_PATH = '/api/profile'; function routeCompatPaths(path: string) { return [path, path.replace('runtime/', '')] as const; } export function createRpgProfileRoutes(context: AppContext) { const router = Router(); const requireAuth = requireJwtAuth(context.config, context.userRepository); routeCompatPaths('/api/runtime/profile/dashboard').forEach((path, index) => { router.get( path.replace('/api/', '/'), requireAuth, routeMeta({ operation: index === 0 ? 'profile.dashboard.get' : 'profile.dashboard.get.compat', }), asyncHandler(async (request, response) => { sendApiResponse( response, await context.rpgProfileDashboardRepository.getProfileDashboard( request.userId!, ), ); }), ); }); routeCompatPaths('/api/runtime/profile/wallet-ledger').forEach((path, index) => { router.get( path.replace('/api/', '/'), requireAuth, routeMeta({ operation: index === 0 ? 'profile.walletLedger.list' : 'profile.walletLedger.list.compat', }), asyncHandler(async (request, response) => { sendApiResponse(response, { entries: await context.rpgProfileDashboardRepository.listProfileWalletLedger( request.userId!, ), }); }), ); }); routeCompatPaths('/api/runtime/profile/play-stats').forEach((path, index) => { router.get( path.replace('/api/', '/'), requireAuth, routeMeta({ operation: index === 0 ? 'profile.playStats.get' : 'profile.playStats.get.compat', }), asyncHandler(async (request, response) => { sendApiResponse( response, await context.rpgProfileDashboardRepository.getProfilePlayStats( request.userId!, ), ); }), ); }); routeCompatPaths('/api/runtime/profile/browse-history').forEach((path, index) => { router.get( path.replace('/api/', '/'), requireAuth, routeMeta({ operation: index === 0 ? 'profile.browseHistory.list' : 'profile.browseHistory.list.compat', }), asyncHandler(async (request, response) => { sendApiResponse(response, { entries: await context.rpgBrowseHistoryRepository.listPlatformBrowseHistory( request.userId!, ), }); }), ); router.post( path.replace('/api/', '/'), requireAuth, routeMeta({ operation: index === 0 ? 'profile.browseHistory.upsert' : 'profile.browseHistory.upsert.compat', }), asyncHandler(async (request, response) => { const rawBody = request.body && typeof request.body === 'object' ? request.body : {}; const payload = ( 'entries' in rawBody ? platformBrowseHistoryBatchSchema.parse(rawBody) : platformBrowseHistoryEntrySchema.parse(rawBody) ) as | PlatformBrowseHistoryBatchSyncRequest | PlatformBrowseHistoryWriteEntry; const entries = 'entries' in payload ? payload.entries : [payload]; sendApiResponse(response, { entries: await context.rpgBrowseHistoryRepository.upsertPlatformBrowseHistoryEntries( request.userId!, entries, ), }); }), ); router.delete( path.replace('/api/', '/'), requireAuth, routeMeta({ operation: index === 0 ? 'profile.browseHistory.clear' : 'profile.browseHistory.clear.compat', }), asyncHandler(async (request, response) => { await context.rpgBrowseHistoryRepository.clearPlatformBrowseHistory( request.userId!, ); sendApiResponse(response, { entries: [], }); }), ); }); router.get( '/api/runtime/settings'.replace('/api/', '/'), requireAuth, routeMeta({ operation: 'runtime.settings.get' }), asyncHandler(async (request, response) => { sendApiResponse( response, await context.rpgProfileDashboardRepository.getSettings(request.userId!), ); }), ); router.put( '/api/runtime/settings'.replace('/api/', '/'), requireAuth, routeMeta({ operation: 'runtime.settings.put' }), asyncHandler(async (request, response) => { const payload = settingsSchema.parse(request.body) as RuntimeSettings; sendApiResponse( response, await context.rpgProfileDashboardRepository.putSettings( request.userId!, payload, ), ); }), ); return router; }