完成: - 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
149 lines
5.4 KiB
JavaScript
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)`);
|
|
});
|