Files

62 lines
9.4 KiB
JavaScript

/**
* portal/auth/pages.mjs — 인증·계정 화면 HTML(서버 렌더). 데이터·동작은 /ui/auth/auth.js 가 /api/auth·/api/account 를 불러 처리한다.
* 로그인 전 화면(로그인·가입·확인·재설정)은 공통 틀의 비로그인 모드로, 로그인 뒤 화면(내 계정·보안·기기)은 왼쪽 메뉴가 있는 모드로 그린다.
*/
import { esc } from '../../lib/http.mjs';
import { t } from '../../lib/i18n.mjs';
const box = (title, inner, foot = '') => `<div class="card" style="max-width:460px;margin:24px auto"><h2>${esc(title)}</h2>${inner}${foot ? `<p class="sub" style="margin-top:12px">${foot}</p>` : ''}</div>`;
const f = (id, label, type = 'text', extra = '', hint = '') => `<div class="f"><label for="${id}">${esc(label)}</label><input id="${id}" name="${id}" type="${type}" ${extra}>${hint ? `<span class="hint">${esc(hint)}</span>` : ''}<span class="err" data-err="${id}"></span></div>`;
export function loginPage(shell, { next = '/', notice = '' } = {}) {
return shell({ title: t('auth.login.title'), nav: null, scripts: ['/ui/auth/auth.js'], body: box(t('auth.login.title'), `
${notice ? `<div class="alert info">${esc(notice)}</div>` : ''}
<form id="login" data-next="${esc(next)}">${f('email', t('auth.login.email'), 'email', 'autocomplete="username" required')}${f('password', t('auth.login.password'), 'password', 'autocomplete="current-password" required')}
<p class="err" id="login-err" role="alert"></p><button class="btn primary" type="submit">${esc(t('auth.login.submit'))}</button></form>
<form id="mfa" hidden><p>${esc(t('auth.login.mfaHint'))}</p>${f('code', t('auth.login.mfaTitle'), 'text', 'inputmode="numeric" autocomplete="one-time-code" required')}<p class="err" id="mfa-err" role="alert"></p><button class="btn primary" type="submit">${esc(t('common.confirm'))}</button></form>`,
`<a href="/signup">${esc(t('auth.login.noAccount'))} ${esc(t('auth.signup.title'))}</a> · <a href="/reset-password">${esc(t('auth.login.forgot'))}</a><br><span class="muted">${esc(t('auth.login.operatorNote'))}</span>`) });
}
export function signupPage(shell) {
return shell({ title: t('auth.signup.title'), nav: null, scripts: ['/ui/auth/auth.js'], body: box(t('auth.signup.title'), `
<form id="signup">${f('email', t('auth.login.email'), 'email', 'autocomplete="username" required')}${f('displayName', t('auth.signup.displayName'), 'text', 'maxlength="80" required')}${f('password', t('auth.login.password'), 'password', 'autocomplete="new-password" minlength="8" required', t('auth.signup.passwordHint'))}
<div class="f"><label><input type="checkbox" name="terms" style="width:auto"> ${esc(t('auth.signup.terms'))} <a href="/help#terms">보기</a></label><label><input type="checkbox" name="privacy" style="width:auto"> ${esc(t('auth.signup.privacy'))} <a href="/help#privacy">보기</a></label><label><input type="checkbox" name="marketing" style="width:auto"> ${esc(t('auth.signup.marketing'))}</label></div>
<p class="hint">${esc(t('auth.signup.policyNote'))}</p><p class="err" id="signup-err" role="alert"></p><button class="btn primary" type="submit">${esc(t('auth.signup.submit'))}</button></form>
<div id="signup-done" hidden class="alert info"></div>`, `<a href="/login">${esc(t('auth.login.title')) }</a>`) });
}
export function verifyPage(shell, { state, user }) {
const msg = { ok: t('auth.verify.ok'), invalid: t('auth.verify.invalid'), used: t('auth.verify.used') }[state] || t('auth.verify.invalid');
return shell({ title: t('auth.verify.title'), nav: null, user, scripts: ['/ui/auth/auth.js'], body: box(t('auth.verify.title'), `<div class="alert ${state === 'ok' ? 'info' : 'warning'}">${esc(msg)}</div>${state === 'ok' ? `<a class="btn primary" href="/">${esc(t('nav.dashboard'))}</a>` : `<button class="btn" data-action="resend-verify">${esc(t('auth.verify.resend'))}</button> <a class="btn" href="/login">${esc(t('auth.login.title'))}</a>`}`) });
}
export function resetRequestPage(shell) {
return shell({ title: t('auth.reset.title'), nav: null, scripts: ['/ui/auth/auth.js'], body: box(t('auth.reset.title'), `<form id="reset-request">${f('email', t('auth.login.email'), 'email', 'required')}<p class="err" id="rr-err"></p><button class="btn primary" type="submit">${esc(t('auth.reset.request'))}</button></form><div id="rr-done" class="alert info" hidden></div>`, `<a href="/login">${esc(t('auth.login.title'))}</a>`) });
}
export function resetFormPage(shell, { token, state }) {
const body = state === 'valid' ? `<form id="reset" data-token="${esc(token)}">${f('password', t('auth.reset.newPassword'), 'password', 'autocomplete="new-password" minlength="8" required', t('auth.signup.passwordHint'))}<p class="err" id="reset-err"></p><button class="btn primary" type="submit">${esc(t('auth.reset.submit'))}</button></form><div id="reset-done" class="alert info" hidden></div>`
: `<div class="alert warning"><b>${esc(t('auth.reset.state'))}:</b> ${esc(state === 'used' ? t('auth.reset.used') : t('auth.reset.expired'))}</div><a class="btn" href="/reset-password">${esc(t('auth.reset.request'))}</a>`;
return shell({ title: t('auth.reset.title'), nav: null, scripts: ['/ui/auth/auth.js'], body: box(t('auth.reset.title'), body) });
}
/** 내 계정(프로필·비밀번호·이메일 확인 상태) */
export function accountPage(shell, o) {
const u = o.user;
return shell({ ...o, title: t('auth.account.title'), nav: null, crumbs: [{ label: t('auth.account.title') }], scripts: ['/ui/auth/auth.js'], body: `
<div class="tabs"><a class="on" href="/account">${esc(t('auth.account.profile'))}</a><a href="/account/security">${esc(t('auth.account.security'))}</a><a href="/account/sessions">${esc(t('auth.sessions.title'))}</a></div>
${!u.email_verified_at ? `<div class="alert warning">${esc(t('auth.verify.banner'))} <button class="btn" data-action="resend-verify">${esc(t('auth.verify.resend'))}</button></div>` : ''}
<div class="card"><h2>${esc(t('auth.account.profile'))}</h2><dl class="kv"><dt>${esc(t('auth.login.email'))}</dt><dd>${esc(u.email)} ${u.email_verified_at ? '<span class="badge ok">✔ 확인됨</span>' : '<span class="badge warning">! 확인 전</span>'}</dd></dl>
<form id="profile" data-dirty-guard>${f('displayName', t('auth.signup.displayName'), 'text', `value="${esc(u.display_name)}" maxlength="80" required`)}<button class="btn primary" type="submit">${esc(t('common.save'))}</button></form></div>
<div class="card"><h2>${esc(t('auth.account.changePassword'))}</h2><form id="password">${f('current', t('auth.account.currentPassword'), 'password', 'autocomplete="current-password" required')}${f('next', t('auth.reset.newPassword'), 'password', 'autocomplete="new-password" minlength="8" required')}<p class="err" id="pw-err"></p><button class="btn primary" type="submit">${esc(t('auth.account.changePassword'))}</button></form><p class="hint">비밀번호를 바꾸면 다른 기기의 로그인이 모두 종료됩니다.</p></div>` });
}
export function securityPage(shell, o) {
const mfaOn = !!o.mfaEnabled;
return shell({ ...o, title: t('auth.mfa.title'), nav: null, crumbs: [{ label: t('auth.account.title'), href: '/account' }, { label: t('auth.account.security') }], scripts: ['/ui/auth/auth.js'], body: `
<div class="tabs"><a href="/account">${esc(t('auth.account.profile'))}</a><a class="on" href="/account/security">${esc(t('auth.account.security'))}</a><a href="/account/sessions">${esc(t('auth.sessions.title'))}</a></div>
<div class="card"><h2>${esc(t('auth.mfa.title'))}</h2><dl class="kv"><dt>${esc(t('auth.mfa.level'))}</dt><dd>${mfaOn ? `<span class="badge ok">✔ ${esc(t('auth.mfa.levelMfa'))}</span>` : `<span class="badge warning">! ${esc(t('auth.mfa.levelPw'))}</span>`}</dd><dt>${esc(t('auth.mfa.title'))}</dt><dd>${esc(mfaOn ? t('auth.mfa.enabled') : t('auth.mfa.disabled'))}${o.mfaEnabledAt ? ` <span class="sub">(${esc(o.mfaEnabledAt)})</span>` : ''}</dd>${mfaOn ? `<dt>${esc(t('auth.mfa.recovery'))}</dt><dd>남은 복구 코드 ${o.recoveryLeft}개</dd>` : ''}</dl>
<div id="mfa-area" data-enabled="${mfaOn ? 1 : 0}">${mfaOn ? `<form id="mfa-disable">${f('password', t('auth.login.password'), 'password', 'autocomplete="current-password" required')}${f('code', t('auth.login.mfaTitle'), 'text', 'inputmode="numeric" required')}<p class="hint">${esc(t('auth.mfa.disableNeeds'))}</p><p class="err" id="mfad-err"></p><button class="btn danger" type="submit">${esc(t('auth.mfa.disable'))}</button></form>
<form id="mfa-regen" style="margin-top:12px">${f('password2', t('auth.login.password'), 'password', 'autocomplete="current-password" required')}<button class="btn" type="submit">${esc(t('auth.mfa.regenerate'))}</button></form><div id="recovery-out"></div>`
: `<button class="btn primary" data-action="mfa-setup">${esc(t('auth.mfa.setup'))}</button><div id="mfa-setup"></div>`}</div></div>` });
}
export function sessionsPage(shell, o) {
return shell({ ...o, title: t('auth.sessions.title'), nav: null, crumbs: [{ label: t('auth.account.title'), href: '/account' }, { label: t('auth.sessions.title') }], scripts: ['/ui/auth/auth.js'], body: `
<div class="tabs"><a href="/account">${esc(t('auth.account.profile'))}</a><a href="/account/security">${esc(t('auth.account.security'))}</a><a class="on" href="/account/sessions">${esc(t('auth.sessions.title'))}</a></div>
<div class="card"><div class="ph-row"><h2>${esc(t('auth.sessions.title'))}</h2><button class="btn danger" data-action="revoke-others">${esc(t('auth.sessions.revokeOthers'))}</button></div><div id="sessions">${o.statePanel('loading')}</div></div>` });
}