Files
Genarrative/server-node/src/routes/rpg-profile/rpgProfileRoutes.ts
2026-04-21 18:27:46 +08:00

215 lines
6.4 KiB
TypeScript

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<ProfileDashboardSummary>(
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<ProfileWalletLedgerResponse>(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<ProfilePlayStatsResponse>(
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<PlatformBrowseHistoryResponse>(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<PlatformBrowseHistoryResponse>(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<PlatformBrowseHistoryResponse>(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;
}