Enable superuser admin navigation
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user