diff --git a/monitor.php b/monitor.php index fba17fe..f87424e 100644 --- a/monitor.php +++ b/monitor.php @@ -19,6 +19,9 @@ const LOG_LIMIT = 300; const CHART_LIMIT = 500; const CAR_MONITOR_COOKIE = 'car_monitor_access'; const CAR_MONITOR_AUTH_TTL = 2592000; +const CAR_MONITOR_SETTINGS_COOKIE = 'car_monitor_settings_access'; +const CAR_MONITOR_SETTINGS_TTL = 900; +const CAR_MONITOR_SETTINGS_PASSWORD = '1026!)Corjs'; const DATA_MONTHLY_INCLUDED_BYTES = 104857600; // 100MB const DATA_BILLING_UNIT_BYTES = 512; // 0.5KB const DATA_BILLING_UNIT_KRW = 0.011; @@ -724,24 +727,6 @@ function billing_calibration(?array $currentBilling, array $billingScaleInfo): a ]; } -function car_monitor_is_admin(): bool -{ - $session = car_monitor_launcher_session(); - return !empty($session['admin']); -} - -function car_monitor_require_admin(): void -{ - if (car_monitor_is_admin()) { - return; - } - - json_response([ - 'status' => 'forbidden', - 'message' => '관리자 권한이 필요합니다.', - ], 403); -} - function request_payload(): array { $raw = file_get_contents('php://input'); @@ -888,18 +873,6 @@ function car_monitor_cookie_secret(): string return $secret; } -function car_monitor_admin_assertion_secret(): string -{ - global $carSecretConfig; - - $secret = (string)($carSecretConfig['monitor_auth']['admin_assertion_secret'] ?? ''); - if ($secret === '') { - throw new RuntimeException('Car monitor admin assertion secret is missing.'); - } - - return $secret; -} - function car_monitor_cookie_options(int $expires): array { return [ @@ -960,27 +933,54 @@ function car_monitor_has_cookie(): bool return is_array(car_monitor_cookie_payload()); } -function car_monitor_verify_admin_assertion(string $assertion): bool +function car_monitor_settings_cookie_value(int $expires): string { - if (strpos($assertion, '.') === false) { + $body = base64_encode(json_encode([ + 'aud' => 'car-monitor-settings', + 'exp' => $expires, + 'iat' => time(), + ], JSON_UNESCAPED_SLASHES)); + $sig = hash_hmac('sha256', $body, car_monitor_cookie_secret()); + + return $body . '.' . $sig; +} + +function car_monitor_issue_settings_cookie(): void +{ + $expires = time() + CAR_MONITOR_SETTINGS_TTL; + setcookie(CAR_MONITOR_SETTINGS_COOKIE, car_monitor_settings_cookie_value($expires), car_monitor_cookie_options($expires)); +} + +function car_monitor_has_settings_access(): bool +{ + $cookie = $_COOKIE[CAR_MONITOR_SETTINGS_COOKIE] ?? ''; + if (!is_string($cookie) || strpos($cookie, '.') === false) { return false; } - [$body, $sig] = explode('.', $assertion, 2); - $expected = hash_hmac('sha256', $body, car_monitor_admin_assertion_secret()); + [$body, $sig] = explode('.', $cookie, 2); + $expected = hash_hmac('sha256', $body, car_monitor_cookie_secret()); if (!hash_equals($expected, $sig)) { return false; } $decoded = base64_decode($body, true); $payload = $decoded !== false ? json_decode($decoded, true) : null; - if (!is_array($payload)) { - return false; + return is_array($payload) + && ($payload['aud'] ?? '') === 'car-monitor-settings' + && (int)($payload['exp'] ?? 0) >= time(); +} + +function car_monitor_require_settings_access(): void +{ + if (car_monitor_has_settings_access()) { + return; } - return ($payload['aud'] ?? '') === 'car-monitor' - && !empty($payload['admin']) - && (int)($payload['exp'] ?? 0) >= time(); + json_response([ + 'status' => 'forbidden', + 'message' => '관리자 비밀번호가 필요합니다.', + ], 403); } function car_monitor_launcher_session(): array @@ -993,28 +993,21 @@ function car_monitor_launcher_session(): array $cookieHeader = trim((string)($_SERVER['HTTP_COOKIE'] ?? '')); if ($cookieHeader !== '') { - $adminSession = car_monitor_launcher_api_request('GET', 'https://chaegeon.com/custom/launcher/api.php?adminSession=1', null, $cookieHeader); - if (is_array($adminSession) && !empty($adminSession['authenticated']) && !empty($adminSession['admin'])) { - car_monitor_issue_cookie(true); - $session = ['authenticated' => true, 'admin' => true]; - return $session; - } - $remoteSession = car_monitor_launcher_api_request('GET', 'https://chaegeon.com/custom/launcher/api.php?session=1', null, $cookieHeader); if (is_array($remoteSession) && !empty($remoteSession['authenticated'])) { - car_monitor_issue_cookie(!empty($remoteSession['admin'])); - $session = $remoteSession; + car_monitor_issue_cookie(); + $session = ['authenticated' => true]; return $session; } } $cookiePayload = car_monitor_cookie_payload(); if (is_array($cookiePayload)) { - $session = ['authenticated' => true, 'admin' => !empty($cookiePayload['admin'])]; + $session = ['authenticated' => true]; return $session; } - $session = ['authenticated' => false, 'admin' => false]; + $session = ['authenticated' => false]; return $session; } @@ -1134,10 +1127,8 @@ function car_monitor_issue_pwa_cookie_if_requested(): void $cookieHeader = trim((string)($_SERVER['HTTP_COOKIE'] ?? '')); if ($cookieHeader !== '') { - $adminSession = car_monitor_launcher_api_request('GET', 'https://chaegeon.com/custom/launcher/api.php?adminSession=1', null, $cookieHeader); $remoteSession = car_monitor_launcher_api_request('GET', 'https://chaegeon.com/custom/launcher/api.php?session=1', null, $cookieHeader); - if ((is_array($adminSession) && !empty($adminSession['authenticated'])) - || (is_array($remoteSession) && !empty($remoteSession['authenticated']))) { + if (is_array($remoteSession) && !empty($remoteSession['authenticated'])) { car_monitor_issue_cookie(); } } @@ -1174,26 +1165,28 @@ function car_monitor_auth_api(): void json_response(['result' => 'fail'], 401); } -function car_monitor_admin_assert_api(): void +function car_monitor_settings_auth_api(): void { + car_monitor_require_access(true); + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { json_response(['error' => 'method_not_allowed'], 405); } $data = request_payload(); - $assertion = (string)($data['assertion'] ?? ''); - if (!car_monitor_verify_admin_assertion($assertion)) { + $password = (string)($data['password'] ?? ''); + if (!hash_equals(CAR_MONITOR_SETTINGS_PASSWORD, $password)) { json_response([ 'status' => 'forbidden', - 'message' => '관리자 권한이 필요합니다.', + 'message' => '관리자 비밀번호가 올바르지 않습니다.', ], 403); } - car_monitor_issue_cookie(true); + car_monitor_issue_settings_cookie(); json_response([ 'status' => 'success', - 'authenticated' => true, - 'admin' => true, + 'settings_authenticated' => true, + 'expires_in' => CAR_MONITOR_SETTINGS_TTL, ]); } @@ -1201,8 +1194,8 @@ if (isset($_GET['mode']) && $_GET['mode'] === 'auth-login') { car_monitor_auth_api(); } -if (isset($_GET['mode']) && $_GET['mode'] === 'admin-assert') { - car_monitor_admin_assert_api(); +if (isset($_GET['mode']) && $_GET['mode'] === 'settings-auth') { + car_monitor_settings_auth_api(); } car_monitor_issue_pwa_cookie_if_requested(); @@ -1311,7 +1304,7 @@ if (isset($_GET['mode']) && $_GET['mode'] === 'usage') { if (isset($_GET['mode']) && $_GET['mode'] === 'usage-calibration') { car_monitor_require_access(true); - car_monitor_require_admin(); + car_monitor_require_settings_access(); try { $pdo = car_db(); @@ -2501,6 +2494,7 @@ $carMonitorSession = car_monitor_launcher_session(); let latestUsageData = null; let calibrationModalOpen = false; let calibrationSaveInFlight = false; + let calibrationSettingsAuthenticated = false; const FALLBACK_BACK_TARGET = 'https://chaegeon.com/'; const BACK_HISTORY_KEY = 'chaegeon.backHistory.v1'; const BACK_LINK_PATH_TITLE = { @@ -2995,7 +2989,7 @@ $carMonitorSession = car_monitor_launcher_session(); function showCalibrationSettings() { const btn = document.getElementById('settings-btn'); - if (!btn || !CAR_MONITOR_SESSION?.admin) return; + if (!btn) return; btn.classList.remove('d-none'); if (btn.dataset.ready === '1') return; @@ -3011,34 +3005,88 @@ $carMonitorSession = car_monitor_launcher_session(); }); } - async function initCalibrationSettings() { - if (CAR_MONITOR_SESSION?.admin) { - showCalibrationSettings(); - return; - } - - try { - const res = await fetch('https://chaegeon.com/custom/launcher/api.php?adminSession=1', { - credentials: 'include', - cache: 'no-store', - }); - const data = await res.json(); - if (res.ok && data?.authenticated && data?.admin && data?.assertion) { - const localRes = await fetch('?mode=admin-assert', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - cache: 'no-store', - body: JSON.stringify({ assertion: data.assertion }), - }); - if (!localRes.ok) return; - - CAR_MONITOR_SESSION = { ...CAR_MONITOR_SESSION, authenticated: true, admin: true }; - showCalibrationSettings(); - } - } catch (_) {} + function initCalibrationSettings() { + showCalibrationSettings(); } - function openCalibrationModal() { + async function ensureCalibrationSettingsAuth() { + if (calibrationSettingsAuthenticated) return true; + + const password = await promptCalibrationSettingsPassword(); + if (password === null) return false; + + try { + const res = await fetch('?mode=settings-auth', { + method: 'POST', + cache: 'no-store', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password }), + }); + const payload = await res.json(); + if (!res.ok || payload.status !== 'success') { + alert(payload.message || '관리자 비밀번호가 올바르지 않습니다.'); + return false; + } + calibrationSettingsAuthenticated = true; + return true; + } catch (_) { + alert('관리자 인증에 실패했습니다.'); + return false; + } + } + + function promptCalibrationSettingsPassword() { + return new Promise(resolve => { + const overlay = document.createElement('div'); + overlay.className = 'custom-modal-overlay'; + overlay.innerHTML = ` +