슈퍼어드민 디스코드 ID를 저장소에서 제거: 서버 deploy/.env의 SUPERADMIN_DISCORD_IDS에만 둔다
- config: 기본값 삭제(설정이 없으면 슈퍼어드민 없음) - 테스트·E2E: 만든 ID(900000000000000001/2) 사용 - 문서·README·.env.example·관리자 화면 입력 예시에서 실제 ID 삭제 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,7 +34,7 @@ cd deploy && docker compose up -d --build
|
|||||||
자세한 절차는 [docs/11-deployment-ops.md](docs/11-deployment-ops.md).
|
자세한 절차는 [docs/11-deployment-ops.md](docs/11-deployment-ops.md).
|
||||||
|
|
||||||
## 관리자
|
## 관리자
|
||||||
`/admin`. 슈퍼어드민은 디스코드 ID `293719842274541579`, `1352267557213573160`(환경 변수 `SUPERADMIN_DISCORD_IDS`), 어드민은 관리자 화면에서 디스코드 ID로 추가한다. 자세한 내용은 [docs/14-admin.md](docs/14-admin.md).
|
`/admin`. 슈퍼어드민은 서버의 `deploy/.env`에 있는 환경 변수 `SUPERADMIN_DISCORD_IDS`의 디스코드 ID(저장소에는 넣지 않음), 어드민은 관리자 화면에서 디스코드 ID로 추가한다. 자세한 내용은 [docs/14-admin.md](docs/14-admin.md).
|
||||||
|
|
||||||
## 구조
|
## 구조
|
||||||
- `packages/engine` — 게임 공통 타입, 시드 난수, 테스트 도구
|
- `packages/engine` — 게임 공통 타입, 시드 난수, 테스트 도구
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/** Admin site tests (docs/14-admin.md §9). */
|
/** Admin site tests (docs/14-admin.md §9). */
|
||||||
import { afterEach, describe, expect, test } from 'bun:test';
|
import { afterEach, describe, expect, test } from 'bun:test';
|
||||||
import { Client, api, boot, guest, sleep } from './test-utils';
|
import { Client, TEST_SUPERADMINS, api, boot, guest, sleep } from './test-utils';
|
||||||
|
|
||||||
const SUPER = '293719842274541579';
|
const [SUPER, SUPER2] = TEST_SUPERADMINS;
|
||||||
type Booted = ReturnType<typeof boot>;
|
type Booted = ReturnType<typeof boot>;
|
||||||
const running: Booted[] = [];
|
const running: Booted[] = [];
|
||||||
const clients: Client[] = [];
|
const clients: Client[] = [];
|
||||||
@@ -47,7 +47,7 @@ describe('roles', () => {
|
|||||||
const me = await api(s.base, '/api/admin/me', { cookie: sup.cookie });
|
const me = await api(s.base, '/api/admin/me', { cookie: sup.cookie });
|
||||||
expect(me.body.role).toBe('superadmin');
|
expect(me.body.role).toBe('superadmin');
|
||||||
expect((await api(s.base, '/api/me', { cookie: sup.cookie })).body.role).toBe('superadmin');
|
expect((await api(s.base, '/api/me', { cookie: sup.cookie })).body.role).toBe('superadmin');
|
||||||
const sup2 = await discordLogin(s, '1352267557213573160');
|
const sup2 = await discordLogin(s, SUPER2);
|
||||||
expect((await api(s.base, '/api/admin/me', { cookie: sup2.cookie })).body.role).toBe('superadmin');
|
expect((await api(s.base, '/api/admin/me', { cookie: sup2.cookie })).body.role).toBe('superadmin');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ describe('roles', () => {
|
|||||||
// Pre-register before the person ever logged in.
|
// 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' }) });
|
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.res.status).toBe(200);
|
||||||
expect(add.body.admins.map((a: any) => a.discordId)).toEqual([SUPER, '1352267557213573160', adminDiscord]);
|
expect(add.body.admins.map((a: any) => a.discordId)).toEqual([SUPER, SUPER2, adminDiscord]);
|
||||||
const adm = await discordLogin(s, 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/me', { cookie: adm.cookie })).body.role).toBe('admin');
|
||||||
expect((await api(s.base, '/api/admin/users', { cookie: adm.cookie })).res.status).toBe(403);
|
expect((await api(s.base, '/api/admin/users', { cookie: adm.cookie })).res.status).toBe(403);
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ export interface Config {
|
|||||||
loadTestNoHttpLimits: boolean;
|
loadTestNoHttpLimits: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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 {
|
export function loadConfig(env: Record<string, string | undefined> = process.env): Config {
|
||||||
const publicOrigin = (env.PUBLIC_ORIGIN ?? 'http://localhost:5173').replace(/\/$/, '');
|
const publicOrigin = (env.PUBLIC_ORIGIN ?? 'http://localhost:5173').replace(/\/$/, '');
|
||||||
const extra = (env.ALLOWED_ORIGINS ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
const extra = (env.ALLOWED_ORIGINS ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
||||||
@@ -36,6 +33,7 @@ export function loadConfig(env: Record<string, string | undefined> = process.env
|
|||||||
secureCookies: publicOrigin.startsWith('https://'),
|
secureCookies: publicOrigin.startsWith('https://'),
|
||||||
trustProxy: env.TRUST_PROXY === '1',
|
trustProxy: env.TRUST_PROXY === '1',
|
||||||
loadTestNoHttpLimits: env.LOADTEST_NO_HTTP_LIMITS === '1' && !publicOrigin.startsWith('https://'),
|
loadTestNoHttpLimits: env.LOADTEST_NO_HTTP_LIMITS === '1' && !publicOrigin.startsWith('https://'),
|
||||||
superadminDiscordIds: (env.SUPERADMIN_DISCORD_IDS ?? DEFAULT_SUPERADMIN).split(',').map((s) => s.trim()).filter(Boolean),
|
/** Superadmin Discord IDs, comma-separated (docs/14-admin.md §2). Only in deploy/.env, never in the repo. */
|
||||||
|
superadminDiscordIds: (env.SUPERADMIN_DISCORD_IDS ?? '').split(',').map((s) => s.trim()).filter(Boolean),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { loadConfig } from './config';
|
|||||||
import { startServer, type StartOptions } from './server';
|
import { startServer, type StartOptions } from './server';
|
||||||
|
|
||||||
export const ORIGIN = 'http://localhost:5173';
|
export const ORIGIN = 'http://localhost:5173';
|
||||||
|
/** Made-up superadmin Discord IDs for tests (the real ones live only in deploy/.env). */
|
||||||
|
export const TEST_SUPERADMINS = ['900000000000000001', '900000000000000002'] as const;
|
||||||
|
|
||||||
export function tempDbPath(): string {
|
export function tempDbPath(): string {
|
||||||
const dir = mkdtempSync(join(process.env.TMPDIR ?? tmpdir(), 'bg-test-'));
|
const dir = mkdtempSync(join(process.env.TMPDIR ?? tmpdir(), 'bg-test-'));
|
||||||
@@ -14,7 +16,7 @@ export function tempDbPath(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function boot(dbPath = tempDbPath(), extra: Partial<StartOptions> = {}) {
|
export function boot(dbPath = tempDbPath(), extra: Partial<StartOptions> = {}) {
|
||||||
const config = loadConfig({ PORT: '0', DB_PATH: dbPath, PUBLIC_ORIGIN: ORIGIN });
|
const config = loadConfig({ PORT: '0', DB_PATH: dbPath, PUBLIC_ORIGIN: ORIGIN, SUPERADMIN_DISCORD_IDS: TEST_SUPERADMINS.join(',') });
|
||||||
const app = startServer({ config, log: () => {}, staticDir: null, stopWaitMs: 50, ...extra });
|
const app = startServer({ config, log: () => {}, staticDir: null, stopWaitMs: 50, ...extra });
|
||||||
const base = `http://localhost:${app.server.port}`;
|
const base = `http://localhost:${app.server.port}`;
|
||||||
return { ...app, base, dbPath, config };
|
return { ...app, base, dbPath, config };
|
||||||
|
|||||||
@@ -577,7 +577,7 @@ function Admins() {
|
|||||||
<h2>어드민 추가</h2>
|
<h2>어드민 추가</h2>
|
||||||
<label className="field">
|
<label className="field">
|
||||||
디스코드 ID(숫자)
|
디스코드 ID(숫자)
|
||||||
<input className="input" inputMode="numeric" value={discordId} onChange={(e) => setDiscordId(e.target.value)} placeholder="예: 293719842274541579" />
|
<input className="input" inputMode="numeric" value={discordId} onChange={(e) => setDiscordId(e.target.value)} placeholder="디스코드 ID (숫자 17~20자리)" />
|
||||||
</label>
|
</label>
|
||||||
<label className="field">
|
<label className="field">
|
||||||
메모(선택)
|
메모(선택)
|
||||||
|
|||||||
@@ -7,5 +7,5 @@ SESSION_SECRET=
|
|||||||
DISCORD_CLIENT_ID=
|
DISCORD_CLIENT_ID=
|
||||||
DISCORD_CLIENT_SECRET=
|
DISCORD_CLIENT_SECRET=
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
# Super admin Discord user IDs (comma separated). Default is the site owner.
|
# Super admin Discord user IDs (comma separated). Required for the admin site; keep the real IDs out of git.
|
||||||
SUPERADMIN_DISCORD_IDS=293719842274541579,1352267557213573160
|
SUPERADMIN_DISCORD_IDS=
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ deploy/
|
|||||||
| `DISCORD_CLIENT_ID` / `DISCORD_CLIENT_SECRET` | 디스코드 앱 |
|
| `DISCORD_CLIENT_ID` / `DISCORD_CLIENT_SECRET` | 디스코드 앱 |
|
||||||
| `SESSION_SECRET` | 짧은 수명 서명 쿠키(OAuth state)용 32바이트 |
|
| `SESSION_SECRET` | 짧은 수명 서명 쿠키(OAuth state)용 32바이트 |
|
||||||
| `DB_PATH` | 기본 `/data/app.db` |
|
| `DB_PATH` | 기본 `/data/app.db` |
|
||||||
| `SUPERADMIN_DISCORD_IDS` | 슈퍼어드민 디스코드 ID(쉼표 구분). 기본 `293719842274541579,1352267557213573160` |
|
| `SUPERADMIN_DISCORD_IDS` | 슈퍼어드민 디스코드 ID(쉼표 구분). 기본값 없음, 실제 값은 서버 `deploy/.env`에만(저장소에 넣지 않음) |
|
||||||
| `TRUST_PROXY` | `1`이면 Caddy가 붙인 X-Forwarded-For로 IP 판단 |
|
| `TRUST_PROXY` | `1`이면 Caddy가 붙인 X-Forwarded-For로 IP 판단 |
|
||||||
| `PORT` | 기본 3000 |
|
| `PORT` | 기본 3000 |
|
||||||
| `LOG_LEVEL` | info |
|
| `LOG_LEVEL` | info |
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
## 0. 확정된 결정 (2026-10-04)
|
## 0. 확정된 결정 (2026-10-04)
|
||||||
- 도메인: **game.tkrmagid.kr** 고정. 모든 기능을 완성한 뒤 연결한다.
|
- 도메인: **game.tkrmagid.kr** 고정. 모든 기능을 완성한 뒤 연결한다.
|
||||||
- 상표명: 친구끼리 쓰는 사이트라 **원래 이름을 그대로 사용**한다.
|
- 상표명: 친구끼리 쓰는 사이트라 **원래 이름을 그대로 사용**한다.
|
||||||
- 관리자 사이트 추가. 슈퍼어드민은 디스코드 ID `293719842274541579`, 어드민은 디스코드 ID로 추가(`14-admin.md`).
|
- 관리자 사이트 추가. 슈퍼어드민은 `SUPERADMIN_DISCORD_IDS`의 디스코드 ID, 어드민은 디스코드 ID로 추가(`14-admin.md`).
|
||||||
- 화면은 **PC 버전 중심**으로 만든다(휴대폰은 접어서 지원).
|
- 화면은 **PC 버전 중심**으로 만든다(휴대폰은 접어서 지원).
|
||||||
- 게임 규칙 기본값은 관리자 사이트에서 모두 바꿀 수 있으므로, 3절의 기본안은 "처음 값"이다.
|
- 게임 규칙 기본값은 관리자 사이트에서 모두 바꿀 수 있으므로, 3절의 기본안은 "처음 값"이다.
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
## 2. 권한
|
## 2. 권한
|
||||||
| 등급 | 누가 | 할 수 있는 것 |
|
| 등급 | 누가 | 할 수 있는 것 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 슈퍼어드민 | 디스코드 ID `293719842274541579`, `1352267557213573160` (설정 `SUPERADMIN_DISCORD_IDS`, 기본값이 이 두 ID) | 모든 것: 아래 어드민 권한 + 사용자 관리 + 어드민 추가/삭제 |
|
| 슈퍼어드민 | 서버 `deploy/.env`의 `SUPERADMIN_DISCORD_IDS`에 적은 디스코드 ID (기본값 없음, 개인정보라 저장소에 넣지 않음) | 모든 것: 아래 어드민 권한 + 사용자 관리 + 어드민 추가/삭제 |
|
||||||
| 어드민 | 슈퍼어드민이 디스코드 ID로 등록한 사람 | 관리자 사이트 보기, 게임 설정, 사이트 설정, 방 관리, 기록 보기 |
|
| 어드민 | 슈퍼어드민이 디스코드 ID로 등록한 사람 | 관리자 사이트 보기, 게임 설정, 사이트 설정, 방 관리, 기록 보기 |
|
||||||
| 일반 | 그 외(게스트 포함) | 관리자 사이트 접근 불가(관리자 주소로 들어오면 "권한이 없어요") |
|
| 일반 | 그 외(게스트 포함) | 관리자 사이트 접근 불가(관리자 주소로 들어오면 "권한이 없어요") |
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ const PORT = Number(process.env.E2E_PORT ?? 4518);
|
|||||||
const ORIGIN = `http://localhost:${PORT}`;
|
const ORIGIN = `http://localhost:${PORT}`;
|
||||||
const SHOTS = process.env.E2E_SHOTS ?? join(import.meta.dir, '../test-results');
|
const SHOTS = process.env.E2E_SHOTS ?? join(import.meta.dir, '../test-results');
|
||||||
mkdirSync(SHOTS, { recursive: true });
|
mkdirSync(SHOTS, { recursive: true });
|
||||||
const SUPER = '293719842274541579';
|
/** Made-up ID; the real superadmin IDs live only in deploy/.env. */
|
||||||
|
const SUPER = '900000000000000001';
|
||||||
|
|
||||||
const config = loadConfig({ PORT: String(PORT), DB_PATH: join(mkdtempSync(join(process.env.TMPDIR ?? '/tmp', 'bg-e2e-')), 'app.db'), PUBLIC_ORIGIN: ORIGIN });
|
const config = loadConfig({ PORT: String(PORT), DB_PATH: join(mkdtempSync(join(process.env.TMPDIR ?? '/tmp', 'bg-e2e-')), 'app.db'), PUBLIC_ORIGIN: ORIGIN, SUPERADMIN_DISCORD_IDS: SUPER });
|
||||||
config.discord = { clientId: 'e2e', clientSecret: 'e2e' };
|
config.discord = { clientId: 'e2e', clientSecret: 'e2e' };
|
||||||
const app = startServer({
|
const app = startServer({
|
||||||
config,
|
config,
|
||||||
|
|||||||
Reference in New Issue
Block a user