210 lines
6.2 KiB
PHP
210 lines
6.2 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
session_start([
|
|
'cookie_httponly' => 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));
|
|
}
|