fix: align legacy general access call boundaries
This commit is contained in:
@@ -71,6 +71,7 @@ export type DatabaseClient = InfraDatabaseClient;
|
||||
|
||||
export interface GameApiContext {
|
||||
requestId?: string;
|
||||
generalAccessTracking?: boolean;
|
||||
db: DatabaseClient;
|
||||
redis: RedisConnector['client'];
|
||||
turnDaemon: TurnDaemonTransport;
|
||||
@@ -102,6 +103,7 @@ export const createGameApiContext = (options: {
|
||||
}): GameApiContext => {
|
||||
return {
|
||||
requestId: options.requestId,
|
||||
generalAccessTracking: true,
|
||||
db: options.db,
|
||||
redis: options.redis,
|
||||
turnDaemon: options.turnDaemon,
|
||||
|
||||
@@ -4,7 +4,13 @@ import { z } from 'zod';
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
import { getDexLevel } from '@sammo-ts/logic';
|
||||
|
||||
import { authedProcedure, readOnlyAuthedProcedure, router } from '../../trpc.js';
|
||||
import {
|
||||
accessAuthedInputProcedure,
|
||||
accessReadOnlyAuthedInputProcedure,
|
||||
authedProcedure,
|
||||
readOnlyAuthedProcedure,
|
||||
router,
|
||||
} from '../../trpc.js';
|
||||
import { buildBattleSimEnvironment, buildBattleSimJobPayload } from '../../battleSim/environment.js';
|
||||
import { zBattleSimJobId, zBattleSimRequest } from '../../battleSim/schema.js';
|
||||
import {
|
||||
@@ -56,7 +62,7 @@ const resolveDexValue = (meta: Record<string, unknown>, key: string): number =>
|
||||
};
|
||||
|
||||
export const battleRouter = router({
|
||||
simulate: readOnlyAuthedProcedure.input(zBattleSimRequest).mutation(async ({ ctx, input }) => {
|
||||
simulate: accessReadOnlyAuthedInputProcedure(zBattleSimRequest).mutation(async ({ ctx, input }) => {
|
||||
const worldState = await ctx.db.worldState.findFirst();
|
||||
if (!worldState) {
|
||||
throw new TRPCError({
|
||||
@@ -169,8 +175,7 @@ export const battleRouter = router({
|
||||
generalsByNation,
|
||||
};
|
||||
}),
|
||||
getGeneralDetail: authedProcedure
|
||||
.input(
|
||||
getGeneralDetail: accessAuthedInputProcedure(
|
||||
z.object({
|
||||
generalId: z.number().int().positive(),
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import { TRPCError } from '@trpc/server';
|
||||
import { GamePrisma } from '@sammo-ts/infra';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { authedProcedure, router } from '../../trpc.js';
|
||||
import { accessAuthedInputProcedure, authedProcedure, router } from '../../trpc.js';
|
||||
import { appendInheritanceLog, readInheritancePoint, setInheritancePoint } from '../../services/inheritance.js';
|
||||
import { getMyGeneral } from '../shared/general.js';
|
||||
|
||||
@@ -30,8 +30,7 @@ const loadWorldDate = async (db: Parameters<typeof getMyGeneral>[0]['db']) => {
|
||||
};
|
||||
|
||||
export const bettingRouter = router({
|
||||
getList: authedProcedure
|
||||
.input(z.object({ req: z.literal('bettingNation').optional() }).optional())
|
||||
getList: accessAuthedInputProcedure(z.object({ req: z.literal('bettingNation').optional() }).optional())
|
||||
.query(async ({ ctx, input }) => {
|
||||
requireUserId(ctx.auth);
|
||||
await getMyGeneral(ctx);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { promises as fs } from 'fs';
|
||||
import { randomUUID } from 'crypto';
|
||||
import sharp, { type WebpOptions } from 'sharp';
|
||||
|
||||
import { authedProcedure, router } from '../../trpc.js';
|
||||
import { accessAuthedInputProcedure, authedProcedure, router } from '../../trpc.js';
|
||||
import { getMyGeneral } from '../shared/general.js';
|
||||
import { resolveSecretPermission } from '../shared/secretPermission.js';
|
||||
|
||||
@@ -107,7 +107,7 @@ export const boardRouter = router({
|
||||
canSecret: permission >= 2,
|
||||
};
|
||||
}),
|
||||
getArticles: authedProcedure.input(z.object({ isSecret: z.boolean() })).query(async ({ ctx, input }) => {
|
||||
getArticles: accessAuthedInputProcedure(z.object({ isSecret: z.boolean() })).query(async ({ ctx, input }) => {
|
||||
const { general, permission } = await getBoardActor(ctx);
|
||||
assertBoardAccess(permission, input.isSecret);
|
||||
|
||||
@@ -159,8 +159,7 @@ export const boardRouter = router({
|
||||
})),
|
||||
}));
|
||||
}),
|
||||
writeArticle: authedProcedure
|
||||
.input(
|
||||
writeArticle: accessAuthedInputProcedure(
|
||||
z.object({
|
||||
isSecret: z.boolean(),
|
||||
title: z.string().trim().max(250),
|
||||
@@ -189,8 +188,7 @@ export const boardRouter = router({
|
||||
|
||||
return { id: post.id };
|
||||
}),
|
||||
writeComment: authedProcedure
|
||||
.input(
|
||||
writeComment: accessAuthedInputProcedure(
|
||||
z.object({
|
||||
postId: z.number().int().positive(),
|
||||
content: z.string().trim().max(2000),
|
||||
|
||||
@@ -4,7 +4,7 @@ import { z } from 'zod';
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
import type { GamePrisma } from '@sammo-ts/infra';
|
||||
|
||||
import { authedProcedure, router } from '../../trpc.js';
|
||||
import { accessAuthedInputProcedure, accessAuthedProcedure, router } from '../../trpc.js';
|
||||
import { getMyGeneral } from '../shared/general.js';
|
||||
import { assertNationAccess, resolveNationPermission } from '../nation/shared.js';
|
||||
|
||||
@@ -28,7 +28,7 @@ const mapLetterState = (state: string): 'PROPOSED' | 'ACTIVATED' | 'CANCELLED' |
|
||||
};
|
||||
|
||||
export const diplomacyRouter = router({
|
||||
getLetters: authedProcedure.query(async ({ ctx }) => {
|
||||
getLetters: accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
const me = await getMyGeneral(ctx);
|
||||
assertNationAccess(me);
|
||||
|
||||
@@ -98,8 +98,7 @@ export const diplomacyRouter = router({
|
||||
permission,
|
||||
};
|
||||
}),
|
||||
sendLetter: authedProcedure
|
||||
.input(
|
||||
sendLetter: accessAuthedInputProcedure(
|
||||
z.object({
|
||||
destNationId: z.number().int().positive(),
|
||||
prevId: z.number().int().positive().nullable().optional(),
|
||||
@@ -217,8 +216,7 @@ export const diplomacyRouter = router({
|
||||
|
||||
return { id: created.id };
|
||||
}),
|
||||
respondLetter: authedProcedure
|
||||
.input(
|
||||
respondLetter: accessAuthedInputProcedure(
|
||||
z.object({
|
||||
letterId: z.number().int().positive(),
|
||||
agree: z.boolean(),
|
||||
@@ -288,8 +286,7 @@ export const diplomacyRouter = router({
|
||||
|
||||
return { ok: true };
|
||||
}),
|
||||
rollbackLetter: authedProcedure
|
||||
.input(z.object({ letterId: z.number().int().positive() }))
|
||||
rollbackLetter: accessAuthedInputProcedure(z.object({ letterId: z.number().int().positive() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const me = await getMyGeneral(ctx);
|
||||
assertNationAccess(me);
|
||||
@@ -324,8 +321,7 @@ export const diplomacyRouter = router({
|
||||
|
||||
return { ok: true };
|
||||
}),
|
||||
destroyLetter: authedProcedure
|
||||
.input(z.object({ letterId: z.number().int().positive() }))
|
||||
destroyLetter: accessAuthedInputProcedure(z.object({ letterId: z.number().int().positive() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const me = await getMyGeneral(ctx);
|
||||
assertNationAccess(me);
|
||||
|
||||
@@ -5,7 +5,14 @@ import { LogCategory, LogScope } from '@sammo-ts/infra';
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
|
||||
import type { GameApiContext } from '../../context.js';
|
||||
import { authedProcedure, engineAuthedProcedure, router } from '../../trpc.js';
|
||||
import {
|
||||
accessAuthedProcedure,
|
||||
accessAuthedInputProcedure,
|
||||
accessEngineAuthedProcedure,
|
||||
accessEngineAuthedInputProcedure,
|
||||
authedProcedure,
|
||||
router,
|
||||
} from '../../trpc.js';
|
||||
import { ConflictingTurnDaemonCommandError } from '../../daemon/databaseTransport.js';
|
||||
import { resolveAccessWindows } from '../../services/generalAccess.js';
|
||||
import { getMyGeneral } from '../shared/general.js';
|
||||
@@ -283,7 +290,7 @@ export const generalRouter = router({
|
||||
penalties,
|
||||
};
|
||||
}),
|
||||
ensureDieOnPrestartStatus: engineAuthedProcedure.mutation(async ({ ctx }) => {
|
||||
ensureDieOnPrestartStatus: accessEngineAuthedProcedure.mutation(async ({ ctx }) => {
|
||||
const userId = ctx.auth?.user.id;
|
||||
if (!userId) {
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED' });
|
||||
@@ -319,14 +326,11 @@ export const generalRouter = router({
|
||||
availableAt: result.availableAt ?? null,
|
||||
};
|
||||
}),
|
||||
dieOnPrestart: engineAuthedProcedure
|
||||
.input(zImmediateActionInput)
|
||||
dieOnPrestart: accessEngineAuthedInputProcedure(zImmediateActionInput)
|
||||
.mutation(({ ctx, input }) => requestImmediateAction(ctx, input, 'dieOnPrestart')),
|
||||
buildNationCandidate: engineAuthedProcedure
|
||||
.input(zImmediateActionInput)
|
||||
buildNationCandidate: accessEngineAuthedInputProcedure(zImmediateActionInput)
|
||||
.mutation(({ ctx, input }) => requestImmediateAction(ctx, input, 'buildNationCandidate')),
|
||||
instantRetreat: engineAuthedProcedure
|
||||
.input(zImmediateActionInput)
|
||||
instantRetreat: accessEngineAuthedInputProcedure(zImmediateActionInput)
|
||||
.mutation(({ ctx, input }) => requestImmediateAction(ctx, input, 'instantRetreat')),
|
||||
vacation: authedProcedure.mutation(async ({ ctx }) => {
|
||||
const general = await getMyGeneral(ctx);
|
||||
@@ -342,7 +346,7 @@ export const generalRouter = router({
|
||||
}
|
||||
return { ok: true };
|
||||
}),
|
||||
setMySetting: authedProcedure.input(zGeneralSettings).mutation(async ({ ctx, input }) => {
|
||||
setMySetting: accessAuthedInputProcedure(zGeneralSettings).mutation(async ({ ctx, input }) => {
|
||||
const general = await getMyGeneral(ctx);
|
||||
const result = await ctx.turnDaemon.requestCommand({
|
||||
type: 'setMySetting',
|
||||
@@ -459,7 +463,7 @@ export const generalRouter = router({
|
||||
history: trimRecentRecords(history, input.lastWorldHistoryId),
|
||||
};
|
||||
}),
|
||||
getFrontStatus: authedProcedure.query(async ({ ctx }) => {
|
||||
getFrontStatus: accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
const me = await getMyGeneral(ctx);
|
||||
const worldState = await ctx.db.worldState.findFirst({
|
||||
orderBy: { id: 'asc' },
|
||||
|
||||
@@ -4,7 +4,7 @@ import { asRecord } from '@sammo-ts/common';
|
||||
import type { UserSanctions } from '@sammo-ts/common/auth/gameToken';
|
||||
import { isMessageAccessBlocked } from '@sammo-ts/common/auth/sanctions';
|
||||
|
||||
import { authedProcedure, router } from '../../trpc.js';
|
||||
import { accessAuthedInputProcedure, authedProcedure, router } from '../../trpc.js';
|
||||
import {
|
||||
MESSAGE_MAILBOX_NATIONAL_BASE,
|
||||
MESSAGE_MAILBOX_PUBLIC,
|
||||
@@ -388,8 +388,7 @@ export const messagesRouter = router({
|
||||
...messageBuckets,
|
||||
};
|
||||
}),
|
||||
send: authedProcedure
|
||||
.input(
|
||||
send: accessAuthedInputProcedure(
|
||||
z.object({
|
||||
generalId: z.number().int().positive(),
|
||||
mailbox: z.number().int(),
|
||||
|
||||
@@ -2,11 +2,11 @@ import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { LogCategory } from '@sammo-ts/infra';
|
||||
|
||||
import { authedProcedure } from '../../../trpc.js';
|
||||
import { accessAuthedProcedure } from '../../../trpc.js';
|
||||
import { getMyGeneral } from '../../shared/general.js';
|
||||
import { assertNationAccess, formatDateTime, resolveNationPermission } from '../shared.js';
|
||||
|
||||
export const getBattleCenter = authedProcedure.query(async ({ ctx }) => {
|
||||
export const getBattleCenter = accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
const me = await getMyGeneral(ctx);
|
||||
assertNationAccess(me);
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { authedProcedure } from '../../../trpc.js';
|
||||
import { accessAuthedProcedure } from '../../../trpc.js';
|
||||
import { getMyGeneral } from '../../shared/general.js';
|
||||
import { resolveSecretPermission } from '../../shared/secretPermission.js';
|
||||
import { MAX_NATION_TURNS, getNationTurnSnapshot } from '../../../turns/reservedTurns.js';
|
||||
import { assertNationAccess } from '../shared.js';
|
||||
|
||||
export const getChiefCenter = authedProcedure.query(async ({ ctx }) => {
|
||||
export const getChiefCenter = accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
const me = await getMyGeneral(ctx);
|
||||
assertNationAccess(me);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { authedProcedure } from '../../../trpc.js';
|
||||
import { accessAuthedProcedure } from '../../../trpc.js';
|
||||
import { getMyGeneral } from '../../shared/general.js';
|
||||
import {
|
||||
assertNationAccess,
|
||||
@@ -18,7 +18,7 @@ const experienceLevel = (experience: number): number =>
|
||||
const dedicationLevel = (dedication: number): number =>
|
||||
Math.max(0, Math.min(10, Math.ceil(Math.sqrt(dedication) / 10)));
|
||||
|
||||
export const getGeneralList = authedProcedure.query(async ({ ctx }) => {
|
||||
export const getGeneralList = accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
const general = await getMyGeneral(ctx);
|
||||
assertNationAccess(general);
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
|
||||
import { authedProcedure } from '../../../trpc.js';
|
||||
import { accessAuthedProcedure } from '../../../trpc.js';
|
||||
import { getMyGeneral } from '../../shared/general.js';
|
||||
import { assertNationAccess, checkSecretMaxPermission, mapGeneralList, resolveChiefStatMin } from '../shared.js';
|
||||
|
||||
export const getPersonnelInfo = authedProcedure.query(async ({ ctx }) => {
|
||||
export const getPersonnelInfo = accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
const me = await getMyGeneral(ctx);
|
||||
assertNationAccess(me);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
|
||||
import { authedProcedure } from '../../../trpc.js';
|
||||
import { accessAuthedProcedure } from '../../../trpc.js';
|
||||
import { getMyGeneral } from '../../shared/general.js';
|
||||
import { assertNationAccess, resolveNationPermission } from '../shared.js';
|
||||
|
||||
@@ -25,7 +25,7 @@ const leadershipBonus = (officerLevel: number, nationLevel: number): number =>
|
||||
const defenceTrainText = (value: number): string =>
|
||||
value === 999 ? '×' : value >= 90 ? '☆' : value >= 80 ? '◎' : value >= 60 ? '○' : '△';
|
||||
|
||||
export const getSecretGeneralList = authedProcedure.query(async ({ ctx }) => {
|
||||
export const getSecretGeneralList = accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
const me = await getMyGeneral(ctx);
|
||||
assertNationAccess(me);
|
||||
const nation = await ctx.db.nation.findUnique({
|
||||
|
||||
@@ -3,7 +3,7 @@ import { TRPCError } from '@trpc/server';
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
import { getGoldIncome, getOutcome, getRiceIncome, getWallIncome, getWarGoldIncome, type NationIncomeContext } from '@sammo-ts/logic';
|
||||
|
||||
import { authedProcedure } from '../../../trpc.js';
|
||||
import { accessAuthedProcedure } from '../../../trpc.js';
|
||||
import { getMyGeneral } from '../../shared/general.js';
|
||||
import {
|
||||
assertNationAccess,
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
type NationStratRow,
|
||||
} from '../shared.js';
|
||||
|
||||
export const getStratFinan = authedProcedure.query(async ({ ctx }) => {
|
||||
export const getStratFinan = accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
const me = await getMyGeneral(ctx);
|
||||
assertNationAccess(me);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { z } from 'zod';
|
||||
import { asRecord, isRecord } from '@sammo-ts/common';
|
||||
import { findCrewTypeById, getTechCost } from '@sammo-ts/logic/world/unitSet.js';
|
||||
|
||||
import { authedProcedure, router } from '../../trpc.js';
|
||||
import { accessAuthedInputProcedure, authedProcedure, router } from '../../trpc.js';
|
||||
import { loadUnitSetDefinitionByName } from '../../battleSim/unitSetLoader.js';
|
||||
import type { GameApiContext } from '../../context.js';
|
||||
import { getMyGeneral } from '../shared/general.js';
|
||||
@@ -537,7 +537,7 @@ export const npcRouter = router({
|
||||
permissionLevel,
|
||||
};
|
||||
}),
|
||||
setNationPolicy: authedProcedure.input(z.record(z.string(), z.unknown())).mutation(async ({ ctx, input }) => {
|
||||
setNationPolicy: accessAuthedInputProcedure(z.record(z.string(), z.unknown())).mutation(async ({ ctx, input }) => {
|
||||
const general = await getMyGeneral(ctx);
|
||||
if (general.nationId <= 0) {
|
||||
throw new TRPCError({ code: 'PRECONDITION_FAILED', message: 'Nation membership required.' });
|
||||
@@ -700,7 +700,7 @@ export const npcRouter = router({
|
||||
|
||||
return { ok: true };
|
||||
}),
|
||||
setNationPriority: authedProcedure.input(z.array(z.string())).mutation(async ({ ctx, input }) => {
|
||||
setNationPriority: accessAuthedInputProcedure(z.array(z.string())).mutation(async ({ ctx, input }) => {
|
||||
const general = await getMyGeneral(ctx);
|
||||
if (general.nationId <= 0) {
|
||||
throw new TRPCError({ code: 'PRECONDITION_FAILED', message: 'Nation membership required.' });
|
||||
@@ -753,7 +753,7 @@ export const npcRouter = router({
|
||||
|
||||
return { ok: true };
|
||||
}),
|
||||
setGeneralPriority: authedProcedure.input(z.array(z.string())).mutation(async ({ ctx, input }) => {
|
||||
setGeneralPriority: accessAuthedInputProcedure(z.array(z.string())).mutation(async ({ ctx, input }) => {
|
||||
const general = await getMyGeneral(ctx);
|
||||
if (general.nationId <= 0) {
|
||||
throw new TRPCError({ code: 'PRECONDITION_FAILED', message: 'Nation membership required.' });
|
||||
|
||||
@@ -8,7 +8,7 @@ import { zWorldStateConfig, zWorldStateMeta } from '../../context.js';
|
||||
import { loadMapLayout } from '../../maps/mapLayout.js';
|
||||
import { loadPublicMap } from '../../maps/worldMap.js';
|
||||
import { accessPages, recordGeneralAccess } from '../../services/generalAccess.js';
|
||||
import { procedure, router, sessionActivityProcedure } from '../../trpc.js';
|
||||
import { accessInputProcedure, procedure, router, sessionActivityProcedure } from '../../trpc.js';
|
||||
import { loadTraitNames } from '../nation/shared.js';
|
||||
|
||||
type WorldTrendSnapshot = {
|
||||
@@ -477,8 +477,7 @@ export const publicRouter = router({
|
||||
intelligence: general.intel,
|
||||
}));
|
||||
}),
|
||||
getNpcList: procedure
|
||||
.input(
|
||||
getNpcList: accessInputProcedure(
|
||||
z
|
||||
.object({
|
||||
sort: z.number().int().min(1).max(8).catch(1).optional(),
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { ItemModule } from '@sammo-ts/logic/items/types.js';
|
||||
import { buildLegacyDefaultUniqueItemPool } from '@sammo-ts/logic/rewards/legacyUniqueItemPool.js';
|
||||
import { resolveUniqueConfig } from '@sammo-ts/logic/rewards/uniqueLottery.js';
|
||||
|
||||
import { authedProcedure, procedure, router } from '../../trpc.js';
|
||||
import { accessAuthedInputProcedure, accessInputProcedure, procedure, router } from '../../trpc.js';
|
||||
|
||||
const DEFAULT_BG_COLOR = '#330000';
|
||||
const DEFAULT_FG_COLOR = '#ffffff';
|
||||
@@ -81,8 +81,7 @@ const loadUniqueItems = () => {
|
||||
};
|
||||
|
||||
export const rankingRouter = router({
|
||||
getBestGeneral: authedProcedure
|
||||
.input(
|
||||
getBestGeneral: accessAuthedInputProcedure(
|
||||
z
|
||||
.object({
|
||||
view: z.enum(['user', 'npc']).optional(),
|
||||
@@ -369,8 +368,7 @@ export const rankingRouter = router({
|
||||
}
|
||||
return Array.from(seasonMap.values());
|
||||
}),
|
||||
getHallOfFame: procedure
|
||||
.input(
|
||||
getHallOfFame: accessInputProcedure(
|
||||
z.object({
|
||||
season: z.number().int(),
|
||||
scenario: z.number().int().optional(),
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { TournamentState } from '../../tournament/types.js';
|
||||
|
||||
import { TournamentStore } from '../../tournament/store.js';
|
||||
import { buildTournamentKeys } from '../../tournament/keys.js';
|
||||
import { authedProcedure, router } from '../../trpc.js';
|
||||
import { accessAuthedProcedure, authedProcedure, router } from '../../trpc.js';
|
||||
import { getMyGeneral } from '../shared/general.js';
|
||||
|
||||
const hasAdminRole = (roles: string[], profileName: string): boolean => {
|
||||
@@ -127,7 +127,7 @@ export const tournamentRouter = router({
|
||||
return store.getState();
|
||||
}),
|
||||
getAdminStatus: adminProcedure.query(async () => ({ ok: true })),
|
||||
getSnapshot: authedProcedure.query(async ({ ctx }) => {
|
||||
getSnapshot: accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
await getMyGeneral(ctx);
|
||||
const store = new TournamentStore(ctx.redis, buildTournamentKeys(ctx.profile.name));
|
||||
const [state, participants, matches, bets] = await Promise.all([
|
||||
|
||||
@@ -4,7 +4,7 @@ import { z } from 'zod';
|
||||
import type { TurnDaemonCommandResult } from '@sammo-ts/common';
|
||||
import { isValidTroopNameWidth, normalizeTroopName, resolveTroopSecretPermission } from '@sammo-ts/logic';
|
||||
|
||||
import { authedProcedure, router } from '../../trpc.js';
|
||||
import { accessAuthedProcedure, authedProcedure, router } from '../../trpc.js';
|
||||
import { getMyGeneral } from '../shared/general.js';
|
||||
|
||||
const troopNameSchema = z
|
||||
@@ -33,7 +33,7 @@ const assertCommandResult = <T extends 'troopCreate' | 'troopJoin' | 'troopExit'
|
||||
};
|
||||
|
||||
export const troopRouter = router({
|
||||
getList: authedProcedure.query(async ({ ctx }) => {
|
||||
getList: accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
const me = await getMyGeneral(ctx);
|
||||
if (me.nationId <= 0) {
|
||||
throw new TRPCError({ code: 'PRECONDITION_FAILED', message: '국가에 소속되어 있지 않습니다.' });
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { authedProcedure } from '../../trpc.js';
|
||||
import { accessAuthedInputProcedure, authedProcedure } from '../../trpc.js';
|
||||
import { loadTraitNames } from '../nation/shared.js';
|
||||
import { getMyGeneral } from '../shared/general.js';
|
||||
import { resolveSecretPermission } from '../shared/secretPermission.js';
|
||||
@@ -204,8 +204,7 @@ export const getNationDirectory = authedProcedure.query(async ({ ctx }) => {
|
||||
});
|
||||
});
|
||||
|
||||
export const getGeneralDirectory = authedProcedure
|
||||
.input(z.object({ sort: zDirectorySort }).optional())
|
||||
export const getGeneralDirectory = accessAuthedInputProcedure(z.object({ sort: zDirectorySort }).optional())
|
||||
.query(async ({ ctx, input }) => {
|
||||
await getMyGeneral(ctx);
|
||||
const sort = input?.sort ?? 9;
|
||||
|
||||
@@ -2,8 +2,7 @@ import { TRPCError } from '@trpc/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type WorldStateRow, zWorldStateConfig, zWorldStateMeta } from '../../context.js';
|
||||
import { procedure, router } from '../../trpc.js';
|
||||
import { authedProcedure } from '../../trpc.js';
|
||||
import { accessAuthedProcedure, authedProcedure, procedure, router } from '../../trpc.js';
|
||||
import { asRecord, isRecord } from '@sammo-ts/common';
|
||||
import { loadWorldMap } from '../../maps/worldMap.js';
|
||||
import { loadMapLayout } from '../../maps/mapLayout.js';
|
||||
@@ -71,7 +70,7 @@ const toWorldStateSnapshot = (row: WorldStateRow) => ({
|
||||
export const worldRouter = router({
|
||||
getNationDirectory,
|
||||
getGeneralDirectory,
|
||||
getGlobalInfo: authedProcedure.query(async ({ ctx }) => {
|
||||
getGlobalInfo: accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
const me = await getMyGeneral(ctx);
|
||||
const [nations, cities, diplomacy, map] = await Promise.all([
|
||||
ctx.db.nation.findMany({ where: { level: { gt: 0 } } }),
|
||||
|
||||
@@ -7,6 +7,10 @@ import { LogCategory, LogScope } from '@sammo-ts/infra';
|
||||
|
||||
import type { GameApiContext } from '../../context.js';
|
||||
import { loadPublicMap, type BaseMapResult } from '../../maps/worldMap.js';
|
||||
import {
|
||||
generalAccessEndpointWeights,
|
||||
recordGeneralAccessWeight,
|
||||
} from '../../services/generalAccess.js';
|
||||
import { authedProcedure, router } from '../../trpc.js';
|
||||
import { getMyGeneral } from '../shared/general.js';
|
||||
|
||||
@@ -24,6 +28,13 @@ const zServerId = z.string().trim().min(1).max(64);
|
||||
|
||||
const computeHash = (payload: unknown): string => createHash('sha256').update(JSON.stringify(payload)).digest('hex');
|
||||
|
||||
const recordHistoryAccess = async (ctx: GameApiContext): Promise<void> => {
|
||||
if (ctx.generalAccessTracking !== true) {
|
||||
return;
|
||||
}
|
||||
await recordGeneralAccessWeight(ctx, generalAccessEndpointWeights['yearbook.getHistory']);
|
||||
};
|
||||
|
||||
const parseTextArray = (value: unknown): string[] =>
|
||||
Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [];
|
||||
|
||||
@@ -257,6 +268,10 @@ export const yearbookRouter = router({
|
||||
}
|
||||
const targetProfileName = input.serverID ?? ctx.profile.name;
|
||||
const isCurrentProfile = targetProfileName === ctx.profile.name;
|
||||
const shouldRecordAfterHashCheck = isCurrentProfile && Boolean(input.hash);
|
||||
if (isCurrentProfile && !shouldRecordAfterHashCheck) {
|
||||
await recordHistoryAccess(ctx);
|
||||
}
|
||||
|
||||
const isCurrent =
|
||||
isCurrentProfile && worldState.currentYear === input.year && worldState.currentMonth === input.month;
|
||||
@@ -280,6 +295,9 @@ export const yearbookRouter = router({
|
||||
if (input.hash && input.hash === hash) {
|
||||
return { notModified: true, hash };
|
||||
}
|
||||
if (shouldRecordAfterHashCheck) {
|
||||
await recordHistoryAccess(ctx);
|
||||
}
|
||||
return { notModified: false, hash, data };
|
||||
}
|
||||
|
||||
@@ -317,6 +335,9 @@ export const yearbookRouter = router({
|
||||
if (input.hash && input.hash === hash) {
|
||||
return { notModified: true, hash };
|
||||
}
|
||||
if (shouldRecordAfterHashCheck) {
|
||||
await recordHistoryAccess(ctx);
|
||||
}
|
||||
return { notModified: false, hash, data };
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -4,59 +4,86 @@ import { GamePrisma } from '@sammo-ts/infra';
|
||||
import type { GameApiContext } from '../context.js';
|
||||
|
||||
export const accessPages = [
|
||||
'front-info',
|
||||
'nation-info',
|
||||
'nation-cities',
|
||||
'global-info',
|
||||
'nation-list',
|
||||
'general-list',
|
||||
'current-city',
|
||||
'diplomacy',
|
||||
'nation-generals',
|
||||
'nation-personnel',
|
||||
'nation-finance',
|
||||
'battle-center',
|
||||
'board',
|
||||
'best-general',
|
||||
'hall-of-fame',
|
||||
'dynasty',
|
||||
'yearbook',
|
||||
'nation-betting',
|
||||
'traffic',
|
||||
'npc-list',
|
||||
'my-page',
|
||||
'npc-control',
|
||||
'tournament',
|
||||
'betting',
|
||||
] as const;
|
||||
|
||||
export type AccessPage = (typeof accessPages)[number];
|
||||
|
||||
export const accessPageWeights: Record<AccessPage, number> = {
|
||||
'front-info': 1,
|
||||
'nation-info': 1,
|
||||
'nation-cities': 1,
|
||||
'global-info': 1,
|
||||
'nation-list': 2,
|
||||
'general-list': 2,
|
||||
'current-city': 1,
|
||||
diplomacy: 1,
|
||||
'nation-generals': 1,
|
||||
'nation-personnel': 1,
|
||||
'nation-finance': 1,
|
||||
'battle-center': 1,
|
||||
board: 1,
|
||||
'best-general': 1,
|
||||
'hall-of-fame': 1,
|
||||
dynasty: 1,
|
||||
yearbook: 1,
|
||||
'nation-betting': 1,
|
||||
traffic: 1,
|
||||
'npc-list': 2,
|
||||
'my-page': 1,
|
||||
'npc-control': 1,
|
||||
tournament: 1,
|
||||
betting: 1,
|
||||
};
|
||||
|
||||
export const generalAccessEndpointWeights = {
|
||||
'world.getGeneralDirectory': 2,
|
||||
'public.getNpcList': 2,
|
||||
'ranking.getBestGeneral': 1,
|
||||
'ranking.getHallOfFame': 1,
|
||||
'tournament.getSnapshot': 1,
|
||||
'nation.getSecretGeneralList': 1,
|
||||
'nation.getPersonnelInfo': 1,
|
||||
'nation.getGeneralList': 1,
|
||||
'general.ensureDieOnPrestartStatus': 1,
|
||||
'nation.getStratFinan': 1,
|
||||
'board.getArticles': 1,
|
||||
'diplomacy.getLetters': 2,
|
||||
'battle.getGeneralDetail': 1,
|
||||
'betting.getList': 1,
|
||||
'general.getFrontStatus': 1,
|
||||
'yearbook.getHistory': 1,
|
||||
'world.getGlobalInfo': 1,
|
||||
'nation.getBattleCenter': 1,
|
||||
'nation.getChiefCenter': 1,
|
||||
'troop.getList': 1,
|
||||
'board.writeArticle': 1,
|
||||
'board.writeComment': 1,
|
||||
'diplomacy.sendLetter': 1,
|
||||
'diplomacy.respondLetter': 1,
|
||||
'diplomacy.rollbackLetter': 1,
|
||||
'diplomacy.destroyLetter': 1,
|
||||
'general.buildNationCandidate': 1,
|
||||
'general.dieOnPrestart': 1,
|
||||
'general.instantRetreat': 1,
|
||||
'messages.send': 1,
|
||||
'general.setMySetting': 0,
|
||||
'npc.setNationPolicy': 0,
|
||||
'npc.setNationPriority': 0,
|
||||
'npc.setGeneralPriority': 0,
|
||||
'battle.simulate': 0,
|
||||
} as const satisfies Record<string, 0 | 1 | 2>;
|
||||
|
||||
export type GeneralAccessEndpoint = keyof typeof generalAccessEndpointWeights;
|
||||
|
||||
export const resolveGeneralAccessEndpointWeight = (
|
||||
path: string,
|
||||
input: unknown,
|
||||
currentProfileName: string
|
||||
): 0 | 1 | 2 | null | undefined => {
|
||||
const weight = generalAccessEndpointWeights[path as GeneralAccessEndpoint];
|
||||
if (weight === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (path === 'yearbook.getHistory') {
|
||||
const requestedServer =
|
||||
typeof input === 'object' && input !== null && 'serverID' in input
|
||||
? (input as { serverID?: unknown }).serverID
|
||||
: undefined;
|
||||
if (requestedServer !== undefined && requestedServer !== currentProfileName) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return weight;
|
||||
};
|
||||
|
||||
const adminRoles = new Set(['superuser', 'admin', 'admin.superuser']);
|
||||
@@ -262,7 +289,16 @@ export const recordGeneralAccess = async (
|
||||
ctx: Pick<GameApiContext, 'auth' | 'db'>,
|
||||
page: AccessPage,
|
||||
now = new Date()
|
||||
): Promise<boolean> => recordGeneralAccessWeight(ctx, accessPageWeights[page], now);
|
||||
|
||||
export const recordGeneralAccessWeight = async (
|
||||
ctx: Pick<GameApiContext, 'auth' | 'db'>,
|
||||
weight: number,
|
||||
now = new Date()
|
||||
): Promise<boolean> => {
|
||||
if (!Number.isInteger(weight) || weight < 0) {
|
||||
throw new RangeError('General access weight must be a non-negative integer.');
|
||||
}
|
||||
const user = ctx.auth?.user;
|
||||
if (!user || user.roles.some((role) => adminRoles.has(role))) {
|
||||
return false;
|
||||
@@ -296,7 +332,6 @@ export const recordGeneralAccess = async (
|
||||
return false;
|
||||
}
|
||||
|
||||
const weight = accessPageWeights[page];
|
||||
const { periodStartedAt, scoreStartedAt } = resolveAccessWindows(now, worldState.tickSeconds, meta);
|
||||
|
||||
await upsertGeneralAccess(ctx.db, {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { isGameAccessBlocked } from '@sammo-ts/common/auth/sanctions';
|
||||
import type { GameApiContext } from './context.js';
|
||||
import { IdempotentTurnDaemonTransport } from './daemon/idempotentTransport.js';
|
||||
import { DuplicateInputEventError, executeInputEvent } from './inputEventBoundary.js';
|
||||
import { recordGeneralAccessWeight, resolveGeneralAccessEndpointWeight } from './services/generalAccess.js';
|
||||
|
||||
const t = initTRPC.context<GameApiContext>().create();
|
||||
|
||||
@@ -67,14 +68,48 @@ const inputEventMiddleware = t.middleware(async ({ ctx, type, path, next }) => {
|
||||
}
|
||||
});
|
||||
|
||||
const generalAccessEndpointMiddleware = t.middleware(async ({ ctx, path, input, next }) => {
|
||||
// 실제 HTTP context는 createGameApiContext()가 이 flag를 설정한다.
|
||||
// Router 단위 테스트의 부분 DB mock은 명시적으로 opt-in할 때만 계측한다.
|
||||
if (ctx.generalAccessTracking !== true) {
|
||||
return next();
|
||||
}
|
||||
const weight = resolveGeneralAccessEndpointWeight(path, input, ctx.profile.name);
|
||||
if (weight === undefined) {
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: `General access endpoint is not registered: ${path}`,
|
||||
});
|
||||
}
|
||||
if (weight === null) {
|
||||
return next();
|
||||
}
|
||||
await recordGeneralAccessWeight(ctx, weight);
|
||||
return next();
|
||||
});
|
||||
|
||||
export const router = t.router;
|
||||
export const procedure = t.procedure.use(inputEventMiddleware);
|
||||
export const authedProcedure: typeof procedure = procedure.use(requireAuthMiddleware);
|
||||
|
||||
// Ref의 increaseRefresh()는 로그인/제재 확인 뒤, 업무 validation과 mutation
|
||||
// transaction보다 먼저 별도 저장된다. access middleware를 input-event보다
|
||||
// 앞에 두어 실패하거나 재시도되는 업무 transaction과 접속 기록을 분리한다.
|
||||
export const accessProcedure: typeof procedure = t.procedure
|
||||
.use(generalAccessEndpointMiddleware)
|
||||
.use(inputEventMiddleware);
|
||||
export const accessAuthedProcedure: typeof procedure = t.procedure
|
||||
.use(requireAuthMiddleware)
|
||||
.use(generalAccessEndpointMiddleware)
|
||||
.use(inputEventMiddleware);
|
||||
|
||||
// 턴 데몬이 ENGINE input_event와 world/DB 변경을 자체 transaction으로
|
||||
// 커밋하는 mutation에 사용한다. API input-event transaction으로 한 번 더
|
||||
// 감싸면 daemon이 아직 commit되지 않은 command를 볼 수 없어 교착된다.
|
||||
export const engineAuthedProcedure: typeof procedure = t.procedure.use(requireAuthMiddleware);
|
||||
export const accessEngineAuthedProcedure: typeof procedure = t.procedure
|
||||
.use(requireAuthMiddleware)
|
||||
.use(generalAccessEndpointMiddleware);
|
||||
|
||||
// 페이지 조회 계측처럼 game state/input-event 원장과 무관한 세션 보조
|
||||
// mutation에 사용한다. gameplay state 변경에는 사용하지 않는다.
|
||||
@@ -83,3 +118,17 @@ export const sessionActivityProcedure = t.procedure;
|
||||
// 시뮬레이터처럼 게임 상태를 변경하지 않는 계산은 input-event transaction과
|
||||
// 이벤트 원장을 만들지 않는다. 인증은 유지하되 lifecycle DB 경계 밖에서 실행한다.
|
||||
export const readOnlyAuthedProcedure: typeof procedure = t.procedure.use(requireAuthMiddleware);
|
||||
export const accessReadOnlyAuthedProcedure: typeof procedure = t.procedure
|
||||
.use(requireAuthMiddleware)
|
||||
.use(generalAccessEndpointMiddleware);
|
||||
|
||||
// 입력이 있는 Ref handler는 request parsing을 마친 뒤 increaseRefresh()를
|
||||
// 호출한다. 이 factory들은 parser를 access/input-event middleware 앞에 둔다.
|
||||
export const accessInputProcedure: typeof procedure.input = (input) =>
|
||||
t.procedure.input(input).use(generalAccessEndpointMiddleware).use(inputEventMiddleware);
|
||||
export const accessAuthedInputProcedure: typeof procedure.input = (input) =>
|
||||
t.procedure.use(requireAuthMiddleware).input(input).use(generalAccessEndpointMiddleware).use(inputEventMiddleware);
|
||||
export const accessEngineAuthedInputProcedure: typeof procedure.input = (input) =>
|
||||
t.procedure.use(requireAuthMiddleware).input(input).use(generalAccessEndpointMiddleware);
|
||||
export const accessReadOnlyAuthedInputProcedure: typeof procedure.input = (input) =>
|
||||
t.procedure.use(requireAuthMiddleware).input(input).use(generalAccessEndpointMiddleware);
|
||||
|
||||
Reference in New Issue
Block a user