diff --git a/README.md b/README.md index f941fc9..23c5a27 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,7 @@ Worker 교체나 예외 종료로 `processing`, `ui_timeout_pending` 상태가 - 차량 API는 API token 또는 허용 IP 정책을 사용합니다. - 차량 모니터 화면과 `monitor.php?mode=ajax`, `monitor.php?mode=usage`는 런처 공통 인증 쿠키 또는 사이트 관리자 세션이 유효할 때만 응답합니다. +- `monitor.php?mode=usage-calibration`은 사이트 관리자 세션이 확인될 때만 응답합니다. 일반 PIN 쿠키로 들어온 사용자는 화면에서 설정 아이콘이 보이지 않고, 직접 API를 호출해도 차단됩니다. - 공통 암호로 로그인한 일반 인증은 30분 유지하고, 사이트 관리자 세션이 확인되면 관리자 장기 쿠키를 발급해 PWA에서 반복 인증이 뜨는 일을 줄입니다. - PWA로 설치한 차량 모니터는 manifest 시작 URL `/car/monitor.php?pwa=1`로 진입합니다. 이 요청은 기존 공통 인증 쿠키가 있으면 유지하고, 사이트 관리자 세션이면 관리자 쿠키를 발급한 뒤 `/car/monitor.php`로 되돌아가므로 설치형 앱 진입 때 반복 PIN 화면이 뜨는 일을 줄입니다. - 런처 공통 인증은 우선 공통 인증 모듈로 직접 확인합니다. 공통 모듈을 사용할 수 없는 경우에만 `https://chaegeon.com/custom/launcher/api.php?session=1` 응답으로 확인합니다. @@ -139,7 +140,7 @@ Worker 교체나 예외 종료로 `processing`, `ui_timeout_pending` 상태가 - 명령별 rate limit과 감사 로그를 유지합니다. - 통신사 기준 데이터 사용량 보정값을 주기적으로 확인합니다. 2026-06-30 15:15 T world 실측 114.5MB(117,250.5KB) 기준 2026-06 보정값은 통신사 누적 120,064,512 bytes, 통신사 과금 단위 추정 누적 129,148,416 bytes, 배율 0.929663입니다. - 2026-07-02 12:42 T world 실측 6.25MB(6,396KB) 기준 2026-07 보정값은 통신사 누적 6,549,504 bytes, 통신사 과금 단위 추정 누적 7,169,536 bytes, 배율 0.913519입니다. -- 관리자 인증 상태에서는 상단 우측 설정 아이콘으로 T world 사용량 기준점을 저장할 수 있습니다. 입력값은 `car_usage_calibrations` 테이블에 저장되고, 같은 월에서는 가장 최근 기준 시각의 값을 우선 사용합니다. +- 관리자 인증 상태에서는 상단 우측 설정 아이콘으로 T world 사용량 기준점을 저장할 수 있습니다. 설정 아이콘은 일반 PIN 인증 상태에서는 숨겨지고, 관리자 상태에서는 별도 비밀번호 재입력 없이 바로 열립니다. 입력값은 `car_usage_calibrations` 테이블에 저장되고, 같은 월에서는 가장 최근 기준 시각의 값을 우선 사용합니다. - 해당 월의 T world 기준점이 DB나 코드 기준점에 있으면 그 월 배율을 우선 사용하고, 아직 기준점이 없는 새 달은 가장 최근 월 배율을 임시로 사용합니다. 새 기준점이 기록되면 해당 월 배율로 다시 계산합니다. - 2026-06 기준 시각의 로컬 계측 범위는 2026-06-01 00:00:00부터 2026-06-30 15:15:00 직전까지이며, 마지막 로컬 사용량 기록은 2026-06-30 15:14:59입니다. - 데이터 사용량은 TCP payload 합계도 보존하지만 화면의 현재 사용량, 잔여 사용량, 예상 사용량, 예상 요금은 0.5KB 과금 단위로 올림 처리한 통신사형 누적값을 우선 사용합니다. 순수 연결 실패와 마지막 성공 이후 60초를 넘긴 수신 공백은 기존처럼 제외합니다. diff --git a/monitor.php b/monitor.php index f38ad5d..18cf741 100644 --- a/monitor.php +++ b/monitor.php @@ -21,10 +21,7 @@ define('DB_CHARSET', (string)($carDbConfig['charset'] ?? 'utf8mb4')); const LOG_LIMIT = 300; const CHART_LIMIT = 500; -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_MONTHLY_INCLUDED_BYTES = 104857600; // 100MB const DATA_BILLING_UNIT_BYTES = 512; // 0.5KB const DATA_BILLING_UNIT_KRW = 0.011; const DATA_MONTHLY_BASE_FEE_KRW = 5500; @@ -862,79 +859,6 @@ function json_response(array $payload, int $status = 200): void exit; } -function car_monitor_cookie_secret(): string -{ - global $carSecretConfig; - - $secret = (string)($carSecretConfig['monitor_auth']['secret'] ?? $carSecretConfig['auth_token'] ?? ''); - if ($secret === '') { - throw new RuntimeException('Car monitor auth secret is missing.'); - } - - return $secret; -} - -function car_monitor_cookie_options(int $expires): array -{ - return [ - 'expires' => $expires, - 'path' => '/custom/car/', - 'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'), - 'httponly' => true, - 'samesite' => 'Strict', - ]; -} - -function car_monitor_settings_cookie_value(int $expires): string -{ - $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('.', $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; - 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; - } - - json_response([ - 'status' => 'forbidden', - 'message' => '관리자 비밀번호가 필요합니다.', - ], 403); -} - function car_monitor_launcher_session(): array { static $session = null; @@ -943,8 +867,7 @@ function car_monitor_launcher_session(): array return $session; } - if (custom_is_site_admin()) { - custom_issue_auth_cookie(true); + if (custom_has_admin_auth()) { $session = ['authenticated' => true, 'admin' => true]; return $session; } @@ -961,6 +884,23 @@ function car_monitor_launcher_session(): array return $session; } +function car_monitor_require_admin_access(): void +{ + if (function_exists('custom_block_login_denied')) { + custom_block_login_denied(true); + } + + $session = car_monitor_launcher_session(); + if (!empty($session['admin'])) { + return; + } + + json_response([ + 'status' => 'forbidden', + 'message' => '관리자 권한이 필요합니다.', + ], 403); +} + function car_monitor_has_access(): bool { $session = car_monitor_launcher_session(); @@ -1063,40 +1003,11 @@ function car_monitor_auth_api(): void json_response(['result' => 'fail'], 401); } -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(); - $password = (string)($data['password'] ?? ''); - if (!hash_equals(CAR_MONITOR_SETTINGS_PASSWORD, $password)) { - json_response([ - 'status' => 'forbidden', - 'message' => '관리자 비밀번호가 올바르지 않습니다.', - ], 403); - } - - car_monitor_issue_settings_cookie(); - json_response([ - 'status' => 'success', - 'settings_authenticated' => true, - 'expires_in' => CAR_MONITOR_SETTINGS_TTL, - ]); -} - -if (isset($_GET['mode']) && $_GET['mode'] === 'auth-login') { - car_monitor_auth_api(); -} - -if (isset($_GET['mode']) && $_GET['mode'] === 'settings-auth') { - car_monitor_settings_auth_api(); -} - -car_monitor_issue_pwa_cookie_if_requested(); +if (isset($_GET['mode']) && $_GET['mode'] === 'auth-login') { + car_monitor_auth_api(); +} + +car_monitor_issue_pwa_cookie_if_requested(); if (isset($_GET['mode']) && $_GET['mode'] === 'ajax') { car_monitor_require_access(true); @@ -1200,11 +1111,11 @@ if (isset($_GET['mode']) && $_GET['mode'] === 'usage') { } } -if (isset($_GET['mode']) && $_GET['mode'] === 'usage-calibration') { - car_monitor_require_access(true); - car_monitor_require_settings_access(); - - try { +if (isset($_GET['mode']) && $_GET['mode'] === 'usage-calibration') { + car_monitor_require_access(true); + car_monitor_require_admin_access(); + + try { $pdo = car_db(); if ($_SERVER['REQUEST_METHOD'] === 'POST') { $saved = save_usage_calibration($pdo, request_payload()); @@ -2388,12 +2299,11 @@ $carMonitorSession = car_monitor_launcher_session(); let wakeLockSentinel = null; let wakeLockWanted = false; let latestDashboardData = null; - let latestDashboardMeta = null; - let latestUsageData = null; - let calibrationModalOpen = false; - let calibrationSaveInFlight = false; - let calibrationSettingsAuthenticated = false; - const FALLBACK_BACK_TARGET = 'https://chaegeon.com/'; + let latestDashboardMeta = null; + let latestUsageData = null; + let calibrationModalOpen = false; + let calibrationSaveInFlight = false; + const FALLBACK_BACK_TARGET = 'https://chaegeon.com/'; const BACK_HISTORY_KEY = 'chaegeon.backHistory.v1'; const BACK_LINK_PATH_TITLE = { '/': '채건닷컴', @@ -2903,90 +2813,21 @@ $carMonitorSession = car_monitor_launcher_session(); }); } - function initCalibrationSettings() { - showCalibrationSettings(); - } - - 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 = ` -