diff --git a/api.php b/api.php index c33e18b..ffac195 100644 --- a/api.php +++ b/api.php @@ -52,7 +52,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } if ($action === 'logout') { - custom_clear_auth_cookie(); + $scope = (string)($data['scope'] ?? ''); + if ($scope === 'admin') { + custom_clear_auth_cookie(); + } elseif (function_exists('custom_clear_non_admin_auth_cookie')) { + custom_clear_non_admin_auth_cookie(false); + } else { + custom_clear_auth_cookie(); + } custom_json(['result' => 'ok']); } @@ -93,12 +100,20 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'GET') { if (isset($_GET['session'])) { $denied = custom_login_denied_state(); - if (!empty($denied['denied'])) { + $blockedType = (string)($denied['type'] ?? ''); + if (!empty($denied['denied']) && !($blockedType !== 'ip' && custom_is_site_admin())) { + if ($blockedType === 'ip') { + custom_clear_auth_cookie(); + } elseif (function_exists('custom_clear_non_admin_auth_cookie')) { + custom_clear_non_admin_auth_cookie(true); + } else { + custom_clear_auth_cookie(); + } custom_json([ 'authenticated' => false, 'admin' => false, 'loginBlocked' => true, - 'blockedType' => $denied['type'] ?? '', + 'blockedType' => $blockedType, 'blockedMessage' => $denied['message'] ?? '현재 로그인할 수 없는 상태입니다.', ]); } diff --git a/js/main.js b/js/main.js index 33ca784..17b80f6 100644 --- a/js/main.js +++ b/js/main.js @@ -5,12 +5,91 @@ let lastRenderedAdmin = null; const apiUrl = '/custom/launcher/api.php'; const PIN_LENGTH = 4; +function isClientLoginBlocked() { + try { + return sessionStorage.getItem('login_blocked') === '1'; + } catch (_) { + return false; + } +} + +const LAUNCHER_LOGIN_BLOCKED_COOKIE_NAMES = { + member: 'chaegeon_member_blocked', + ip: 'chaegeon_ip_blocked', + login_fail: 'chaegeon_login_fail_blocked', + sms_verify_fail: 'chaegeon_sms_verify_blocked', + client: 'chaegeon_member_blocked' +}; +const LAUNCHER_LOGIN_BLOCKED_LEGACY_COOKIE = 'chaegeon_login_blocked'; + +function clientLoginBlockedCookieName(type) { + return LAUNCHER_LOGIN_BLOCKED_COOKIE_NAMES[type || 'member'] || LAUNCHER_LOGIN_BLOCKED_COOKIE_NAMES.member; +} + +function writeClientLoginBlockedCookie(name, blocked) { + const secure = location.protocol === 'https:' ? '; Secure' : ''; + document.cookie = `${name}=${blocked ? '1' : ''}; path=/; max-age=${blocked ? '1800' : '0'}; SameSite=Strict${secure}`; +} + +function clearClientLoginBlockedCookies(exceptName) { + [ + LAUNCHER_LOGIN_BLOCKED_LEGACY_COOKIE, + LAUNCHER_LOGIN_BLOCKED_COOKIE_NAMES.member, + LAUNCHER_LOGIN_BLOCKED_COOKIE_NAMES.ip, + LAUNCHER_LOGIN_BLOCKED_COOKIE_NAMES.login_fail, + LAUNCHER_LOGIN_BLOCKED_COOKIE_NAMES.sms_verify_fail + ].forEach(name => { + if (name !== exceptName) writeClientLoginBlockedCookie(name, false); + }); +} + +function setClientLoginBlockedCookie(type = 'member') { + const name = clientLoginBlockedCookieName(type); + clearClientLoginBlockedCookies(name); + writeClientLoginBlockedCookie(name, true); +} + +function rememberClientLoginBlocked(type, message) { + try { + sessionStorage.setItem('login_blocked', '1'); + sessionStorage.setItem('login_blocked_type', type || 'member'); + if (message) sessionStorage.setItem('login_blocked_reason', message); + } catch (_) {} + setClientLoginBlockedCookie(type || 'member'); +} + +function isLauncherLoginBlockedAllowedHomePage() { + const path = location.pathname.toLowerCase(); + return location.protocol === 'https:' && location.hostname === 'chaegeon.com' && (path === '/' || path === '/home' || path.startsWith('/home/')); +} + +function replaceLauncherToChaegeonHome() { + if (isLauncherLoginBlockedAllowedHomePage()) return false; + location.replace('https://chaegeon.com/'); + return true; +} + +function redirectIfClientLoginBlocked() { + if (!isClientLoginBlocked()) return false; + let type = 'member'; + try { + type = sessionStorage.getItem('login_blocked_type') || 'member'; + } catch (_) {} + setClientLoginBlockedCookie(type); + replaceLauncherToChaegeonHome(); + return true; +} + +function openLauncherTarget(url) { + window.open(url, '_blank', 'noopener'); +} + const userIconItems = [ { id: 'findmydevice', img: '/custom/launcher/img/find-my.svg', label: '나의 찾기', - onclick: () => requireAccess(() => location.href = '/findmydevice') + onclick: () => requireAccess(() => openLauncherTarget('/findmydevice')) }, { id: 'poke', @@ -22,13 +101,13 @@ const userIconItems = [ id: 'car', img: '/custom/launcher/img/car.png', label: '니모', - onclick: () => requireAccess(() => location.href = 'https://seo.chaegeon.com/car/monitor.php') + onclick: () => requireAccess(() => openLauncherTarget('https://chaegeon.com/custom/car/monitor.php')) }, { id: 'project', img: 'https://git.chaegeon.com/assets/img/favicon.svg', label: '프로젝트', - onclick: () => location.href = 'https://git.chaegeon.com/explore/repos' + onclick: () => openLauncherTarget('https://git.chaegeon.com/explore/repos') } ]; @@ -47,26 +126,29 @@ const adminIconItems = [ id: 'block', img: '/admin/favicon/block/favicon.svg', label: 'Block', - onclick: () => requireAccess(() => location.href = 'https://chaegeon.com/admin/index.php') + onclick: () => requireAccess(() => openLauncherTarget('https://chaegeon.com/admin/index.php')) }, { id: 'notice', img: '/admin/favicon/notice/favicon.svg', label: 'Notice', - onclick: () => requireAccess(() => location.href = 'https://chaegeon.com/admin/notice.php') + onclick: () => requireAccess(() => openLauncherTarget('https://chaegeon.com/admin/notice.php')) }, { id: 'speedstats', img: '/custom/speedtest/favicon.ico', label: 'Speed Stats', - onclick: () => requireAccess(() => location.href = 'https://chaegeon.com/custom/speedtest/results/stats.php') + onclick: () => requireAccess(() => openLauncherTarget('https://chaegeon.com/custom/speedtest/results/stats.php')) } ]; async function renderIcons() { + if (redirectIfClientLoginBlocked()) return; + const session = await getSession(); if (session.loginBlocked) { - renderLoginBlocked(session); + rememberClientLoginBlocked(session.blockedType, session.blockedMessage); + replaceLauncherToChaegeonHome(); return; } @@ -125,10 +207,13 @@ async function getSession() { } async function requireAccess(nextAction) { + if (redirectIfClientLoginBlocked()) return; + try { const session = await getSession(); if (session.loginBlocked) { - renderLoginBlocked(session); + rememberClientLoginBlocked(session.blockedType, session.blockedMessage); + replaceLauncherToChaegeonHome(); return; } @@ -142,6 +227,13 @@ async function requireAccess(nextAction) { showPasswordForm(nextAction); } +async function finishPasswordSuccess(onSuccess) { + if (typeof onSuccess === 'function') { + onSuccess(); + } + await renderIcons(); +} + async function refreshAdminModeIfNeeded() { if (lastRenderedAdmin !== true) return; const isAdmin = await hasAdminSession(); @@ -172,6 +264,8 @@ function renderLoginBlocked(session = {}) { } function showPasswordForm(onSuccess) { + if (redirectIfClientLoginBlocked()) return; + shortcut.className = 'password-container'; shortcut.innerHTML = ''; @@ -190,12 +284,16 @@ function showPasswordForm(onSuccess) { column.appendChild(inputRow); shortcut.appendChild(column); + let submitting = false; const submit = async () => { + if (redirectIfClientLoginBlocked()) return; + if (submitting) return; const password = onlyDigits(input.value); input.value = password; if (password.length !== PIN_LENGTH) return; try { + submitting = true; const messageRes = await fetch(`${apiUrl}?failMessages=1`); const messagesJson = await messageRes.json(); const failMessages = messagesJson.failMessages || []; @@ -208,12 +306,14 @@ function showPasswordForm(onSuccess) { }); const data = await res.json(); + if (data.loginBlocked) { + rememberClientLoginBlocked(data.blockedType, data.blockedMessage); + replaceLauncherToChaegeonHome(); + return; + } + if (res.ok && data.result === 'ok') { - if (typeof onSuccess === 'function') { - onSuccess(); - } else { - renderIcons(); - } + await finishPasswordSuccess(onSuccess); return; } @@ -222,9 +322,11 @@ function showPasswordForm(onSuccess) { failIndex = (failIndex + 1) % Math.max(1, failMessages.length); input.value = ''; input.focus(); + submitting = false; } catch (_) { column.appendChild(msg); msg.textContent = '요청을 처리하지 못했습니다.'; + submitting = false; } };