fix: keep profile entry tokens out of URLs

This commit is contained in:
2026-08-11 14:52:17 +00:00
parent d1c35cc380
commit d85c6eb279
8 changed files with 239 additions and 11 deletions
@@ -189,10 +189,7 @@ test('exchanges the gateway token before loading authenticated lobby general dat
await expect(row).toContainText('선택장수');
await expect(row.getByRole('button', { name: '입장' })).toBeVisible();
const portrait = row.locator('img');
await expect(portrait).toHaveAttribute(
'src',
'https://sam-image.hided.net/icons/users/core2026/account-hash.png'
);
await expect(portrait).toHaveAttribute('src', 'https://sam-image.hided.net/icons/users/core2026/account-hash.png');
await expect.poll(() => portrait.evaluate((image: HTMLImageElement) => image.naturalWidth)).toBe(1);
expect(gameOperations.find(({ operation }) => operation === 'auth.exchangeGatewayToken')).toEqual({
@@ -219,7 +216,32 @@ test('applies the signed general-acquisition policy to both create and possessio
await expect(row.getByRole('button', { name: '장수빙의' })).toBeDisabled();
});
test('opens the mode-1 possession tab with a fresh gateway game token', async ({ page }) => {
test('opens the profile root without profile or game token query parameters', async ({ page }) => {
await installFixture(page);
await page.route('**/hwe/', async (route) => {
await route.fulfill({
status: 200,
contentType: 'text/html',
body: '<title>Clean profile target</title>',
});
});
await page.goto('lobby');
const row = page.locator('tbody tr').filter({ hasText: 'hwe섭' });
await row.getByRole('button', { name: '입장', exact: true }).click();
await expect(page).toHaveURL(/\/hwe\/$/);
const target = new URL(page.url());
expect(target.search).toBe('');
expect(
await page.evaluate(() => JSON.parse(window.sessionStorage.getItem('sammo-pending-game-session') ?? 'null'))
).toEqual({
profile: 'hwe:903',
gatewayToken: 'encrypted-gateway-game-token',
});
});
test('opens the mode-1 possession route with a fresh gateway game token outside the URL', async ({ page }) => {
await installFixture(page, {
myGeneral: null,
selectionPoolEnabled: false,
@@ -238,11 +260,17 @@ test('opens the mode-1 possession tab with a fresh gateway game token', async ({
await expect(row.getByRole('button', { name: '장수빙의' })).toBeEnabled();
await row.getByRole('button', { name: '장수빙의' }).click();
await expect(page).toHaveURL(/\/hwe\/join\?/);
await expect(page).toHaveURL(/\/hwe\/join\?tab=possess$/);
const target = new URL(page.url());
expect(target.searchParams.get('tab')).toBe('possess');
expect(target.searchParams.get('profile')).toBe('hwe:903');
expect(target.searchParams.get('gameToken')).toBe('encrypted-gateway-game-token');
expect(target.searchParams.has('profile')).toBe(false);
expect(target.searchParams.has('gameToken')).toBe(false);
expect(
await page.evaluate(() => JSON.parse(window.sessionStorage.getItem('sammo-pending-game-session') ?? 'null'))
).toEqual({
profile: 'hwe:903',
gatewayToken: 'encrypted-gateway-game-token',
});
});
test('shows registration closed instead of acquisition actions at the Ref capacity boundary', async ({ page }) => {
+20 -3
View File
@@ -3,6 +3,7 @@ import { computed, ref, onMounted, watch } from 'vue';
import { useRouter } from 'vue-router';
import type { inferRouterOutputs } from '@trpc/server';
import type { AppRouter } from '@sammo-ts/gateway-api';
import { writeGameSessionTransfer } from '@sammo-ts/common/auth/gameSessionTransfer';
import DefaultLayout from '../layouts/DefaultLayout.vue';
import MapPreview from '../components/MapPreview.vue';
import { useToast } from '../composables/useToast';
@@ -227,8 +228,21 @@ const resolveGameUrl = (path: string, profileName: string, gameToken: string): s
const base = new URL(baseUrl, window.location.origin);
const normalizedPath = path.replace(/^\//, '');
const url = new URL(normalizedPath, base);
url.searchParams.set('profile', profileName);
url.searchParams.set('gameToken', gameToken);
let transferredInSessionStorage = false;
if (url.origin === window.location.origin) {
try {
transferredInSessionStorage = writeGameSessionTransfer(window.sessionStorage, {
profile: profileName,
gatewayToken: gameToken,
});
} catch {
transferredInSessionStorage = false;
}
}
if (!transferredInSessionStorage) {
url.searchParams.set('profile', profileName);
url.searchParams.set('gameToken', gameToken);
}
return url.toString();
};
@@ -571,7 +585,10 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => {
:map-layout="selectedMapPreview.mapLayout"
mode="detail"
/>
<div v-if="profileDetails[selectedMapProfile.profileName]" class="text-xs text-zinc-400 mt-2">
<div
v-if="profileDetails[selectedMapProfile.profileName]"
class="text-xs text-zinc-400 mt-2"
>
유저 {{ profileDetails[selectedMapProfile.profileName]?.userCnt ?? '-' }} /
{{ profileDetails[selectedMapProfile.profileName]?.maxUserCnt ?? '-' }} ·
{{ profileDetails[selectedMapProfile.profileName]?.nationCnt ?? '-' }}국 ·