feat: send-code before register, dup game_name check, skin hides uid
This commit is contained in:
@@ -14,15 +14,43 @@ async function getSiteUrl() {
|
||||
return row?.v || 'http://localhost:3100';
|
||||
}
|
||||
|
||||
router.post('/send-verify-code', async (req, res) => {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
if (!email) return res.status(400).json({ error: '请输入邮箱' });
|
||||
|
||||
const code = String(Math.floor(100000 + Math.random() * 900000));
|
||||
const codeId = uuid();
|
||||
await query("DELETE FROM captchas WHERE created_at < NOW() - INTERVAL 5 MINUTE");
|
||||
await query('INSERT INTO captchas(id, question, answer) VALUES (?,?,?)', [codeId, email, code]);
|
||||
|
||||
const sent = await sendEmail(email, 'verify_email', {
|
||||
username: email.split('@')[0], game_name: '', game_uid: '',
|
||||
verify_link: code,
|
||||
});
|
||||
if (!sent) {
|
||||
await query('DELETE FROM captchas WHERE id = ?', [codeId]);
|
||||
return res.status(200).json({ code_id: codeId, code, dev: true });
|
||||
}
|
||||
res.json({ code_id: codeId, message: '验证码已发送' });
|
||||
} catch (err) { console.error(err); res.status(500).json({ error: '服务器内部错误' }); }
|
||||
});
|
||||
|
||||
router.post('/register', validateLengths({
|
||||
username: 20, password: 100, email: 100, game_name: 50, game_uid: 50,
|
||||
}), async (req, res) => {
|
||||
try {
|
||||
const { username, password, email, game_name, game_uid, captcha_id, captcha_answer, source } = req.body;
|
||||
const { username, password, email, game_name, game_uid, captcha_id, captcha_answer, source, code_id, code } = req.body;
|
||||
|
||||
if (!username || !password || !email || !game_name || !game_uid) {
|
||||
if (!username || !password || !email || !game_name) {
|
||||
return res.status(400).json({ error: '所有字段均为必填' });
|
||||
}
|
||||
if (source === 'netease' && !game_uid) return res.status(400).json({ error: '网易端必须填写UID' });
|
||||
|
||||
if (!code_id || !code) return res.status(400).json({ error: '请先完成邮箱验证' });
|
||||
const codeRow = await getRow("SELECT * FROM captchas WHERE id = ? AND created_at > NOW() - INTERVAL 10 MINUTE", [code_id]);
|
||||
if (!codeRow || codeRow.answer !== code) return res.status(400).json({ error: '验证码无效或已过期' });
|
||||
await query('DELETE FROM captchas WHERE id = ?', [code_id]);
|
||||
if (username.length < 2 || username.length > 20) {
|
||||
return res.status(400).json({ error: '用户名需2-20个字符' });
|
||||
}
|
||||
@@ -45,27 +73,17 @@ router.post('/register', validateLengths({
|
||||
if (existingUser || existingEmail) {
|
||||
return res.status(400).json({ error: '用户名或邮箱已被使用' });
|
||||
}
|
||||
|
||||
const hashed = await bcrypt.hash(password, 10);
|
||||
const verifyCode = String(Math.floor(100000 + Math.random() * 900000));
|
||||
const verifyExpires = new Date(Date.now() + 30 * 60000).toISOString().replace('T', ' ').slice(0, 19);
|
||||
|
||||
await query(
|
||||
'INSERT INTO users(username, password, email, game_name, game_uid, active, email_verified, verify_token, verify_expires, source) VALUES (?,?,?,?,?,?,?,?,?,?)',
|
||||
[username, hashed, email, game_name, game_uid, 0, 0, verifyCode, verifyExpires, source || 'netease']
|
||||
);
|
||||
|
||||
const sent = await sendEmail(email, 'verify_email', {
|
||||
username, game_name, game_uid,
|
||||
verify_link: verifyCode,
|
||||
});
|
||||
|
||||
if (!sent) {
|
||||
await query('UPDATE users SET email_verified = 1, active = 1, verify_token = NULL, verify_expires = NULL WHERE username = ?', [username]);
|
||||
return res.status(201).json({ message: '注册成功!SMTP未配置,账号已自动激活,请直接登录' });
|
||||
if (game_name && (source || 'netease') === 'netease') {
|
||||
const dup = await getRow('SELECT id FROM users WHERE game_name = ? AND source = ?', [game_name, source || 'netease']);
|
||||
if (dup) return res.status(400).json({ error: '该游戏名在此来源下已注册' });
|
||||
}
|
||||
|
||||
res.status(201).json({ message: '注册成功,请查收验证邮件' });
|
||||
const hashed = await bcrypt.hash(password, 10);
|
||||
await query(
|
||||
'INSERT INTO users(username, password, email, game_name, game_uid, active, email_verified, source) VALUES (?,?,?,?,?,?,?,?)',
|
||||
[username, hashed, email, game_name, game_uid||'', 1, 1, source || 'netease']
|
||||
);
|
||||
res.status(201).json({ message: '注册成功,请登录' });
|
||||
} catch (err) {
|
||||
console.error('[auth]', err);
|
||||
res.status(500).json({ error: '服务器内部错误' });
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const RegisterPage = {
|
||||
captcha: null,
|
||||
codeId: null,
|
||||
|
||||
async render() {
|
||||
try { this.captcha = await API.get('/captcha'); } catch { this.captcha = null; }
|
||||
@@ -9,14 +10,14 @@ const RegisterPage = {
|
||||
<form id="reg-form">
|
||||
<div class="grid-2">
|
||||
<div class="form-group"><label>站点用户名 *</label><input id="ru" required placeholder="登录用"></div>
|
||||
<div class="form-group"><label>邮箱 *</label><input type="email" id="re" required placeholder="接收通知"></div>
|
||||
<div class="form-group"><label>邮箱 *</label><input type="email" id="re" required placeholder="接收验证码"></div>
|
||||
</div>
|
||||
<div class="form-group"><label>账号来源</label><select id="rs" onchange="RegisterPage.toggleSource()">
|
||||
<option value="netease">网易端</option><option value="skin">皮肤站</option>
|
||||
</select></div>
|
||||
<div class="grid-2">
|
||||
<div class="form-group"><label id="rs-label1">游戏名称 *</label><input id="rgn" required placeholder="Minecraft ID/网易昵称"></div>
|
||||
<div class="form-group"><label id="rs-label2">网易UID *</label><input id="rgu" required placeholder="UID"></div>
|
||||
<div class="form-group"><label id="rs-label1">游戏名称 *</label><input id="rgn" required placeholder="Minecraft ID"></div>
|
||||
<div class="form-group" id="uid-group"><label id="rs-label2">UID</label><input id="rgu" placeholder="UID"></div>
|
||||
</div>
|
||||
<div class="form-group"><label>密码 *</label><input type="password" id="rp" required placeholder="至少6位"></div>
|
||||
${this.captcha ? `
|
||||
@@ -28,9 +29,18 @@ const RegisterPage = {
|
||||
</div>
|
||||
<div class="help">点击图片刷新验证码</div>
|
||||
</div>` : ''}
|
||||
|
||||
<div id="verify-section">
|
||||
<div class="form-group"><label>邮箱验证码</label>
|
||||
<div style="display:flex;gap:8px">
|
||||
<input id="vf-code" placeholder="6位验证码" maxlength="6" pattern="[0-9]{6}" style="flex:1">
|
||||
<button type="button" class="btn btn-o btn-sm" id="send-code-btn" style="white-space:nowrap">发送验证码</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="regerr" class="alert alert-e hidden"></div>
|
||||
<div id="regok" class="alert alert-s hidden"></div>
|
||||
<button type="submit" class="btn btn-p btn-lg w-full"><i class="fas fa-user-plus"></i> 注册</button>
|
||||
<button type="submit" class="btn btn-p btn-lg w-full"><i class="fas fa-user-plus"></i> 完成注册</button>
|
||||
</form>
|
||||
<div style="margin-top:14px;text-align:center;font-size:13px">
|
||||
<a href="#/login" style="color:var(--p);text-decoration:none;font-weight:600">已有账号?登录</a>
|
||||
@@ -42,9 +52,12 @@ const RegisterPage = {
|
||||
|
||||
toggleSource() {
|
||||
const src = document.getElementById('rs').value;
|
||||
const labels = { netease:['游戏名称','网易UID'], skin:['游戏名称','皮肤站UID'] };
|
||||
const labels = { netease:['游戏名称','网易UID'], skin:['游戏名称','皮肤站UUID'] };
|
||||
document.getElementById('rs-label1').textContent = labels[src][0] + ' *';
|
||||
document.getElementById('rs-label2').textContent = labels[src][1] + ' *';
|
||||
document.getElementById('rs-label2').textContent = labels[src][1];
|
||||
const uidGroup = document.getElementById('uid-group');
|
||||
uidGroup.style.display = src === 'netease' ? '' : 'none';
|
||||
document.getElementById('rgu').required = src === 'netease';
|
||||
},
|
||||
|
||||
async refreshCaptcha() {
|
||||
@@ -52,9 +65,25 @@ const RegisterPage = {
|
||||
},
|
||||
|
||||
mount() {
|
||||
this.toggleSource();
|
||||
|
||||
document.getElementById('send-code-btn').addEventListener('click', async () => {
|
||||
const email = document.getElementById('re').value.trim();
|
||||
if (!email) { alert('请先填写邮箱'); return; }
|
||||
const btn = document.getElementById('send-code-btn');
|
||||
btn.disabled = true; btn.textContent = '发送中...';
|
||||
try {
|
||||
const r = await API.post('/auth/send-verify-code', { email });
|
||||
this.codeId = r.code_id;
|
||||
if (r.dev) alert('开发模式,验证码: ' + r.code);
|
||||
btn.textContent = '已发送';
|
||||
document.getElementById('vf-code').focus();
|
||||
} catch(ex) { alert(ex.message); btn.disabled = false; btn.textContent = '发送验证码'; }
|
||||
});
|
||||
|
||||
document.getElementById('reg-form').addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const btn = e.target.querySelector('button');
|
||||
const btn = e.target.querySelector('button[type="submit"]');
|
||||
const err = document.getElementById('regerr');
|
||||
const ok = document.getElementById('regok');
|
||||
err.classList.add('hidden'); ok.classList.add('hidden');
|
||||
@@ -70,21 +99,15 @@ const RegisterPage = {
|
||||
source: document.getElementById('rs').value,
|
||||
captcha_id: document.getElementById('captcha-id')?.value,
|
||||
captcha_answer: document.getElementById('captcha-a')?.value.trim(),
|
||||
code_id: this.codeId,
|
||||
code: document.getElementById('vf-code').value.trim(),
|
||||
});
|
||||
ok.textContent = '注册成功!验证码已发送至邮箱,30分钟内有效';
|
||||
ok.textContent = '注册成功!即将跳转登录...';
|
||||
ok.classList.remove('hidden');
|
||||
e.target.innerHTML = `<div class="form-group"><label>邮箱验证码</label>
|
||||
<div style="display:flex;gap:8px"><input id="vf-code" placeholder="6位验证码" maxlength="6" pattern="[0-9]{6}" style="flex:1"><button type="button" class="btn btn-s btn-sm" id="vf-btn" style="white-space:nowrap">验证</button></div>
|
||||
</div>`;
|
||||
document.getElementById('vf-btn').onclick = async () => {
|
||||
const code = document.getElementById('vf-code').value.trim();
|
||||
if (!code) return;
|
||||
try { await API.post('/auth/verify-email', { code }); alert('验证成功!请登录'); location.hash = '#/login'; }
|
||||
catch(ex) { alert(ex.message); }
|
||||
};
|
||||
setTimeout(() => location.hash = '#/login', 2000);
|
||||
} catch (ex) {
|
||||
err.textContent = ex.message; err.classList.remove('hidden');
|
||||
btn.disabled = false; btn.innerHTML = '<i class="fas fa-user-plus"></i> 注册';
|
||||
btn.disabled = false; btn.innerHTML = '<i class="fas fa-user-plus"></i> 完成注册';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user