|
@@ -110,8 +110,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
if (req.method === 'OPTIONS') return handleOptions(res);
|
|
if (req.method === 'OPTIONS') return handleOptions(res);
|
|
|
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
|
if (url.pathname === '/healthz') return sendJson(res, 200, { ok: true });
|
|
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);
|
|
if (req.method === 'POST' && url.pathname === '/api/logout') return handleLogout(res);
|
|
|
|
|
|
|
|
const user = getRequestUser(req, url.pathname);
|
|
const user = getRequestUser(req, url.pathname);
|
|
@@ -452,30 +452,36 @@ async function handleRegister(req, res) {
|
|
|
email: body.email,
|
|
email: body.email,
|
|
|
password: body.password
|
|
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) {
|
|
} 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) {
|
|
async function handleLogin(req, res) {
|
|
|
const body = await readJson(req);
|
|
const body = await readJson(req);
|
|
|
const user = authenticateUser(body.username || body.email, body.password);
|
|
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);
|
|
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',
|
|
'Content-Type': 'application/json; charset=utf-8',
|
|
|
- 'Set-Cookie': sessionCookie(token)
|
|
|
|
|
|
|
+ 'Set-Cookie': cookie
|
|
|
});
|
|
});
|
|
|
res.end(JSON.stringify({ user }));
|
|
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) {
|
|
function handleLogout(res) {
|
|
|
res.writeHead(200, {
|
|
res.writeHead(200, {
|
|
|
'Content-Type': 'application/json; charset=utf-8',
|
|
'Content-Type': 'application/json; charset=utf-8',
|
|
@@ -608,11 +614,17 @@ function sendJson(res, status, payload) {
|
|
|
res.end(JSON.stringify(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();
|
|
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) {
|
|
function setSecurityHeaders(res) {
|
|
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
|
res.setHeader('X-Frame-Options', 'DENY');
|
|
res.setHeader('X-Frame-Options', 'DENY');
|