M1.5 관리자 사이트: 슈퍼어드민/어드민 권한, 게임·사이트 설정, 방·사용자 관리, 기록
- 권한: 디스코드 ID로 요청마다 계산. 슈퍼어드민 293719842274541579(SUPERADMIN_DISCORD_IDS), 어드민은 슈퍼어드민이 디스코드 ID로 추가(로그인 전 미리 등록 가능), 고정 슈퍼어드민은 삭제 불가 - 게임 설정: 사용 여부, 표시 이름·설명·안내문, 원작 범위 안 인원, 모든 규칙 옵션 기본값, 옵션별 방장 변경 금지(잠금), 초기화. 입력 화면은 옵션 스키마(zod→JSON Schema)로 자동 생성 - 오목 옵션 전체에 한국어 제목·선택지 메타, 엔진 고정값(자동 착수 허용 횟수, 무승부 재제안 간격, 한 수 제한 초)을 옵션으로 꺼냄 - 사이트 설정: 이름, 공지, 점검 모드, 금지어, 방 수 제한, 연결 유예, 채팅 기본값 - 방 관리(목록·닫기), 사용자 관리(검색·닉네임 변경·이용 제한·강제 로그아웃·삭제, 슈퍼어드민 전용), 관리자 작업 기록(전/후 값) - 이용 제한: 세션 삭제 + 접속 종료 + 디스코드 재로그인 거부 - 테스트: 관리자 API 7개(권한·설정 반영·잠금·점검·금지어·방 닫기·이용 제한·기록), 오목 옵션 2개, e2e/admin.e2e.ts(슈퍼어드민 로그인→규칙 변경·잠금→어드민 추가→게스트 새 방 반영) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -32,7 +32,7 @@ export class Sessions {
|
||||
const row = this.db
|
||||
.query<{ user_id: string; expires_at: number; last_used_at: number; kind: 'guest' | 'member' }, [string]>(
|
||||
`SELECT s.user_id, s.expires_at, s.last_used_at, u.kind FROM sessions s JOIN users u ON u.id = s.user_id
|
||||
WHERE s.token_hash = ? AND u.deleted_at IS NULL`,
|
||||
WHERE s.token_hash = ? AND u.deleted_at IS NULL AND u.banned_at IS NULL`,
|
||||
)
|
||||
.get(hash);
|
||||
if (!row) return null;
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface UserRow {
|
||||
created_at: number;
|
||||
last_seen_at: number;
|
||||
deleted_at: number | null;
|
||||
banned_at: number | null;
|
||||
banned_reason: string | null;
|
||||
}
|
||||
|
||||
export const NICK_CHANGES_PER_DAY = 10;
|
||||
@@ -111,6 +113,39 @@ export class Users {
|
||||
}
|
||||
}
|
||||
|
||||
/** Admin rename: ignores the daily limit. */
|
||||
forceNickname(userId: string, nickname: string): void {
|
||||
this.db.query('UPDATE users SET nickname = ? WHERE id = ?').run(nickname, userId);
|
||||
}
|
||||
|
||||
setBan(userId: string, reason: string | null, now = Date.now()): void {
|
||||
if (reason === null) this.db.query('UPDATE users SET banned_at = NULL, banned_reason = NULL WHERE id = ?').run(userId);
|
||||
else this.db.query('UPDATE users SET banned_at = ?, banned_reason = ? WHERE id = ?').run(now, reason, userId);
|
||||
}
|
||||
|
||||
search(q: string, limit: number, offset: number) {
|
||||
const like = `%${q.replace(/[%_]/g, (m) => '\\' + m)}%`;
|
||||
const rows = this.db
|
||||
.query<UserRow & { discord_id: string | null; discord_name: string | null }, [string, string, string, string, number, number]>(
|
||||
`SELECT u.*, o.provider_user_id AS discord_id, o.username AS discord_name FROM users u
|
||||
LEFT JOIN oauth_accounts o ON o.user_id = u.id AND o.provider = 'discord'
|
||||
WHERE u.deleted_at IS NULL AND (? = '' OR u.nickname LIKE ? ESCAPE '\\' OR u.id = ? OR o.provider_user_id = ?)
|
||||
ORDER BY u.last_seen_at DESC LIMIT ? OFFSET ?`,
|
||||
)
|
||||
.all(q, like, q, q, limit, offset);
|
||||
const total = this.db.query<{ n: number }, []>('SELECT COUNT(*) AS n FROM users WHERE deleted_at IS NULL').get()!.n;
|
||||
return { rows, total };
|
||||
}
|
||||
|
||||
counts(): { total: number; guests: number; members: number; banned: number } {
|
||||
return this.db
|
||||
.query<{ total: number; guests: number; members: number; banned: number }, []>(
|
||||
`SELECT COUNT(*) AS total, SUM(kind = 'guest') AS guests, SUM(kind = 'member') AS members, SUM(banned_at IS NOT NULL) AS banned
|
||||
FROM users WHERE deleted_at IS NULL`,
|
||||
)
|
||||
.get()!;
|
||||
}
|
||||
|
||||
touch(userId: string, now = Date.now()): void {
|
||||
this.db.query('UPDATE users SET last_seen_at = ? WHERE id = ?').run(now, userId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user