/** * 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 = '') => `

${esc(title)}

${inner}${foot ? `

${foot}

` : ''}
`; const f = (id, label, type = 'text', extra = '', hint = '') => `
${hint ? `${esc(hint)}` : ''}
`; 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 ? `
${esc(notice)}
` : ''}
${f('email', t('auth.login.email'), 'email', 'autocomplete="username" required')}${f('password', t('auth.login.password'), 'password', 'autocomplete="current-password" required')}
`, `${esc(t('auth.login.noAccount'))} ${esc(t('auth.signup.title'))} · ${esc(t('auth.login.forgot'))}
${esc(t('auth.login.operatorNote'))}`) }); } export function signupPage(shell) { return shell({ title: t('auth.signup.title'), nav: null, scripts: ['/ui/auth/auth.js'], body: box(t('auth.signup.title'), `
${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'))}

${esc(t('auth.signup.policyNote'))}

`, `${esc(t('auth.login.title')) }`) }); } 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'), `
${esc(msg)}
${state === 'ok' ? `${esc(t('nav.dashboard'))}` : ` ${esc(t('auth.login.title'))}`}`) }); } export function resetRequestPage(shell) { return shell({ title: t('auth.reset.title'), nav: null, scripts: ['/ui/auth/auth.js'], body: box(t('auth.reset.title'), `
${f('email', t('auth.login.email'), 'email', 'required')}

`, `${esc(t('auth.login.title'))}`) }); } export function resetFormPage(shell, { token, state }) { const body = state === 'valid' ? `
${f('password', t('auth.reset.newPassword'), 'password', 'autocomplete="new-password" minlength="8" required', t('auth.signup.passwordHint'))}

` : `
${esc(t('auth.reset.state'))}: ${esc(state === 'used' ? t('auth.reset.used') : t('auth.reset.expired'))}
${esc(t('auth.reset.request'))}`; 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: `
${esc(t('auth.account.profile'))}${esc(t('auth.account.security'))}${esc(t('auth.sessions.title'))}
${!u.email_verified_at ? `
${esc(t('auth.verify.banner'))}
` : ''}

${esc(t('auth.account.profile'))}

${esc(t('auth.login.email'))}
${esc(u.email)} ${u.email_verified_at ? '✔ 확인됨' : '! 확인 전'}
${f('displayName', t('auth.signup.displayName'), 'text', `value="${esc(u.display_name)}" maxlength="80" required`)}

${esc(t('auth.account.changePassword'))}

${f('current', t('auth.account.currentPassword'), 'password', 'autocomplete="current-password" required')}${f('next', t('auth.reset.newPassword'), 'password', 'autocomplete="new-password" minlength="8" required')}

비밀번호를 바꾸면 다른 기기의 로그인이 모두 종료됩니다.

` }); } 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: `
${esc(t('auth.account.profile'))}${esc(t('auth.account.security'))}${esc(t('auth.sessions.title'))}

${esc(t('auth.mfa.title'))}

${esc(t('auth.mfa.level'))}
${mfaOn ? `✔ ${esc(t('auth.mfa.levelMfa'))}` : `! ${esc(t('auth.mfa.levelPw'))}`}
${esc(t('auth.mfa.title'))}
${esc(mfaOn ? t('auth.mfa.enabled') : t('auth.mfa.disabled'))}${o.mfaEnabledAt ? ` (${esc(o.mfaEnabledAt)})` : ''}
${mfaOn ? `
${esc(t('auth.mfa.recovery'))}
남은 복구 코드 ${o.recoveryLeft}개
` : ''}
${mfaOn ? `
${f('password', t('auth.login.password'), 'password', 'autocomplete="current-password" required')}${f('code', t('auth.login.mfaTitle'), 'text', 'inputmode="numeric" required')}

${esc(t('auth.mfa.disableNeeds'))}

${f('password2', t('auth.login.password'), 'password', 'autocomplete="current-password" required')}
` : `
`}
` }); } 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: `
${esc(t('auth.account.profile'))}${esc(t('auth.account.security'))}${esc(t('auth.sessions.title'))}

${esc(t('auth.sessions.title'))}

${o.statePanel('loading')}
` }); }