merge: 최신 main을 정적 프런트엔드 작업에 통합한다
This commit is contained in:
@@ -21,6 +21,7 @@ import { openPassword, zDisplayName, zPasswordEnvelope, zRegistrationUsername }
|
||||
import { resolveEffectiveAccountIcon } from './auth/accountIconProjection.js';
|
||||
import { purifyGatewayNoticeHtml } from './security/gatewayNoticeHtml.js';
|
||||
import type { GatewayApiContext } from './context.js';
|
||||
import { listScenarioPreviews } from './scenario/scenarioCatalog.js';
|
||||
import {
|
||||
KakaoVerificationError,
|
||||
mergeRequiredKakaoScopes,
|
||||
@@ -193,6 +194,42 @@ export const appRouter = router({
|
||||
})
|
||||
);
|
||||
}),
|
||||
scenarios: procedure
|
||||
.input(z.object({ profileName: z.string().min(1).max(64) }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const provided = ctx.requestHeaders['x-session-token'];
|
||||
const sessionToken = Array.isArray(provided) ? provided[0] : provided;
|
||||
if (!sessionToken) {
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Session token is required.' });
|
||||
}
|
||||
const session = await ctx.sessions.getSession(sessionToken);
|
||||
const user = session ? await ctx.users.findById(session.userId) : null;
|
||||
if (!session || !user) {
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Session is not valid.' });
|
||||
}
|
||||
|
||||
const visibleProfiles = await ctx.profileStatus.listLobbyProfiles({ userId: user.id });
|
||||
if (!visibleProfiles.some((profile) => profile.profileName === input.profileName)) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Profile not found.' });
|
||||
}
|
||||
const profile = await ctx.profiles.getProfile(input.profileName);
|
||||
const activeBuildCommit = profile?.buildCommitSha?.trim();
|
||||
if (!profile || !activeBuildCommit) {
|
||||
throw new TRPCError({
|
||||
code: 'PRECONDITION_FAILED',
|
||||
message: 'The profile has no active build commit.',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
return await listScenarioPreviews({ gitRef: activeBuildCommit });
|
||||
} catch {
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'The active build scenario catalog could not be read.',
|
||||
});
|
||||
}
|
||||
}),
|
||||
}),
|
||||
admin: adminRouter,
|
||||
account: accountRouter,
|
||||
|
||||
@@ -22,6 +22,8 @@ export interface ScenarioPreview {
|
||||
id: number;
|
||||
title: string;
|
||||
year: number | null;
|
||||
defaultStatTotal: number;
|
||||
fiction: number | null;
|
||||
npcCount: number;
|
||||
npcExCount: number;
|
||||
npcNeutralCount: number;
|
||||
@@ -227,6 +229,8 @@ const buildScenarioPreview = async (scenarioId: number): Promise<ScenarioPreview
|
||||
id: scenarioId,
|
||||
title: scenario.title,
|
||||
year: scenario.startYear ?? null,
|
||||
defaultStatTotal: scenario.config.stat.total,
|
||||
fiction: scenario.fiction,
|
||||
npcCount: scenario.generals.length,
|
||||
npcExCount: scenario.generalsEx.length,
|
||||
npcNeutralCount: scenario.generalsNeutral.length,
|
||||
@@ -272,6 +276,8 @@ const buildScenarioPreviewFromGit = async (commitSha: string, scenarioId: number
|
||||
id: scenarioId,
|
||||
title: scenario.title,
|
||||
year: scenario.startYear ?? null,
|
||||
defaultStatTotal: scenario.config.stat.total,
|
||||
fiction: scenario.fiction,
|
||||
npcCount: scenario.generals.length,
|
||||
npcExCount: scenario.generalsEx.length,
|
||||
npcNeutralCount: scenario.generalsNeutral.length,
|
||||
|
||||
@@ -98,6 +98,7 @@ const buildCaller = (
|
||||
apiPort: 15003,
|
||||
status: 'RUNNING' as const,
|
||||
buildStatus: 'SUCCEEDED' as const,
|
||||
buildCommitSha: 'HEAD',
|
||||
meta: {},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -111,6 +112,7 @@ const buildCaller = (
|
||||
apiPort: 15015,
|
||||
status: 'RUNNING' as const,
|
||||
buildStatus: 'SUCCEEDED' as const,
|
||||
buildCommitSha: 'HEAD',
|
||||
meta: {},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -268,6 +270,35 @@ const buildCaller = (
|
||||
};
|
||||
|
||||
describe('gateway auth flow', () => {
|
||||
it('allows a signed-in regular user to read only the active profile scenario catalog', async () => {
|
||||
const { caller, sealPassword, setSessionHeader } = buildCaller();
|
||||
|
||||
await expect(caller.lobby.scenarios({ profileName: 'che:default' })).rejects.toMatchObject({
|
||||
code: 'UNAUTHORIZED',
|
||||
});
|
||||
|
||||
const register = await caller.auth.registerLocal({
|
||||
username: 'scenario-reader',
|
||||
credential: sealPassword('scenario-reader-password'),
|
||||
displayName: '시나리오조회자',
|
||||
termsAgreed: true,
|
||||
privacyAgreed: true,
|
||||
thirdPartyUse: false,
|
||||
});
|
||||
setSessionHeader(register.sessionToken);
|
||||
|
||||
const scenarios = await caller.lobby.scenarios({ profileName: 'che:default' });
|
||||
expect(scenarios.length).toBeGreaterThan(0);
|
||||
expect(scenarios[0]).toMatchObject({
|
||||
id: expect.any(Number),
|
||||
title: expect.any(String),
|
||||
defaultStatTotal: expect.any(Number),
|
||||
});
|
||||
await expect(caller.lobby.scenarios({ profileName: 'hidden:default' })).rejects.toMatchObject({
|
||||
code: 'NOT_FOUND',
|
||||
});
|
||||
});
|
||||
|
||||
it('registers a local account first and accepts an encrypted password login', async () => {
|
||||
const { caller, users, sealPassword } = buildCaller();
|
||||
const register = await caller.auth.registerLocal({
|
||||
|
||||
@@ -14,6 +14,10 @@ describe('scenarioCatalog git ref support', () => {
|
||||
const ids = previews.map((scenario) => scenario.id);
|
||||
const sorted = [...ids].sort((a, b) => a - b);
|
||||
expect(ids).toEqual(sorted);
|
||||
expect(previews.every((scenario) => scenario.defaultStatTotal > 0)).toBe(true);
|
||||
expect(previews.every((scenario) => scenario.fiction === null || Number.isInteger(scenario.fiction))).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects without crashing when git cannot be spawned', async () => {
|
||||
|
||||
Reference in New Issue
Block a user