true, 'cookie_samesite' => 'Lax', 'name' => 'SITEADMINSESSID', 'use_strict_mode' => true, ]); define('ADMIN_PRIVATE_DIR', dirname(__DIR__) . '/private'); define('ADMIN_STORE', ADMIN_PRIVATE_DIR . '/store.json'); function json_response(array $data, int $code = 200): void { http_response_code($code); header('Content-Type: application/json; charset=utf-8'); header('X-Content-Type-Options: nosniff'); echo json_encode($data, JSON_UNESCAPED_UNICODE); exit; } function default_store(): array { return [ 'version' => 1, 'oauth' => [ 'facebookAppId' => '', 'facebookAppSecret' => '', 'facebookRedirectUri' => '', 'googleClientId' => '', 'googleClientSecret' => '', 'googleRedirectUri' => '', ], 'admins' => [], 'accounts' => [], ]; } function read_store(): array { if (!is_file(ADMIN_STORE)) { return default_store(); } $raw = @file_get_contents(ADMIN_STORE); $j = json_decode($raw ?: '{}', true); return is_array($j) ? array_replace_recursive(default_store(), $j) : default_store(); } function write_store(array $data): bool { if (!is_dir(ADMIN_PRIVATE_DIR)) { if (!@mkdir(ADMIN_PRIVATE_DIR, 0750, true)) { return false; } } $tmp = ADMIN_STORE . '.tmp.' . bin2hex(random_bytes(4)); $json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); if ($json === false) { return false; } if (file_put_contents($tmp, $json, LOCK_EX) === false) { return false; } if (!rename($tmp, ADMIN_STORE)) { @unlink($tmp); return false; } return true; } function store_needs_setup(): bool { if (!is_file(ADMIN_STORE)) { return true; } $s = read_store(); return empty($s['admins']); } function require_json_body(): array { $raw = file_get_contents('php://input'); $j = json_decode($raw ?: '{}', true); return is_array($j) ? $j : []; } function current_admin(): ?array { $id = $_SESSION['admin_id'] ?? null; if (!$id || !is_string($id)) { return null; } foreach (read_store()['admins'] ?? [] as $a) { if (($a['id'] ?? '') === $id) { return $a; } } return null; } function require_login(): void { if (!current_admin()) { json_response(['ok' => false, 'error' => 'Unauthorized'], 401); } } function is_super(array $admin): bool { return ($admin['role'] ?? '') === 'super'; } /** * รายชื่อหมวด (tab) ทั้งหมดในหน้า admin — ต้องตรงกับ data-tab ใน Admin/index.html * ใช้เป็น whitelist เวลาบันทึกสิทธิ์ กันค่ามั่วหลุดเข้า store */ function admin_all_tabs(): array { return [ 'accounts', 'admins', 'oauth', 'change-password', 'achievements', 'ai-admin', 'case-media', 'characters', 'evidence-cards', 'game-timing', 'highscore', 'map-editor', 'postcase', 'qb-map-editor', 'quiz', 'quiz-battle', 'quiz-carry', 'sound', 'special-quiz', 'jump-survive', 'mega-virus', 'space-shooter', 'stack-game', 'test-mode', 'troublesome', 'vote-timing', ]; } /** * หมวดที่แอดมินคนนี้เข้าได้จริง * - super = ทุกหมวดเสมอ * - ไม่มีคีย์ tabs หรือ tabs ว่าง = ทุกหมวด (ความเข้ากันได้กับบัญชีเดิมที่สร้างก่อนมีระบบสิทธิ์) */ function admin_tabs(array $a): array { if (is_super($a)) { return admin_all_tabs(); } $t = $a['tabs'] ?? null; if (!is_array($t) || count($t) === 0) { return admin_all_tabs(); } return array_values(array_intersect(admin_all_tabs(), $t)); } function admin_can(array $a, string $tab): bool { return in_array($tab, admin_tabs($a), true); } /** กันที่ API: แอดมินที่ไม่มีสิทธิ์หมวดนี้ ยิงตรงมาก็ต้องโดนปฏิเสธ (ซ่อน tab ฝั่งหน้าเว็บอย่างเดียวไม่ใช่ security) */ function require_tab(string $tab): array { $a = current_admin(); if (!$a) { json_response(['ok' => false, 'error' => 'Unauthorized'], 401); } if (!admin_can($a, $tab)) { json_response(['ok' => false, 'error' => 'บัญชีนี้ไม่มีสิทธิ์เข้าหมวด "' . $tab . '"'], 403); } return $a; } /** * บาง endpoint เป็น "config ก้อนเดียวที่หลายหมวดใช้ร่วมกัน" (เช่น quiz-settings.json) * ถ้า gate ไว้หมวดเดียวจะพังหมวดอื่น → ผ่านถ้ามีสิทธิ์ "อย่างน้อย 1 หมวด" ในลิสต์ */ function require_any_tab(array $tabs): array { $a = current_admin(); if (!$a) { json_response(['ok' => false, 'error' => 'Unauthorized'], 401); } foreach ($tabs as $t) { if (admin_can($a, $t)) { return $a; } } json_response(['ok' => false, 'error' => 'บัญชีนี้ไม่มีสิทธิ์เข้าหมวดที่เกี่ยวข้อง'], 403); } /** normalize ค่า tabs ที่รับมาจาก client ให้เหลือเฉพาะที่อยู่ใน whitelist */ function sanitize_tabs($raw): array { if (!is_array($raw)) { return []; } $all = admin_all_tabs(); $out = []; foreach ($raw as $t) { if (is_string($t) && in_array($t, $all, true) && !in_array($t, $out, true)) { $out[] = $t; } } return $out; } function strip_admin(array $a): array { unset($a['passwordHash']); $a['tabsEffective'] = admin_tabs($a); return $a; } function new_id(): string { return bin2hex(random_bytes(12)); }