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({
|
export default defineConfig({
|
||||||
testDir: '.',
|
testDir: '.',
|
||||||
testMatch: 'server-operations.spec.ts',
|
testMatch: ['server-operations.spec.ts', 'lobby-admin-navigation.spec.ts'],
|
||||||
fullyParallel: false,
|
fullyParallel: false,
|
||||||
workers: 1,
|
workers: 1,
|
||||||
timeout: 30_000,
|
timeout: 30_000,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue';
|
import { computed, ref, onMounted } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import type { inferRouterOutputs } from '@trpc/server';
|
import type { inferRouterOutputs } from '@trpc/server';
|
||||||
import type { AppRouter } from '@sammo-ts/gateway-api';
|
import type { AppRouter } from '@sammo-ts/gateway-api';
|
||||||
@@ -28,6 +28,16 @@ const profiles = ref<LobbyProfile[]>([]);
|
|||||||
const profileDetails = ref<Record<string, LobbyInfo | undefined>>({});
|
const profileDetails = ref<Record<string, LobbyInfo | undefined>>({});
|
||||||
const profileMapPreviews = ref<Record<string, MapPreviewBundle | undefined>>({});
|
const profileMapPreviews = ref<Record<string, MapPreviewBundle | undefined>>({});
|
||||||
const entryLoading = ref<Record<string, boolean>>({});
|
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 () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -353,12 +363,13 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => {
|
|||||||
>
|
>
|
||||||
로 그 아 웃
|
로 그 아 웃
|
||||||
</button>
|
</button>
|
||||||
<button
|
<RouterLink
|
||||||
v-if="me?.roles?.includes('admin')"
|
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"
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user