29 lines
7.2 KiB
JavaScript
29 lines
7.2 KiB
JavaScript
/* portal/auth/public/auth.js — 로그인·가입·확인·재설정·내 계정·보안·기기 화면 동작. 서버 응답 message 를 그대로 보이고 오류는 입력 아래에 표시한다. */
|
|||
|
|
(() => {
|
||
|
|
const $ = (s, r = document) => r.querySelector(s); const val = (form, n) => (form.elements[n] ? form.elements[n].value : '');
|
||
|
|
const showErr = (id, e) => { const el = $(id); if (el) el.textContent = e.message || String(e); };
|
||
|
|
const login = $('#login'); const mfa = $('#mfa'); let preauth = null;
|
||
|
|
if (login) login.onsubmit = async (e) => { e.preventDefault(); showErr('#login-err', ''); try { const r = await aw.api('POST', '/api/auth/login', { email: val(login, 'email'), password: val(login, 'password'), next: login.dataset.next }); if (r.mfaRequired) { preauth = r.preauth; login.hidden = true; mfa.hidden = false; mfa.elements.code.focus(); } else location.href = r.next || '/'; } catch (err) { showErr('#login-err', err); if (err.detail && err.detail.lockUntil) login.querySelector('button').disabled = true; } };
|
||
|
|
if (mfa) mfa.onsubmit = async (e) => { e.preventDefault(); showErr('#mfa-err', ''); try { const r = await aw.api('POST', '/api/auth/mfa', { preauth, code: val(mfa, 'code') }); if (r.recoveryLeft != null) alert(`복구 코드로 로그인했습니다. 남은 복구 코드 ${r.recoveryLeft}개`); location.href = r.next || '/'; } catch (err) { showErr('#mfa-err', err); if (/preauth_/.test(err.code)) { mfa.hidden = true; login.hidden = false; showErr('#login-err', err); } } };
|
||
|
|
const signup = $('#signup');
|
||
|
|
if (signup) signup.onsubmit = async (e) => { e.preventDefault(); showErr('#signup-err', ''); try { const r = await aw.api('POST', '/api/auth/signup', { email: val(signup, 'email'), displayName: val(signup, 'displayName'), password: val(signup, 'password'), consents: { terms: signup.elements.terms.checked, privacy: signup.elements.privacy.checked, marketing: signup.elements.marketing.checked } }); const d = $('#signup-done'); d.textContent = r.message; d.hidden = false; signup.hidden = true; setTimeout(() => { location.href = r.next || '/'; }, 1500); } catch (err) { showErr('#signup-err', err); } };
|
||
|
|
document.addEventListener('click', async (e) => { if (e.target.matches('[data-action=resend-verify]')) { try { const r = await aw.api('POST', '/api/auth/resend-verification'); aw.toast(r.message || '보냈습니다'); } catch (err) { aw.toast(err.message); } } });
|
||
|
|
const rr = $('#reset-request');
|
||
|
|
if (rr) rr.onsubmit = async (e) => { e.preventDefault(); try { const r = await aw.api('POST', '/api/auth/request-reset', { email: val(rr, 'email') }); const d = $('#rr-done'); d.textContent = r.message; d.hidden = false; rr.hidden = true; } catch (err) { showErr('#rr-err', err); } };
|
||
|
|
const reset = $('#reset');
|
||
|
|
if (reset) reset.onsubmit = async (e) => { e.preventDefault(); try { const r = await aw.api('POST', '/api/auth/reset', { token: reset.dataset.token, password: val(reset, 'password') }); const d = $('#reset-done'); d.textContent = r.message; d.hidden = false; reset.hidden = true; setTimeout(() => { location.href = '/login?notice=reset'; }, 1500); } catch (err) { showErr('#reset-err', err); } };
|
||
|
|
const profile = $('#profile');
|
||
|
|
if (profile) profile.onsubmit = async (e) => { e.preventDefault(); try { const r = await aw.api('PATCH', '/api/account/profile', { displayName: val(profile, 'displayName') }); aw.markClean(); aw.toast(r.message); } catch (err) { aw.toast(err.message); } };
|
||
|
|
const pw = $('#password');
|
||
|
|
if (pw) pw.onsubmit = async (e) => { e.preventDefault(); showErr('#pw-err', ''); try { const r = await aw.api('POST', '/api/account/password', { current: val(pw, 'current'), next: val(pw, 'next') }); pw.reset(); aw.toast(r.message); } catch (err) { showErr('#pw-err', err); } };
|
||
|
|
const codesHtml = (r) => `<div class="alert warning"><b>${aw.esc(r.message)}</b><ul class="mono">${r.recoveryCodes.map((c) => `<li>${aw.esc(c)}</li>`).join('')}</ul><button class="btn copy" data-action="copy" data-copy="${aw.esc(r.recoveryCodes.join('\n'))}">복사</button></div>`;
|
||
|
|
document.addEventListener('click', async (e) => { if (!e.target.matches('[data-action=mfa-setup]')) return; try { const r = await aw.api('POST', '/api/account/mfa/setup'); $('#mfa-setup').innerHTML = `<p>인증 앱에서 아래 주소를 등록하거나 수동 입력 키를 넣으세요.</p><p class="mono" style="word-break:break-all">${aw.esc(r.otpauth)}</p><p><b>수동 입력 키:</b> <span class="mono copy" data-action="copy" data-copy="${aw.esc(r.secret.replace(/ /g, ''))}">${aw.esc(r.secret)}</span></p><form id="mfa-enable"><div class="f"><label for="mcode">인증 앱의 6자리 코드</label><input id="mcode" name="code" inputmode="numeric" required></div><p class="err" id="mfae-err"></p><button class="btn primary">코드 확인 후 켜기</button></form><div id="mfa-codes"></div>`; $('#mfa-enable').onsubmit = async (ev) => { ev.preventDefault(); try { const rr2 = await aw.api('POST', '/api/account/mfa/enable', { code: $('#mcode').value }); $('#mfa-enable').hidden = true; $('#mfa-codes').innerHTML = codesHtml(rr2) + '<a class="btn" href="/account/security">완료</a>'; } catch (err) { showErr('#mfae-err', err); } }; } catch (err) { aw.toast(err.message); } });
|
||
|
|
const md = $('#mfa-disable');
|
||
|
|
if (md) md.onsubmit = async (e) => { e.preventDefault(); try { await aw.api('POST', '/api/account/mfa/disable', { password: val(md, 'password'), code: val(md, 'code') }); location.reload(); } catch (err) { showErr('#mfad-err', err); } };
|
||
|
|
const mr = $('#mfa-regen');
|
||
|
|
if (mr) mr.onsubmit = async (e) => { e.preventDefault(); try { const r = await aw.api('POST', '/api/account/mfa/recovery-codes', { password: val(mr, 'password2') }); $('#recovery-out').innerHTML = codesHtml(r); mr.reset(); } catch (err) { aw.toast(err.message); } };
|
||
|
|
const sess = $('#sessions');
|
||
|
|
async function loadSessions() { try { const r = await aw.api('GET', '/api/account/sessions'); if (!r.sessions.length) return aw.state(sess, 'empty'); sess.innerHTML = `<table class="r"><thead><tr><th>기기</th><th>IP</th><th>로그인 시각</th><th>마지막 사용</th><th></th></tr></thead><tbody>${r.sessions.map((s) => `<tr><td data-l="기기">${aw.esc(s.label || '기기')}${s.id === r.current ? ' <span class="badge ok">● 현재 세션</span>' : ''}</td><td data-l="IP">${aw.esc(s.ip || '')}</td><td data-l="로그인 시각">${aw.esc(aw.fmt(s.created_at))}</td><td data-l="마지막 사용">${aw.esc(aw.fmt(s.last_seen_at))}</td><td class="right">${s.id === r.current ? '' : `<button class="btn danger" data-rev="${aw.esc(s.id)}">종료</button>`}</td></tr>`).join('')}</tbody></table>`; sess.querySelectorAll('[data-rev]').forEach((b) => { b.onclick = async () => { await aw.api('DELETE', '/api/account/sessions/' + b.dataset.rev); loadSessions(); }; }); } catch (err) { aw.state(sess, err.status === 403 ? 'forbidden' : 'failed', err.message, { retry: loadSessions }); } }
|
||
|
|
if (sess) { loadSessions(); document.addEventListener('click', async (e) => { if (e.target.matches('[data-action=revoke-others]')) { if (await aw.confirmDanger({ title: '다른 모든 세션 종료', target: '이 기기를 제외한 모든 로그인', impacts: ['다른 기기에서는 다시 로그인해야 합니다'] })) { await aw.api('POST', '/api/account/sessions/revoke-others'); loadSessions(); } } }); }
|
||
|
|
})();
|