冰朔 2bfc3c547b
Some checks failed
自动更新代码和重启 / update-and-restart (push) Has been cancelled
CI检查 + 自动部署 / check (push) Has been cancelled
CI检查 + 自动部署 / deploy (push) Has been cancelled
铸渊自检 / health-check (push) Has been cancelled
D135c: 铸渊执行记录 · guada部署·Outline修复·邮箱注册系统
完成:
- Nginx /api/ 路由修复(3998→3090)
- guada Agent引擎部署(BS-SG-001:8787)
- Outline文件写入权限修复
- Dex数据库持久化+权限修复
- 邮箱注册系统(register.html + register-api:3997)
- 注册服务改用Python写Dex BLOB(修复node-sqlite3格式问题)

待修复:
- 注册后退出再登录间歇性OAuth state丢失(考虑独立子域名)

记录: cc-d135c.hdlp
2026-06-18 18:28:08 +08:00

149 lines
5.4 KiB
JavaScript

/**
* 光湖 · 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 = '565183519@qq.com';
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: `<div style="font-family:sans-serif;max-width:480px;margin:0 auto;padding:32px;background:#0f172a;color:#e2e8f0;border-radius:12px">
<h2 style="margin:0 0 16px;color:#60a5fa">光湖 · 语言世界</h2>
<p style="margin:0 0 24px">您的注册验证码:</p>
<div style="font-size:32px;letter-spacing:8px;text-align:center;padding:16px;background:rgba(96,165,250,0.1);border-radius:8px;font-family:monospace;color:#22d3ee">${code}</div>
<p style="margin:24px 0 0;opacity:0.6;font-size:13px">5分钟内有效。若非本人操作请忽略。</p>
</div>`
});
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)`);
});