From 0f64af42a1ae5da6099a6f6c40725372dd14d388 Mon Sep 17 00:00:00 2001 From: canglan Date: Wed, 15 Jul 2026 02:45:33 +0800 Subject: [PATCH] feat: password reset + data-action delegation for all onclick --- backend/routes/auth.js | 36 +++++++++++++++++++++++++ public/index.html | 1 + public/js/app.js | 5 +++- public/js/pages/forgot.js | 56 +++++++++++++++++++++++++++++++++++++++ public/js/pages/login.js | 6 +++-- 5 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 public/js/pages/forgot.js diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 4259e71..7fa3949 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -205,4 +205,40 @@ router.put('/password', authenticate, async (req, res) => { } }); +router.post('/forgot-password', async (req, res) => { + try { + const { email } = req.body; + if (!email) return res.status(400).json({ error: '请输入邮箱' }); + const user = await getRow('SELECT * FROM users WHERE email = ?', [email]); + if (!user) return res.json({ message: '如果邮箱已注册,重置链接已发送' }); + const resetToken = uuid(); + await query("UPDATE users SET reset_token = ?, reset_expires = DATE_ADD(NOW(), INTERVAL 1 HOUR) WHERE id = ?", [resetToken, user.id]); + const site = await getRow("SELECT v FROM settings WHERE k='site_url'"); + await sendEmail(email, 'verify_email', { + username: user.username, game_name: user.game_name, game_uid: user.game_uid, + verify_link: `${site?.v||'http://localhost:3100'}#/reset?token=${resetToken}`, + }); + res.json({ message: '如果邮箱已注册,重置链接已发送' }); + } catch (err) { + console.error('[auth]', err); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +router.post('/reset-password', async (req, res) => { + try { + const { token, password } = req.body; + if (!token || !password) return res.status(400).json({ error: '参数不完整' }); + if (password.length < 6) return res.status(400).json({ error: '密码至少6位' }); + const user = await getRow("SELECT * FROM users WHERE reset_token = ? AND reset_expires > NOW()", [token]); + if (!user) return res.status(400).json({ error: '重置链接无效或已过期' }); + const hashed = await bcrypt.hash(password, 10); + await query("UPDATE users SET password = ?, reset_token = NULL, reset_expires = NULL WHERE id = ?", [hashed, user.id]); + res.json({ message: '密码重置成功,请登录' }); + } catch (err) { + console.error('[auth]', err); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + module.exports = router; diff --git a/public/index.html b/public/index.html index 517cfa3..7f20a76 100644 --- a/public/index.html +++ b/public/index.html @@ -57,6 +57,7 @@ + diff --git a/public/js/app.js b/public/js/app.js index becf556..caedc7b 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -63,6 +63,7 @@ const App = { if (page === 'install') { this.showPublic('install'); return; } if (page === 'home') { this.showPublic('home', param); return; } if (page === 'login' || page === 'register') { this.showPublic(page); return; } + if (page === 'forgot' || page === 'reset') { this.showPublic(page); return; } if (!Auth.logged()) { location.hash = '#/login'; return; } this.showLayout(); @@ -98,7 +99,7 @@ const App = { const pub = document.getElementById('public-page'); pub.classList.remove('hidden'); this.updateTopAuth(); - const comp = page === 'login' ? LoginPage : page === 'register' ? RegisterPage : page === 'install' ? InstallPage : HomePage; + const comp = page === 'login' ? LoginPage : page === 'register' ? RegisterPage : page === 'install' ? InstallPage : page === 'forgot' ? ForgotPage : page === 'reset' ? ResetPage : HomePage; comp.render(param).then(html => { pub.innerHTML = html; if (comp.mount) comp.mount(param); }).catch(() => { pub.innerHTML = '
'; }); @@ -167,6 +168,8 @@ window.PollsPage = PollsPage; window.FeaturesPage = FeaturesPage; window.LoginPage = LoginPage; window.RegisterPage = RegisterPage; +window.ForgotPage = ForgotPage; +window.ResetPage = ResetPage; window.HomePage = HomePage; window.InstallPage = InstallPage; window.U = U; diff --git a/public/js/pages/forgot.js b/public/js/pages/forgot.js new file mode 100644 index 0000000..8533fcd --- /dev/null +++ b/public/js/pages/forgot.js @@ -0,0 +1,56 @@ +const ForgotPage = { + async render() { + return `
+
+ +
+
返回登录
+
`; + }, + mount() { + document.getElementById('forgot-form').addEventListener('submit', async e => { + e.preventDefault(); + try { + await API.post('/auth/forgot-password', { email: document.getElementById('fg-email').value }); + const el = document.getElementById('fg-msg'); + el.className = 'alert alert-s'; + el.textContent = '如果邮箱已注册,重置链接已发送'; + el.classList.remove('hidden'); + } catch(ex) { alert(ex.message); } + }); + } +}; + +const ResetPage = { + async render() { + const params = new URLSearchParams((location.hash.split('?')[1] || '')); + const token = params.get('token'); + if (!token) return `
返回登录
`; + return `
+
+ + +
返回登录
`; + }, + mount() { + const params = new URLSearchParams((location.hash.split('?')[1] || '')); + const token = params.get('token'); + if (!token) return; + document.getElementById('reset-form').addEventListener('submit', async e => { + e.preventDefault(); + try { + await API.post('/auth/reset-password', { token, password: document.getElementById('rs-pwd').value }); + const el = document.getElementById('rs-msg'); + el.className = 'alert alert-s'; + el.textContent = '密码重置成功!请登录'; + el.classList.remove('hidden'); + setTimeout(() => location.hash = '#/login', 2000); + } catch(ex) { + const el = document.getElementById('rs-msg'); + el.className = 'alert alert-e'; + el.textContent = ex.message; + el.classList.remove('hidden'); + } + }); + } +}; diff --git a/public/js/pages/login.js b/public/js/pages/login.js index 6fb9051..e5e703f 100644 --- a/public/js/pages/login.js +++ b/public/js/pages/login.js @@ -4,7 +4,7 @@ const LoginPage = {
-
@@ -12,6 +12,8 @@ const LoginPage = {
还没有账号?立即注册 | + 忘记密码 + | 匿名提交
`; @@ -22,7 +24,7 @@ const LoginPage = { e.preventDefault(); const btn = e.target.querySelector('button'); btn.disabled = true; - btn.innerHTML = ' 登录?..'; + btn.innerHTML = ' 登录�?..'; try { await Auth.login(document.getElementById('lu').value.trim(), document.getElementById('lp').value.trim()); App.afterLogin();