(function () { const TOKEN_KEYS = { access: 'access_token', refresh: 'refresh_token', preAuth: 'pre_auth_token', }; function isRotateMode() { return new URLSearchParams(window.location.search).get('rotate') === '1'; } function getReturnUrl() { const params = new URLSearchParams(window.location.search); const next = params.get('next'); if (next && next.startsWith('/') && !next.startsWith('//')) { return next; } return '/'; } function getPreAuthToken() { return sessionStorage.getItem(TOKEN_KEYS.preAuth); } function getAccessToken() { return localStorage.getItem(TOKEN_KEYS.access); } function getAuthToken() { return isRotateMode() ? getAccessToken() : getPreAuthToken(); } function setTokens(accessToken, refreshToken) { sessionStorage.removeItem(TOKEN_KEYS.preAuth); localStorage.setItem(TOKEN_KEYS.access, accessToken); if (refreshToken) { localStorage.setItem(TOKEN_KEYS.refresh, refreshToken); } } function applyPageMode() { if (!isRotateMode()) return; document.getElementById('setupTitle').textContent = 'Google OTP 재등록'; document.getElementById('setupSubtitle').textContent = '새 기기·앱으로 다시 연결합니다. 등록 완료 후 이전 OTP 코드는 사용할 수 없습니다.'; document.getElementById('setupConfirmBtn').textContent = '재등록 완료'; } function showError(message) { const el = document.getElementById('setupError'); if (!message) { el.classList.add('hidden'); el.textContent = ''; return; } el.textContent = message; el.classList.remove('hidden'); } function showScanStep(data) { document.getElementById('setupLoading').classList.add('hidden'); document.getElementById('setupStepScan').classList.remove('hidden'); document.getElementById('setupSecret').textContent = data.secret; const qr = document.getElementById('setupQr'); if (data.qr_data_url) { qr.src = data.qr_data_url; } else { qr.src = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(data.provisioning_uri)}`; } document.getElementById('setupOtpCode').focus(); } function loginNextUrl() { const suffix = isRotateMode() ? '?rotate=1' : ''; return `/setup-2fa${suffix}`; } async function beginSetup() { const rotate = isRotateMode(); const token = getAuthToken(); if (!token) { window.location.href = `/login?next=${encodeURIComponent(loginNextUrl())}`; return; } const url = rotate ? '/api/v1/auth/setup-2fa' : '/api/v1/auth/setup-2fa/begin'; const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, }); const data = await res.json(); if (!res.ok) { const detail = data.detail || ''; if (res.status === 401) { if (rotate) { localStorage.removeItem(TOKEN_KEYS.access); localStorage.removeItem(TOKEN_KEYS.refresh); } else { sessionStorage.removeItem(TOKEN_KEYS.preAuth); localStorage.removeItem(TOKEN_KEYS.access); localStorage.removeItem(TOKEN_KEYS.refresh); } window.location.href = `/login?next=${encodeURIComponent(loginNextUrl())}`; return; } showError(detail || 'OTP 설정을 시작할 수 없습니다. 다시 로그인하세요.'); return; } showScanStep(data); } async function confirmSetup() { const code = document.getElementById('setupOtpCode').value.trim(); if (code.length < 6) { showError('인증 코드 6자리를 입력하세요.'); return; } showError(''); const rotate = isRotateMode(); const token = getAuthToken(); const url = rotate ? '/api/v1/auth/setup-2fa/activate' : '/api/v1/auth/setup-2fa/confirm'; const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, body: JSON.stringify({ code }), }); const data = await res.json(); if (!res.ok) { showError( data.detail === 'Invalid OTP' ? 'OTP 코드가 올바르지 않습니다.' : (data.detail || '등록에 실패했습니다.'), ); return; } setTokens(data.access_token, data.refresh_token); window.location.href = getReturnUrl(); } applyPageMode(); document.getElementById('setupConfirmBtn').addEventListener('click', confirmSetup); document.getElementById('setupOtpCode').addEventListener('keydown', (e) => { if (e.key === 'Enter') confirmSetup(); }); beginSetup().catch(() => { showError('네트워크 오류가 발생했습니다.'); }); })();