diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 6def71f..dd25f2a 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -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: '服务器内部错误' }); diff --git a/public/js/pages/register.js b/public/js/pages/register.js index 44b2380..ca273fe 100644 --- a/public/js/pages/register.js +++ b/public/js/pages/register.js @@ -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 = {
-
+
-
-
+
+
${this.captcha ? ` @@ -28,9 +29,18 @@ const RegisterPage = {
点击图片刷新验证码
` : ''} + +
+
+
+ + +
+
+
- +
已有账号?登录 @@ -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 = `
-
-
`; - 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 = ' 注册'; + btn.disabled = false; btn.innerHTML = ' 完成注册'; } }); }