/* 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) => `
인증 앱에서 아래 주소를 등록하거나 수동 입력 키를 넣으세요.
${aw.esc(r.otpauth)}
수동 입력 키: ${aw.esc(r.secret)}
`; $('#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) + '완료'; } 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 = `| 기기 | IP | 로그인 시각 | 마지막 사용 | |
|---|---|---|---|---|
| ${aw.esc(s.label || '기기')}${s.id === r.current ? ' ● 현재 세션' : ''} | ${aw.esc(s.ip || '')} | ${aw.esc(aw.fmt(s.created_at))} | ${aw.esc(aw.fmt(s.last_seen_at))} | ${s.id === r.current ? '' : ``} |