위협 가이드를 관리자 전용 옵션으로(기본 꺼짐) + 최고관리자 추가 + 계정 메뉴에 관리자 메뉴
- 엔진: GameDefinition.adminOnlyOptions — 방은 항상 관리자 값 사용(잠금처럼 강제, 잠금 안내는 안 띄움) - 오목·체스·장기 threatHints, 바둑 atariWarning: 기본 false, 대기실 설정에서 뺌, 관리자 화면 게임 기본 규칙에서만 켬 - 체스·장기 hints는 off / moves만 남김 - 최고관리자 기본값에 1352267557213573160 추가(.env.example·문서 포함) - 상단 계정 메뉴: 관리자·최고관리자면 [🛠 관리자 메뉴] → /admin - 테스트: admin.test(관리자 전용 옵션 강제, 두 번째 최고관리자), admin.e2e(계정 메뉴, 위협 가이드 켜기 저장) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,8 @@ describe('roles', () => {
|
||||
const me = await api(s.base, '/api/admin/me', { cookie: sup.cookie });
|
||||
expect(me.body.role).toBe('superadmin');
|
||||
expect((await api(s.base, '/api/me', { cookie: sup.cookie })).body.role).toBe('superadmin');
|
||||
const sup2 = await discordLogin(s, '1352267557213573160');
|
||||
expect((await api(s.base, '/api/admin/me', { cookie: sup2.cookie })).body.role).toBe('superadmin');
|
||||
});
|
||||
|
||||
test('superadmin adds/removes an admin by Discord ID; takes effect immediately; admin cannot manage users', async () => {
|
||||
@@ -58,7 +60,7 @@ describe('roles', () => {
|
||||
// Pre-register before the person ever logged in.
|
||||
const add = await api(s.base, '/api/admin/admins', { method: 'POST', cookie: sup.cookie, body: JSON.stringify({ discordId: adminDiscord, note: '친구1' }) });
|
||||
expect(add.res.status).toBe(200);
|
||||
expect(add.body.admins.map((a: any) => a.discordId)).toEqual([SUPER, adminDiscord]);
|
||||
expect(add.body.admins.map((a: any) => a.discordId)).toEqual([SUPER, '1352267557213573160', adminDiscord]);
|
||||
const adm = await discordLogin(s, adminDiscord);
|
||||
expect((await api(s.base, '/api/admin/me', { cookie: adm.cookie })).body.role).toBe('admin');
|
||||
expect((await api(s.base, '/api/admin/users', { cookie: adm.cookie })).res.status).toBe(403);
|
||||
@@ -139,6 +141,37 @@ describe('game settings', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('admin-only options (threat guides)', () => {
|
||||
test('off by default; the host cannot turn them on; the admin default always wins and is not shown as a lock', async () => {
|
||||
const s = start();
|
||||
const host = await guest(s.base, '방장');
|
||||
const room = await api(s.base, '/api/rooms', { method: 'POST', cookie: host.cookie, body: JSON.stringify({ gameId: 'omok' }) });
|
||||
const c = await new Client(s.base, host.cookie, host.id).connect();
|
||||
clients.push(c);
|
||||
c.send({ t: 'join', code: room.body.code, as: 'player' });
|
||||
const first = await c.next('room');
|
||||
expect((first.room.options as any).threatHints).toBe(false);
|
||||
expect(first.room.lockedOptions).toEqual([]);
|
||||
c.send({ t: 'config', options: { ...(first.room.options as any), threatHints: true, boardSize: 19 } });
|
||||
const after = await c.next('room', (m) => (m.room.options as any).boardSize === 19);
|
||||
expect((after.room.options as any).threatHints).toBe(false);
|
||||
|
||||
const sup = await discordLogin(s, SUPER);
|
||||
const put = await api(s.base, '/api/admin/games/omok', { method: 'PUT', cookie: sup.cookie, body: JSON.stringify({ defaultOptions: { threatHints: true } }) });
|
||||
expect(put.res.status).toBe(200);
|
||||
const room2 = await api(s.base, '/api/rooms', { method: 'POST', cookie: host.cookie, body: JSON.stringify({ gameId: 'omok' }) });
|
||||
const c2 = await new Client(s.base, host.cookie, host.id).connect();
|
||||
clients.push(c2);
|
||||
c2.send({ t: 'join', code: room2.body.code, as: 'player' });
|
||||
const r2 = await c2.next('room');
|
||||
expect((r2.room.options as any).threatHints).toBe(true);
|
||||
expect(r2.room.lockedOptions).toEqual([]);
|
||||
c2.send({ t: 'config', options: { ...(r2.room.options as any), threatHints: false, boardSize: 19 } });
|
||||
const after2 = await c2.next('room', (m) => (m.room.options as any).boardSize === 19);
|
||||
expect((after2.room.options as any).threatHints).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('site settings, rooms, users', () => {
|
||||
test('maintenance blocks room creation; banned words masked in chat and rejected in nicknames', async () => {
|
||||
const s = start();
|
||||
|
||||
@@ -14,8 +14,8 @@ export interface Config {
|
||||
loadTestNoHttpLimits: boolean;
|
||||
}
|
||||
|
||||
/** The site owner's Discord ID (docs/14-admin.md §2). */
|
||||
export const DEFAULT_SUPERADMIN = '293719842274541579';
|
||||
/** Superadmin Discord IDs, comma-separated (docs/14-admin.md §2). */
|
||||
export const DEFAULT_SUPERADMIN = '293719842274541579,1352267557213573160';
|
||||
|
||||
export function loadConfig(env: Record<string, string | undefined> = process.env): Config {
|
||||
const publicOrigin = (env.PUBLIC_ORIGIN ?? 'http://localhost:5173').replace(/\/$/, '');
|
||||
|
||||
@@ -175,9 +175,14 @@ export class Room {
|
||||
return min <= max ? { min, max } : base;
|
||||
}
|
||||
|
||||
/** Options forced to the admin value: admin-locked ones plus the game's admin-only options (e.g. threat guides). */
|
||||
private locked(gameId: string): { defaults: unknown; paths: string[] } {
|
||||
const eff = this.deps.settings?.game(gameId);
|
||||
return { defaults: eff?.defaultOptions, paths: eff?.lockedOptions ?? [] };
|
||||
const adminOnly = this.deps.games[gameId]?.adminOnlyOptions ?? [];
|
||||
return {
|
||||
defaults: eff?.defaultOptions ?? this.deps.games[gameId]?.defaultOptions,
|
||||
paths: [...new Set([...(eff?.lockedOptions ?? []), ...adminOnly])],
|
||||
};
|
||||
}
|
||||
|
||||
seatOf(userId: string): number {
|
||||
@@ -321,7 +326,7 @@ export class Room {
|
||||
rematchVotes: [...this.rematchVotes],
|
||||
sessionStats: this.sessionStats,
|
||||
seedHash: this.game?.seedHash ?? null,
|
||||
lockedOptions: this.locked(this.config.gameId).paths,
|
||||
lockedOptions: this.deps.settings?.game(this.config.gameId)?.lockedOptions ?? [],
|
||||
lastSeed: this.lastSeed,
|
||||
lastGamePk: this.game?.finished ? this.game.gamePk : null,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user