차량 모니터 설정 인증 분리
This commit is contained in:
1 parent
f69b9febf4
commit
c3a3e29d31
1 file changed
+130
-82
+130
-82
@@ -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();
|
||||
}
|
||||
|
||||
return ($payload['aud'] ?? '') === 'car-monitor'
|
||||
&& !empty($payload['admin'])
|
||||
&& (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
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in new issue
Block a user