차량 모니터 설정 인증 분리

This commit is contained in:
seo committed 2026-07-29 19:13:11 +09:00
1 parent f69b9febf4
commit c3a3e29d31
1 file changed
+130 -82
+130 -82
View File
@@ -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) {
function initCalibrationSettings() {
showCalibrationSettings();
return;
}
async function ensureCalibrationSettingsAuth() {
if (calibrationSettingsAuthenticated) return true;
const password = await promptCalibrationSettingsPassword();
if (password === null) return false;
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', {
const res = await fetch('?mode=settings-auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
cache: 'no-store',
body: JSON.stringify({ assertion: data.assertion }),
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password }),
});
if (!localRes.ok) return;
CAR_MONITOR_SESSION = { ...CAR_MONITOR_SESSION, authenticated: true, admin: true };
showCalibrationSettings();
const payload = await res.json();
if (!res.ok || payload.status !== 'success') {
alert(payload.message || '관리자 비밀번호가 올바르지 않습니다.');
return false;
}
calibrationSettingsAuthenticated = true;
return true;
} catch (_) {
alert('관리자 인증에 실패했습니다.');
return false;
}
} catch (_) {}
}
function openCalibrationModal() {
function promptCalibrationSettingsPassword() {
return new Promise(resolve => {
const overlay = document.createElement('div');
overlay.className = 'custom-modal-overlay';
overlay.innerHTML = `
<div class="custom-modal-panel" role="dialog" aria-modal="true" aria-labelledby="settings-auth-title">
<div class="custom-modal-header">
<div>
<div id="settings-auth-title" class="fw-bold">관리자 확인</div>
<div class="small text-muted">설정 변경 권한을 확인합니다.</div>
</div>
<button type="button" class="btn-icon-toggle" data-settings-auth-cancel title="닫기">
<i class="fas fa-xmark"></i>
</button>
</div>
<div class="custom-modal-body">
<div class="calibration-field">
<label for="settings-auth-password">관리자 비밀번호</label>
<input id="settings-auth-password" type="password" autocomplete="current-password" autofocus>
</div>
<div class="calibration-message text-muted" data-settings-auth-message></div>
</div>
<div class="custom-modal-footer">
<button type="button" class="btn btn-light rounded-pill px-4" data-settings-auth-cancel>닫기</button>
<button type="button" class="btn btn-primary rounded-pill px-4" data-settings-auth-ok>확인</button>
</div>
</div>
`;
const done = value => {
document.body.style.overflow = '';
overlay.remove();
resolve(value);
};
const input = overlay.querySelector('#settings-auth-password');
overlay.querySelectorAll('[data-settings-auth-cancel]').forEach(btn => {
btn.addEventListener('click', () => done(null));
});
overlay.querySelector('[data-settings-auth-ok]')?.addEventListener('click', () => done(input?.value || ''));
input?.addEventListener('keydown', event => {
if (event.key === 'Enter') done(input.value || '');
if (event.key === 'Escape') done(null);
});
document.body.appendChild(overlay);
document.body.style.overflow = 'hidden';
setTimeout(() => input?.focus(), 30);
});
}
async function openCalibrationModal() {
if (!await ensureCalibrationSettingsAuth()) return;
const overlay = document.getElementById('calibration-overlay');
if (!overlay) return;