fix: harden launch server runtime and public config
This commit is contained in:
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user