fix: harden launch server runtime and public config

This commit is contained in:
stringadmin
2026-06-04 18:58:45 +08:00
parent 1a5992845a
commit df5ea8c65e
14 changed files with 926 additions and 32 deletions
+146
View File
@@ -30,6 +30,13 @@ const {
buildOssPublicUrl,
normalizeAvatarOssKey,
normalizeProfileMediaUrl,
EMAIL_PURPOSES,
EMAIL_CODE_TTL_MINUTES,
EMAIL_CODE_COOLDOWN_SECONDS,
EMAIL_CODE_MAX_ATTEMPTS,
hashEmailCode,
sendEmailCode,
consumeEmailCode,
} = require("./context");
const {
checkBetaInviteCodeForRegistration,
@@ -208,15 +215,20 @@ function registerAuthRoutes(router) {
const email = normalizeEmail(req.body?.email);
const usernameInput = String(req.body?.username || "").trim();
const password = String(req.body?.password || "");
const code = String(req.body?.code || "").trim();
const emailError = validateEmail(email);
if (emailError) return res.status(400).json({ error: emailError });
if (!code) return res.status(400).json({ error: "缺少验证码" });
const passwordError = validatePassword(password);
if (passwordError) return res.status(400).json({ error: passwordError });
const registrationInvite = await ensureRegistrationInvite(req, res);
if (!registrationInvite) return;
try {
const verified = await consumeEmailCode(email, code, "register");
if (!verified) return res.status(400).json({ error: "验证码错误或已过期" });
const { rows: existingEmail } = await pool.query(
"SELECT id FROM users WHERE LOWER(email) = LOWER($1) LIMIT 1",
[email],
@@ -751,6 +763,140 @@ function registerAuthRoutes(router) {
res.status(500).json({ error: "更新个人资料失败" });
}
});
// ============================================================
// Email verification routes
// ============================================================
router.post("/auth/email/send-code", async (req, res) => {
const email = normalizeEmail(req.body?.email);
const purpose = String(req.body?.purpose || "register");
const emailError = validateEmail(email);
if (emailError) return res.status(400).json({ error: emailError });
if (!EMAIL_PURPOSES.has(purpose)) return res.status(400).json({ error: "验证码用途无效" });
if (purpose === "register") {
const inviteOk = await ensureBetaInviteCode(req, res);
if (!inviteOk) return;
}
try {
const { rows: recentCodes } = await pool.query(
"SELECT created_at FROM email_verification_codes WHERE email = $1 AND purpose = $2 AND created_at > NOW() - ($3::text || ' seconds')::interval ORDER BY created_at DESC LIMIT 1",
[email, purpose, EMAIL_CODE_COOLDOWN_SECONDS]
);
if (recentCodes.length > 0) {
return res.status(429).json({ error: "验证码发送太频繁,请 " + EMAIL_CODE_COOLDOWN_SECONDS + " 秒后再试" });
}
if (purpose === "register") {
const { rows: existing } = await pool.query("SELECT id FROM users WHERE LOWER(email) = LOWER($1) LIMIT 1", [email]);
if (existing.length > 0) return res.status(409).json({ error: "该邮箱已注册" });
}
if (purpose === "login" || purpose === "reset") {
const { rows: existing } = await pool.query("SELECT id FROM users WHERE LOWER(email) = LOWER($1) AND enabled = 1 LIMIT 1", [email]);
if (existing.length === 0) return res.status(404).json({ error: "该邮箱尚未注册" });
}
const code = generateSmsCode();
const codeHash = hashEmailCode(email, code);
await pool.query(
"INSERT INTO email_verification_codes (email, purpose, code_hash, expires_at) VALUES ($1, $2, $3, NOW() + ($4::text || ' minutes')::interval)",
[email, purpose, codeHash, EMAIL_CODE_TTL_MINUTES]
);
const sendResult = await sendEmailCode(email, code, purpose);
res.json({
success: true,
provider: sendResult.provider,
ttlSeconds: EMAIL_CODE_TTL_MINUTES * 60,
cooldownSeconds: EMAIL_CODE_COOLDOWN_SECONDS,
...(sendResult.devCode ? { devCode: sendResult.devCode } : {}),
});
} catch (error) {
console.error("[auth/email/send-code] failed", error);
res.status(500).json({ error: "验证码发送失败" });
}
});
router.post("/auth/email/verify", async (req, res) => {
const email = normalizeEmail(req.body?.email);
const code = String(req.body?.code || "").trim();
const purpose = String(req.body?.purpose || "register");
const emailError = validateEmail(email);
if (emailError) return res.status(400).json({ error: emailError });
if (!code) return res.status(400).json({ error: "缺少验证码" });
if (!EMAIL_PURPOSES.has(purpose)) return res.status(400).json({ error: "验证码用途无效" });
try {
const verified = await consumeEmailCode(email, code, purpose);
if (!verified) return res.status(400).json({ error: "验证码错误或已过期" });
if (purpose === "register" || purpose === "login") {
await pool.query("UPDATE users SET email_verified = 1 WHERE LOWER(email) = LOWER($1)", [email]);
}
res.json({ success: true });
} catch (error) {
console.error("[auth/email/verify] failed", error);
res.status(500).json({ error: "验证失败" });
}
});
router.post("/auth/forgot-password", async (req, res) => {
const email = normalizeEmail(req.body?.email);
const emailError = validateEmail(email);
if (emailError) return res.status(400).json({ error: emailError });
try {
const { rows } = await pool.query("SELECT id FROM users WHERE LOWER(email) = LOWER($1) AND enabled = 1 LIMIT 1", [email]);
if (rows.length === 0) {
return res.json({ success: true, message: "如果该邮箱已注册,重置链接已发送" });
}
const { rows: recentCodes } = await pool.query(
"SELECT created_at FROM email_verification_codes WHERE email = $1 AND purpose = 'reset' AND created_at > NOW() - ($2::text || ' seconds')::interval ORDER BY created_at DESC LIMIT 1",
[email, EMAIL_CODE_COOLDOWN_SECONDS]
);
if (recentCodes.length > 0) {
return res.status(429).json({ error: "发送太频繁,请 " + EMAIL_CODE_COOLDOWN_SECONDS + " 秒后再试" });
}
const code = generateSmsCode();
const codeHash = hashEmailCode(email, code);
await pool.query(
"INSERT INTO email_verification_codes (email, purpose, code_hash, expires_at) VALUES ($1, 'reset', $2, NOW() + ($3::text || ' minutes')::interval)",
[email, codeHash, EMAIL_CODE_TTL_MINUTES]
);
await sendEmailCode(email, code, "reset");
res.json({ success: true, message: "重置验证码已发送到您的邮箱" });
} catch (error) {
console.error("[auth/forgot-password] failed", error);
res.status(500).json({ error: "发送失败" });
}
});
router.post("/auth/reset-password", async (req, res) => {
const email = normalizeEmail(req.body?.email);
const code = String(req.body?.code || "").trim();
const newPassword = String(req.body?.newPassword || "");
const emailError = validateEmail(email);
if (emailError) return res.status(400).json({ error: emailError });
if (!code) return res.status(400).json({ error: "缺少验证码" });
const passwordError = validatePassword(newPassword);
if (passwordError) return res.status(400).json({ error: passwordError });
try {
const verified = await consumeEmailCode(email, code, "reset");
if (!verified) return res.status(400).json({ error: "验证码错误或已过期" });
const hash = await bcrypt.hash(newPassword, 10);
await pool.query("UPDATE users SET password_hash = $1 WHERE LOWER(email) = LOWER($2)", [hash, email]);
res.json({ success: true, message: "密码重置成功,请重新登录" });
} catch (error) {
console.error("[auth/reset-password] failed", error);
res.status(500).json({ error: "密码重置失败" });
}
});
}
module.exports = {