From 1e84e8e2105b66b484601d1cb2c99b4b71f5222a Mon Sep 17 00:00:00 2001 From: guoqingshan Date: Sat, 1 Aug 2026 21:11:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=AF=86=E7=A0=81=E4=B8=8E=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=91=98=E9=87=8D=E7=BD=AE=E5=AF=86=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 ++ admin.html | 13 ++++++++++++- api.js | 8 ++++++++ app.js | 45 +++++++++++++++++++++++++++++++++++++++++++++ index.html | 19 +++++++++++++++++++ server/app.py | 34 +++++++++++++++++++++++++++++++++- server/auth.py | 12 ++++++++++++ style.css | 38 ++++++++++++++++++++++++++++++++++++-- 8 files changed, 167 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2e60fbd..3b596d0 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,9 @@ server/data/spanish_diary.db ## 阶段2 功能 - 用户注册、登录、登出 +- 用户修改密码 - 管理员添加、禁用、删除用户 +- 管理员重置用户密码 - 用户日记数据隔离 - 共享知识库 - 每用户翻译配置(本地 / MyMemory / 自定义接口) diff --git a/admin.html b/admin.html index c410cf3..ea81533 100644 --- a/admin.html +++ b/admin.html @@ -26,7 +26,7 @@ table { width: 100%; border-collapse: collapse; font-size: 0.9rem; } th, td { border: 1px solid var(--border); padding: 0.5rem 0.6rem; text-align: left; } th { background: var(--bg); } - .user-actions { display: flex; gap: 0.35rem; } + .user-actions { display: flex; gap: 0.35rem; flex-wrap: wrap; max-width: 240px; } .user-actions button { border: 1px solid var(--border); background: none; border-radius: 4px; padding: 0.3rem 0.5rem; cursor: pointer; color: var(--text-muted); @@ -88,6 +88,7 @@ '' + esc(u.created_at) + '' + '
' + '' + + '' + '' + '
' + ''; @@ -131,6 +132,16 @@ adminMsg.textContent = '用户已删除'; loadUsers(); }).catch(function(err) { adminMsg.textContent = err.message; }); + } else if (btn.classList.contains('reset-password')) { + var newPassword = prompt('请输入该用户的新密码(至少 6 位):'); + if (newPassword == null) return; + if (newPassword.length < 6) { + adminMsg.textContent = '密码至少 6 位'; + return; + } + apiAdminResetPassword(id, newPassword).then(function() { + adminMsg.textContent = '密码已重置'; + }).catch(function(err) { adminMsg.textContent = err.message; }); } }); diff --git a/api.js b/api.js index 163c5e3..7aec478 100644 --- a/api.js +++ b/api.js @@ -40,6 +40,10 @@ function apiLogout() { return apiFetch('/api/logout', { method: 'POST' }); } +function apiChangePassword(data) { + return apiFetch('/api/change-password', { method: 'POST', body: data }); +} + function apiGetEntries() { return apiFetch('/api/entries'); } @@ -99,3 +103,7 @@ function apiAdminSetStatus(id, isActive) { function apiAdminDeleteUser(id) { return apiFetch('/api/admin/users/' + id, { method: 'DELETE' }); } + +function apiAdminResetPassword(id, password) { + return apiFetch('/api/admin/users/' + id + '/password', { method: 'PUT', body: { password: password } }); +} diff --git a/app.js b/app.js index fef4f86..ab7c239 100644 --- a/app.js +++ b/app.js @@ -380,6 +380,7 @@ function renderUserArea() { if (!currentUser) return; document.getElementById('user-name').textContent = currentUser.displayName || currentUser.username; document.getElementById('btn-logout').style.display = ''; + document.getElementById('btn-change-password').style.display = ''; if (currentUser.isAdmin) { document.getElementById('admin-link').style.display = ''; } @@ -477,6 +478,47 @@ function closeSettingsModal() { document.getElementById('settings-modal-overlay').classList.remove('show'); } +function openPasswordModal() { + document.getElementById('password-msg').textContent = ''; + document.getElementById('password-old').value = ''; + document.getElementById('password-new').value = ''; + document.getElementById('password-confirm').value = ''; + document.getElementById('password-modal-overlay').classList.add('show'); + setTimeout(function() { document.getElementById('password-old').focus(); }, 0); +} + +function closePasswordModal() { + document.getElementById('password-modal-overlay').classList.remove('show'); +} + +function savePasswordModal() { + var oldPassword = document.getElementById('password-old').value; + var newPassword = document.getElementById('password-new').value; + var confirmPassword = document.getElementById('password-confirm').value; + var msg = document.getElementById('password-msg'); + if (!oldPassword) { + msg.textContent = '请输入当前密码'; + return; + } + if (newPassword.length < 6) { + msg.textContent = '新密码至少 6 位'; + return; + } + if (newPassword !== confirmPassword) { + msg.textContent = '两次输入的新密码不一致'; + return; + } + apiChangePassword({ + oldPassword: oldPassword, + newPassword: newPassword + }).then(function() { + closePasswordModal(); + alert('密码已修改'); + }).catch(function(err) { + msg.textContent = err.message || '修改密码失败'; + }); +} + function saveSettingsModal() { var extra = {}; var rawExtra = document.getElementById('extra-config').value.trim(); @@ -530,6 +572,9 @@ function bindAppEvents() { document.getElementById('btn-settings').addEventListener('click', openSettingsModal); document.getElementById('settings-cancel').addEventListener('click', closeSettingsModal); document.getElementById('settings-save').addEventListener('click', saveSettingsModal); + document.getElementById('btn-change-password').addEventListener('click', openPasswordModal); + document.getElementById('password-cancel').addEventListener('click', closePasswordModal); + document.getElementById('password-save').addEventListener('click', savePasswordModal); document.getElementById('btn-logout').addEventListener('click', handleLogout); document.querySelectorAll('#mobile-nav button').forEach(function(btn) { diff --git a/index.html b/index.html index ead9e9f..0944f4e 100644 --- a/index.html +++ b/index.html @@ -50,6 +50,7 @@ 未登录 + @@ -155,6 +156,24 @@ + +
+
+

修改密码

+ + + + + + +
+ +
+
+ diff --git a/server/app.py b/server/app.py index 572de7f..97151d2 100644 --- a/server/app.py +++ b/server/app.py @@ -7,7 +7,7 @@ ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if ROOT_DIR not in sys.path: sys.path.insert(0, ROOT_DIR) -from server.auth import admin_required, authenticate, create_user, login_required +from server.auth import admin_required, authenticate, create_user, login_required, set_password from server.db import BASE_DIR, get_db, init_db from server.entries_api import entries_bp from server.settings_api import settings_bp @@ -80,6 +80,19 @@ def create_app(): session.clear() return jsonify({"ok": True}) + @app.post("/api/change-password") + @login_required + def change_password(user): + data = request.get_json(silent=True) or {} + old_password = data.get("oldPassword") or "" + new_password = data.get("newPassword") or "" + if len(new_password) < 6: + return jsonify({"error": "新密码至少 6 位"}), 400 + if authenticate(user["username"], old_password) is None: + return jsonify({"error": "当前密码不正确"}), 400 + set_password(user["id"], new_password) + return jsonify({"ok": True}) + @app.get("/api/me") @login_required def me(user): @@ -185,6 +198,25 @@ def create_app(): finally: conn.close() + @app.put("/api/admin/users//password") + @admin_required + def admin_reset_password(user, user_id): + data = request.get_json(silent=True) or {} + new_password = data.get("password") or data.get("newPassword") or "" + if len(new_password) < 6: + return jsonify({"error": "新密码至少 6 位"}), 400 + conn = get_db() + try: + target = conn.execute( + "SELECT id FROM users WHERE id = ?", (user_id,) + ).fetchone() + finally: + conn.close() + if target is None: + return jsonify({"error": "用户不存在"}), 404 + set_password(user_id, new_password) + return jsonify({"ok": True}) + @app.get("/") def index(): return send_from_directory(BASE_DIR, "index.html") diff --git a/server/auth.py b/server/auth.py index a82662a..5a0fe45 100644 --- a/server/auth.py +++ b/server/auth.py @@ -71,6 +71,18 @@ def create_user(username, password, display_name, is_admin=False, is_active=True conn.close() +def set_password(user_id, new_password): + conn = get_db() + try: + conn.execute( + "UPDATE users SET password_hash = ? WHERE id = ?", + (generate_password_hash(new_password), user_id), + ) + conn.commit() + finally: + conn.close() + + def authenticate(username, password): conn = get_db() try: diff --git a/style.css b/style.css index c4d968d..5812c76 100644 --- a/style.css +++ b/style.css @@ -46,13 +46,13 @@ body { /* User area and stats */ #user-area { - display: flex; align-items: center; gap: 0.4rem; + display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; padding: 0.6rem 1rem; border-top: 1px solid var(--border); font-size: 0.8rem; flex-shrink: 0; } -#user-area #user-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +#user-area #user-name { flex: 1 1 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #user-area button, #user-area a { background: none; border: 1px solid var(--border); border-radius: 4px; padding: 0.25rem 0.5rem; font-size: 0.75rem; cursor: pointer; @@ -338,6 +338,40 @@ body { } #settings-modal-box .btn-confirm { background: var(--accent); color: #fff; border-color: var(--accent); } +/* Change password modal */ +#password-modal-overlay { + display: none; + position: fixed; inset: 0; + background: rgba(0,0,0,0.4); + align-items: center; justify-content: center; + z-index: 130; +} +#password-modal-overlay.show { display: flex; } +#password-modal-box { + background: var(--panel-bg); border-radius: var(--radius); + padding: 1.5rem; max-width: 420px; width: 90%; + box-shadow: 0 8px 24px rgba(0,0,0,0.15); +} +#password-modal-box h3 { font-size: 1rem; margin-bottom: 0.75rem; } +#password-modal-box label { display: block; font-size: 0.8rem; color: var(--text-muted); margin: 0.6rem 0 0.25rem; } +#password-modal-box input { + width: 100%; border: 1px solid var(--border); border-radius: 6px; + padding: 0.45rem 0.6rem; font-size: 0.85rem; + background: var(--bg); color: var(--text); outline: none; +} +#password-modal-box input:focus { border-color: var(--accent); } +#password-msg { + min-height: 1.4em; font-size: 0.8rem; color: var(--danger); + margin-top: 0.6rem; +} +#password-modal-box .modal-actions { display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 1rem; } +#password-modal-box .modal-actions button { + padding: 0.4rem 1rem; border-radius: 6px; border: 1px solid var(--border); + cursor: pointer; font-size: 0.85rem; +} +#password-modal-box .btn-cancel { background: var(--panel-bg); color: var(--text); } +#password-modal-box .btn-confirm { background: var(--accent); color: #fff; border-color: var(--accent); } + @media (max-width: 768px) { body { padding: 0; } #mobile-nav {