Enable superuser admin navigation

This commit is contained in:
2026-07-25 13:35:03 +00:00
parent 36d11acdfa
commit 46f9d8e916
3 changed files with 93 additions and 5 deletions
@@ -0,0 +1,77 @@
import { expect, test, type Page, type Route } from '@playwright/test';
const response = (data: unknown) => ({ result: { data } });
const operationNames = (route: Route): string[] => {
const url = new URL(route.request().url());
return decodeURIComponent(url.pathname.slice(url.pathname.lastIndexOf('/trpc/') + 6)).split(',');
};
const installGatewayFixture = async (page: Page, roles: string[]) => {
await page.addInitScript(() => {
window.localStorage.setItem('sammo-session-token', 'playwright-admin-session');
});
await page.route('**/gateway/api/trpc/**', async (route) => {
const results = operationNames(route).map((operation) => {
if (operation === 'me') {
return response({
id: 'admin-user',
username: 'admin',
displayName: '관리자',
roles,
createdAt: '2026-07-25T00:00:00.000Z',
});
}
if (operation === 'lobby.notice' || operation === 'admin.system.getNotice') {
return response(operation === 'lobby.notice' ? '' : { notice: '' });
}
if (
operation === 'lobby.profiles' ||
operation === 'admin.profiles.list' ||
operation === 'admin.profiles.listScenarios' ||
operation === 'admin.operations.list'
) {
return response([]);
}
if (operation === 'admin.users.getLocalAccountStatus') {
return response({ enabled: true });
}
throw new Error(`Unhandled tRPC operation: ${operation}`);
});
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(results),
});
});
};
test('bootstrap superuser can navigate from the lobby to server operations', async ({ page }) => {
await installGatewayFixture(page, ['superuser']);
await page.goto('lobby');
const adminLink = page.getByRole('link', { name: '관리자 페이지' });
await expect(adminLink).toBeVisible();
await adminLink.click();
await expect(page).toHaveURL(/\/gateway\/admin$/);
await expect(page.getByRole('heading', { name: '관리자 콘솔' })).toBeVisible();
await page.getByRole('link', { name: '서버 배포 · 시나리오 초기화' }).click();
await expect(page).toHaveURL(/\/gateway\/admin\/server-operations$/);
});
test('scoped administrators see the same navigation while ordinary users do not', async ({ browser }) => {
const scopedContext = await browser.newContext();
const scopedPage = await scopedContext.newPage();
await installGatewayFixture(scopedPage, ['admin.profiles.manage:hwe:2']);
await scopedPage.goto('lobby');
await expect(scopedPage.getByRole('link', { name: '관리자 페이지' })).toBeVisible();
await scopedContext.close();
const userContext = await browser.newContext();
const userPage = await userContext.newPage();
await installGatewayFixture(userPage, []);
await userPage.goto('lobby');
await expect(userPage.getByRole('link', { name: '관리자 페이지' })).toHaveCount(0);
await userContext.close();
});
@@ -6,7 +6,7 @@ const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../.
export default defineConfig({
testDir: '.',
testMatch: 'server-operations.spec.ts',
testMatch: ['server-operations.spec.ts', 'lobby-admin-navigation.spec.ts'],
fullyParallel: false,
workers: 1,
timeout: 30_000,
+15 -4
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { computed, ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import type { inferRouterOutputs } from '@trpc/server';
import type { AppRouter } from '@sammo-ts/gateway-api';
@@ -28,6 +28,16 @@ const profiles = ref<LobbyProfile[]>([]);
const profileDetails = ref<Record<string, LobbyInfo | undefined>>({});
const profileMapPreviews = ref<Record<string, MapPreviewBundle | undefined>>({});
const entryLoading = ref<Record<string, boolean>>({});
const canAccessAdmin = computed(
() =>
me.value?.roles.some(
(role) =>
role === 'superuser' ||
role === 'admin' ||
role === 'admin.superuser' ||
role.startsWith('admin.')
) ?? false
);
onMounted(async () => {
try {
@@ -353,12 +363,13 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => {
>
</button>
<button
v-if="me?.roles?.includes('admin')"
<RouterLink
v-if="canAccessAdmin"
to="/admin"
class="bg-zinc-800 hover:bg-zinc-700 text-white px-6 py-2 rounded border border-zinc-700 transition-colors"
>
관리자 페이지
</button>
</RouterLink>
</div>
</div>
</div>