/** * Login form handler: intercepts form submission and submits via fetch. * On success (200), redirects to dashboard. On error (401/423), shows * the #login-error element with a user-friendly message. * * XSS-safe: all dynamic values are set via textContent, never innerHTML. */ (function () { 'use strict'; var form = document.querySelector('.login-form'); if (!form) return; var errorEl = document.getElementById('login-error'); form.addEventListener('submit', function (e) { e.preventDefault(); // Hide any previous error message if (errorEl) { errorEl.setAttribute('hidden', ''); errorEl.textContent = ''; } // Collect form data and convert to application/x-www-form-urlencoded var formData = new FormData(form); var body = new URLSearchParams(formData).toString(); fetch('/login', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body }) .then(function (res) { if (res.status === 200) { // Success: redirect to dashboard window.location.href = '/'; return null; } // Error: parse response and show error message if (!res.ok) throw new Error('login_failed:' + res.status); return res.json(); }) .then(function (data) { if (data && data.error) { showError(data.error); } }) .catch(function (err) { // Extract status code from error message if available var status = /login_failed:(\d+)/.exec(String(err)) ? parseInt(RegExp.$1) : 0; if (status === 401 || status === 0) { showError('invalid_credentials'); } else if (status === 423) { showError('locked'); } }); }); function showError(errorCode) { if (!errorEl) return; var message; if (errorCode === 'invalid_credentials') { message = '로그인 실패'; } else if (errorCode === 'locked') { message = '계정 잠김'; } else { message = '로그인 실패'; } errorEl.textContent = message; errorEl.removeAttribute('hidden'); } })();