From bc1b804931def338031ac447c9b5110f3a113368 Mon Sep 17 00:00:00 2001 From: gouki Date: Mon, 10 Aug 2026 14:51:52 +0000 Subject: [PATCH] =?UTF-8?q?feat(account):=20=E8=B4=A6=E5=8F=B7=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E5=AF=86=E7=A0=81=E6=94=AF=E6=8C=81=E6=9F=A5=E7=9C=8B?= =?UTF-8?q?=E6=98=8E=E6=96=87=E2=80=94=E2=80=94=E6=96=B0=E5=A2=9EGET=20/ac?= =?UTF-8?q?counts/{id}/password=E8=A7=A3=E5=AF=86=E6=8E=A5=E5=8F=A3+?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=F0=9F=94=91=E6=9F=A5=E7=9C=8B=E5=BC=B9?= =?UTF-8?q?=E7=AA=97(8s=E8=87=AA=E5=8A=A8=E5=85=B3=E9=97=AD/=E4=B8=80?= =?UTF-8?q?=E9=94=AE=E5=A4=8D=E5=88=B6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routers/accounts.py | 9 +++++ app/services/account_service.py | 14 ++++++++ static/js/modals.js | 58 +++++++++++++++++++++++++++++++-- 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/app/routers/accounts.py b/app/routers/accounts.py index f8d32f6..c8b09bd 100644 --- a/app/routers/accounts.py +++ b/app/routers/accounts.py @@ -55,6 +55,15 @@ def delete_account(account_id: int, session: Session = Depends(get_session)) -> return account_service.delete_account(session, account_id) +@router.get( + "/{account_id}/password", + summary="查看账号登录密码明文(解密返回)", + dependencies=[Depends(require_api_key)], +) +def reveal_password(account_id: int, session: Session = Depends(get_session)) -> dict: + return account_service.reveal_password(session, account_id) + + @router.post( "/{account_id}/test", summary="测试账号凭证有效性(按账号所属平台的 SDK)", diff --git a/app/services/account_service.py b/app/services/account_service.py index 966abdf..408e3c8 100644 --- a/app/services/account_service.py +++ b/app/services/account_service.py @@ -109,3 +109,17 @@ def delete_account(session: Session, account_id: int) -> Dict[str, int]: session.commit() # 资产端保留原账号名文本(不级联清空),由用户自行处理 return {"affected_assets": affected} + + +def reveal_password(session: Session, account_id: int) -> Dict[str, str]: + """解密返回账号登录密码明文(供前端「查看密码」功能)。 + + 安全说明:接口受 API Key 保护;密码本可逆加密存储,此处合法解密还原。 + """ + account = _get_account(session, account_id) + plain = crypto.decrypt(account.login_password_encrypted) + if not plain: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="该账号未配置登录密码" + ) + return {"login_user": account.login_user or "", "password": plain} diff --git a/static/js/modals.js b/static/js/modals.js index ff425e5..9a91b2f 100644 --- a/static/js/modals.js +++ b/static/js/modals.js @@ -291,6 +291,7 @@ const AccountsViewModal = {
{{ a.name }} · 关联 {{ assetsOf(a).length }} 个资产
+ @@ -310,9 +311,31 @@ const AccountsViewModal = { + +
+
+

🔑 {{ pwd.name }} 的登录凭证

+
+
+ 用户名 + {{ pwd.login_user }} +
+
+ 密码 + {{ pwd.password }} +
+
+
+ + +
+

{{ pwd.countdown }}s 后自动关闭

+
+
`, setup() { const expanded = Vue.reactive({}); + const pwd = Vue.reactive({ show: false, name: '', login_user: '', password: '', copied: false, countdown: 0, _timer: null }); const singleProvider = Vue.computed(() => !!store.accountsModal.providerSlug); const title = Vue.computed(() => { const slug = store.accountsModal.providerSlug; @@ -372,9 +395,40 @@ const AccountsViewModal = { Vue.watch(() => store.accountsModal.show, (show) => { if (show) for (const k of Object.keys(expanded)) delete expanded[k]; }); + // 查看密码:拉取明文并展示,8 秒后自动关闭;关闭时清除定时器 + async function viewPwd(a) { + try { + const r = await Api.get('/accounts/' + a.id + '/password'); + pwd.name = a.remark || a.name; + pwd.login_user = r.login_user || ''; + pwd.password = r.password; + pwd.copied = false; + pwd.show = true; + if (pwd._timer) clearInterval(pwd._timer); + pwd.countdown = 8; + pwd._timer = setInterval(() => { + pwd.countdown--; + if (pwd.countdown <= 0) { pwd.show = false; clearInterval(pwd._timer); pwd._timer = null; } + }, 1000); + } catch (e) { alert('获取密码失败:' + e.message); } + } + function copyPwd() { + const done = () => { pwd.copied = true; setTimeout(() => { pwd.copied = false; }, 1500); }; + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(pwd.password).then(done).catch(() => fallbackCopy()); + } else { fallbackCopy(); } + function fallbackCopy() { + const ta = document.createElement('textarea'); + ta.value = pwd.password; document.body.appendChild(ta); ta.select(); + try { document.execCommand('copy'); done(); } catch (e) {} + document.body.removeChild(ta); + } + } + // 关闭弹窗时清定时器,避免泄漏 + Vue.watch(() => pwd.show, (v) => { if (!v && pwd._timer) { clearInterval(pwd._timer); pwd._timer = null; } }); return { - store, Fmt, expanded, singleProvider, title, accountList, groups, - platformName, assetsOf, toggle, canSync, testAcct, syncAcct, + store, Fmt, expanded, pwd, singleProvider, title, accountList, groups, + platformName, assetsOf, toggle, canSync, testAcct, syncAcct, viewPwd, copyPwd, add: () => openAccountCreate(store.accountsModal.providerSlug || ''), edit: openAccountEdit, del: deleteAccount, };