/** * 光湖 · Outline 邮箱注册服务 v1.1 * BS-SG-001 · 铸渊 ICE-GL-ZY001 · D135c * * 修复: SQLite WAL模式 + busy_timeout, 解决Dex并发写入冲突 */ const express = require('express'); const crypto = require('crypto'); const nodemailer = require('nodemailer'); const bcrypt = require('bcryptjs'); const { execSync } = require('child_process'); const app = express(); app.use(express.json()); const PORT = process.env.PORT || 3997; const DEX_DB_HOST = '/opt/outline/dex-data/dex.db'; const CODE_EXPIRE_MS = 5 * 60 * 1000; const CODE_COOLDOWN_MS = 60 * 1000; const SMTP_USER = '<<@committer_handle:base64>>'; const SMTP_PASS = 'ggeuegmaragmbejb'; const codes = new Map(); let transporter = null; function getTransporter() { if (transporter) return transporter; transporter = nodemailer.createTransport({ host: 'smtp.qq.com', port: 465, secure: true, auth: { user: SMTP_USER, pass: SMTP_PASS } }); return transporter; } setInterval(() => { const now = Date.now(); for (const [k, v] of codes) { if (now - v.ts > CODE_EXPIRE_MS) codes.delete(k); } }, 10 * 60 * 1000); function hashEmail(email) { return crypto.createHash('sha256').update(email.trim().toLowerCase()).digest('hex').slice(0, 16); } function generateCode() { return String(Math.floor(100000 + Math.random() * 900000)); } function addUserToDex(email, hash, username, userId) { return new Promise((resolve, reject) => { try { const result = execSync( `python3 /opt/zhuyuan/register/add_dex_user.py "${email}" "${hash}" "${username}" "${userId}"`, { encoding: 'utf8', timeout: 10000 } ).trim(); if (result === 'EMAIL_EXISTS') return reject(new Error('EMAIL_EXISTS')); if (result === 'OK') return resolve(userId); reject(new Error('DB_ERROR: ' + result)); } catch (err) { reject(err); } }); } app.post('/send-code', async (req, res) => { const { email } = req.body; if (!email || !email.includes('@')) { return res.json({ ok: false, error: '请输入有效邮箱' }); } const normalized = email.trim().toLowerCase(); const h = hashEmail(normalized); const existing = codes.get(h); if (existing && Date.now() - existing.ts < CODE_COOLDOWN_MS) { const wait = Math.ceil((CODE_COOLDOWN_MS - (Date.now() - existing.ts)) / 1000); return res.json({ ok: false, error: `请 ${wait} 秒后再试` }); } const code = generateCode(); codes.set(h, { code, ts: Date.now(), attempts: 0, email: normalized }); try { await getTransporter().sendMail({ from: `"光湖协作中心" <${SMTP_USER}>`, to: normalized, subject: '光湖 · 邮箱验证码', html: `

光湖 · 语言世界

您的注册验证码:

${code}

5分钟内有效。若非本人操作请忽略。

` }); console.log(`[Register] Code sent to ${normalized.slice(0,3)}***`); res.json({ ok: true, message: '验证码已发送,请查收邮箱' }); } catch (err) { codes.delete(h); console.error(`[Register] Send failed: ${err.message}`); res.json({ ok: false, error: '验证码发送失败,请稍后重试' }); } }); app.post('/verify', async (req, res) => { const { email, code, password } = req.body; if (!email || !code || !password) { return res.json({ ok: false, error: '邮箱、验证码和密码不能为空' }); } if (password.length < 6) { return res.json({ ok: false, error: '密码至少6位' }); } const normalized = email.trim().toLowerCase(); const h = hashEmail(normalized); const pending = codes.get(h); if (!pending) return res.json({ ok: false, error: '验证码不存在或已过期,请重新获取' }); if (Date.now() - pending.ts > CODE_EXPIRE_MS) { codes.delete(h); return res.json({ ok: false, error: '验证码已过期' }); } if (pending.attempts >= 3) { codes.delete(h); return res.json({ ok: false, error: '错误次数过多,请重新获取' }); } if (String(code).trim() !== pending.code) { pending.attempts++; return res.json({ ok: false, error: `验证码错误(${3 - pending.attempts} 次机会)` }); } codes.delete(h); try { const pwHash = bcrypt.hashSync(password, 10); const userId = crypto.randomUUID(); const username = normalized.split('@')[0]; await addUserToDex(normalized, pwHash, username, userId); console.log(`[Register] User created: ${normalized.slice(0,3)}***`); res.json({ ok: true, message: '注册成功!请返回登录', loginUrl: '/outline/' }); } catch (err) { console.error(`[Register] DB error: ${err.message}`); if (err.message === 'EMAIL_EXISTS') { return res.json({ ok: false, error: '该邮箱已注册,请直接登录' }); } res.json({ ok: false, error: '注册失败,请稍后重试' }); } }); app.get('/health', (req, res) => { res.json({ ok: true, service: 'guanghu-register', db: 'python' }); }); app.listen(PORT, '127.0.0.1', () => { console.log(`[Register] Listening on 127.0.0.1:${PORT} (WAL mode)`); });