Quellcode durchsuchen

fix: redirect browser auth form submissions

Codex vor 1 Monat
Ursprung
Commit
985ccf3e3a
3 geänderte Dateien mit 38 neuen und 19 gelöschten Zeilen
  1. 2 2
      public/login.html
  2. 9 2
      public/login.js
  3. 27 15
      src/server.js

+ 2 - 2
public/login.html

@@ -41,7 +41,7 @@
           <button data-mode="register" type="button">注册</button>
         </div>
 
-        <form id="loginForm" class="login-form" method="post" action="/api/login">
+        <form id="loginForm" class="login-form" method="post" action="/login">
           <label>
             用户名或邮箱
             <input name="username" autocomplete="username" required autofocus>
@@ -53,7 +53,7 @@
           <button type="submit">登录</button>
         </form>
 
-        <form id="registerForm" class="login-form hidden" method="post" action="/api/register">
+        <form id="registerForm" class="login-form hidden" method="post" action="/register">
           <label>
             用户名
             <input name="username" autocomplete="username" minlength="3" required>

+ 9 - 2
public/login.js

@@ -4,6 +4,8 @@ const message = document.querySelector('#loginMessage');
 const modeTitle = document.querySelector('#modeTitle');
 const modeEyebrow = document.querySelector('#modeEyebrow');
 const tabs = document.querySelectorAll('[data-mode]');
+const initialParams = new URLSearchParams(window.location.search);
+const initialError = initialParams.get('error');
 
 if (window.location.search) {
   window.history.replaceState(null, '', window.location.pathname);
@@ -24,6 +26,7 @@ registerForm.addEventListener('submit', async (event) => {
 });
 
 if (window.location.pathname === '/register') setMode('register');
+if (initialError) showError(initialError);
 
 function setMode(mode) {
   const registering = mode === 'register';
@@ -53,9 +56,13 @@ async function submitAuth(path, form, button) {
     if (!response.ok) throw new Error(data.error || '请求失败。');
     window.location.href = '/';
   } catch (error) {
-    message.textContent = error.message;
-    message.classList.add('error');
+    showError(error.message);
   } finally {
     button.disabled = false;
   }
 }
+
+function showError(text) {
+  message.textContent = text;
+  message.classList.add('error');
+}

+ 27 - 15
src/server.js

@@ -110,8 +110,8 @@ const server = http.createServer(async (req, res) => {
     if (req.method === 'OPTIONS') return handleOptions(res);
     const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
     if (url.pathname === '/healthz') return sendJson(res, 200, { ok: true });
-    if (req.method === 'POST' && url.pathname === '/api/register') return await handleRegister(req, res);
-    if (req.method === 'POST' && url.pathname === '/api/login') return await handleLogin(req, res);
+    if (req.method === 'POST' && (url.pathname === '/api/register' || url.pathname === '/register')) return await handleRegister(req, res);
+    if (req.method === 'POST' && (url.pathname === '/api/login' || url.pathname === '/login')) return await handleLogin(req, res);
     if (req.method === 'POST' && url.pathname === '/api/logout') return handleLogout(res);
 
     const user = getRequestUser(req, url.pathname);
@@ -452,30 +452,36 @@ async function handleRegister(req, res) {
       email: body.email,
       password: body.password
     });
-    const token = createSessionToken(user);
-    res.writeHead(201, {
-      'Content-Type': 'application/json; charset=utf-8',
-      'Set-Cookie': sessionCookie(token)
-    });
-    return res.end(JSON.stringify({ user }));
+    return sendAuthSuccess(req, res, 201, user);
   } catch (error) {
-    if (isUniqueError(error)) return sendJson(res, 409, { error: '用户名或邮箱已被注册。' });
-    return sendJson(res, 400, { error: error.message || '注册失败。' });
+    if (isUniqueError(error)) return sendAuthError(req, res, 409, '用户名或邮箱已被注册。', '/register');
+    return sendAuthError(req, res, 400, error.message || '注册失败。', '/register');
   }
 }
 
 async function handleLogin(req, res) {
   const body = await readJson(req);
   const user = authenticateUser(body.username || body.email, body.password);
-  if (!user) return sendJson(res, 401, { error: '账号或密码不正确。' });
+  if (!user) return sendAuthError(req, res, 401, '账号或密码不正确。', '/login');
+  return sendAuthSuccess(req, res, 200, user);
+}
+
+function sendAuthSuccess(req, res, status, user) {
   const token = createSessionToken(user);
-  res.writeHead(200, {
+  const cookie = sessionCookie(token);
+  if (wantsHtmlRedirect(req)) return redirect(res, '/', 303, { 'Set-Cookie': cookie });
+  res.writeHead(status, {
     'Content-Type': 'application/json; charset=utf-8',
-    'Set-Cookie': sessionCookie(token)
+    'Set-Cookie': cookie
   });
   res.end(JSON.stringify({ user }));
 }
 
+function sendAuthError(req, res, status, message, fallbackPath) {
+  if (wantsHtmlRedirect(req)) return redirect(res, `${fallbackPath}?error=${encodeURIComponent(message)}`, 303);
+  return sendJson(res, status, { error: message });
+}
+
 function handleLogout(res) {
   res.writeHead(200, {
     'Content-Type': 'application/json; charset=utf-8',
@@ -608,11 +614,17 @@ function sendJson(res, status, payload) {
   res.end(JSON.stringify(payload));
 }
 
-function redirect(res, location) {
-  res.writeHead(302, { Location: location });
+function redirect(res, location, status = 302, headers = {}) {
+  res.writeHead(status, { ...headers, Location: location });
   res.end();
 }
 
+function wantsHtmlRedirect(req) {
+  const contentType = String(req.headers['content-type'] || '').toLowerCase();
+  const accept = String(req.headers.accept || '').toLowerCase();
+  return contentType.includes('application/x-www-form-urlencoded') && accept.includes('text/html');
+}
+
 function setSecurityHeaders(res) {
   res.setHeader('X-Content-Type-Options', 'nosniff');
   res.setHeader('X-Frame-Options', 'DENY');