From ee784d18db8d5a44fd118259181846fcbc1cc533 Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Thu, 26 Feb 2026 05:35:29 +0800 Subject: [PATCH] Implement code changes to enhance functionality and improve performance --- src/handlers/web.ts | 844 ++++++++++++++++++++++++++++---------------- 1 file changed, 536 insertions(+), 308 deletions(-) diff --git a/src/handlers/web.ts b/src/handlers/web.ts index c4d618a..313d1f0 100644 --- a/src/handlers/web.ts +++ b/src/handlers/web.ts @@ -13,298 +13,352 @@ function renderWebClientHTML(): string { NodeWarden Web @@ -316,6 +370,7 @@ function renderWebClientHTML(): string { var defaultKdfIterations = ${defaultKdfIterations}; var state = { phase: 'loading', + lang: (navigator.language || '').toLowerCase().startsWith('zh') ? 'zh' : 'en', msg: '', msgType: 'ok', inviteCode: '', @@ -346,13 +401,162 @@ function renderWebClientHTML(): string { }; var NO_FOLDER_FILTER = '__none__'; + var i18n = { + en: { + brand: 'NodeWarden', + subtitle: 'Open Source Password Manager', + login: 'Log In', + register: 'Create Account', + email: 'Email Address', + masterPwd: 'Master Password', + confirmPwd: 'Confirm Master Password', + name: 'Name', + inviteCode: 'Invite Code (Optional)', + loginBtn: 'Log In', + registerBtn: 'Create Account', + backToLogin: 'Back to Log In', + vault: 'Vault', + settings: 'Settings', + admin: 'Admin', + help: 'Help', + logout: 'Log Out', + folders: 'Folders', + allItems: 'All Items', + noFolder: 'No Folder', + refresh: 'Refresh', + move: 'Move', + delete: 'Delete', + selectAll: 'Select All', + clear: 'Clear', + noItems: 'There are no items to list.', + selectItem: 'Select an item to view details.', + profile: 'Profile', + saveProfile: 'Save Profile', + changePwd: 'Change Master Password', + currentPwd: 'Current Master Password', + newPwd: 'New Master Password', + totpSetup: 'Two-Step Login (TOTP)', + enableTotp: 'Enable TOTP', + disableTotp: 'Disable TOTP', + secret: 'Authenticator Key', + verifyCode: 'Verification Code', + users: 'Users', + invites: 'Invites', + createInvite: 'Create Invite', + expiresIn: 'Expires in (hours)', + copyLink: 'Copy Link', + revoke: 'Revoke', + ban: 'Ban', + unban: 'Unban', + status: 'Status', + role: 'Role', + action: 'Options', + loading: 'Loading NodeWarden...', + totpVerify: 'Two-step verification', + totpVerifySub: 'Password is already verified.', + totpCode: 'TOTP Code', + verify: 'Verify', + cancel: 'Cancel', + totpDisableSub: 'Enter master password to disable two-step verification.', + helpSync: 'Upstream Sync', + helpSync1: 'Track upstream with a fork and scheduled sync workflow (recommended).', + helpSync2: 'Before merge: compare API routes, migration files, and auth logic changes.', + helpSync3: 'After merge: run local dev migration tests, then deploy Worker after validation.', + helpErr: 'Common Errors', + helpErr1: '401 Unauthorized: token expired or revoked, login again.', + helpErr2: '403 Account disabled: admin must unban user in User Management.', + helpErr3: '403 Invite invalid: invite expired/used/revoked, create a new invite.', + helpErr4: '429 Too many requests: wait retry seconds and avoid burst writes.', + helpTb: 'Troubleshooting', + helpTb1: 'Login OK but encrypted values shown: verify profile key and KDF settings are consistent.', + helpTb2: 'TOTP fails repeatedly: sync device time and re-scan QR using latest secret.', + helpTb3: 'Password change failed: ensure current password is correct and new password has at least 12 chars.', + helpTb4: 'Sync conflicts: refresh vault and retry one operation at a time.', + langSwitch: '中文' + }, + zh: { + brand: 'NodeWarden', + subtitle: '开源密码管理器', + login: '登录', + register: '创建账号', + email: '电子邮件地址', + masterPwd: '主密码', + confirmPwd: '确认主密码', + name: '姓名', + inviteCode: '邀请码 (可选)', + loginBtn: '登录', + registerBtn: '创建账号', + backToLogin: '返回登录', + vault: '密码库', + settings: '设置', + admin: '管理', + help: '帮助', + logout: '退出登录', + folders: '文件夹', + allItems: '所有项目', + noFolder: '无文件夹', + refresh: '刷新', + move: '移动', + delete: '删除', + selectAll: '全选', + clear: '清除', + noItems: '没有可列出的项目。', + selectItem: '选择一个项目以查看详细信息。', + profile: '个人资料', + saveProfile: '保存个人资料', + changePwd: '更改主密码', + currentPwd: '当前主密码', + newPwd: '新主密码', + totpSetup: '两步登录 (TOTP)', + enableTotp: '启用 TOTP', + disableTotp: '禁用 TOTP', + secret: '身份验证器密钥', + verifyCode: '验证码', + users: '用户', + invites: '邀请', + createInvite: '创建邀请', + expiresIn: '过期时间 (小时)', + copyLink: '复制链接', + revoke: '撤销', + ban: '封禁', + unban: '解封', + status: '状态', + role: '角色', + action: '选项', + loading: '正在加载 NodeWarden...', + totpVerify: '两步验证', + totpVerifySub: '密码已验证。', + totpCode: 'TOTP 验证码', + verify: '验证', + cancel: '取消', + totpDisableSub: '输入主密码以禁用两步验证。', + helpSync: '上游同步', + helpSync1: '建议通过 fork 和定时同步工作流跟踪上游。', + helpSync2: '合并前:比较 API 路由、迁移文件和认证逻辑的更改。', + helpSync3: '合并后:运行本地开发迁移测试,验证后部署 Worker。', + helpErr: '常见错误', + helpErr1: '401 未授权:令牌过期或被撤销,请重新登录。', + helpErr2: '403 账号被禁用:管理员必须在用户管理中解封用户。', + helpErr3: '403 邀请无效:邀请已过期/已使用/被撤销,请创建新邀请。', + helpErr4: '429 请求过多:等待重试时间,避免突发写入。', + helpTb: '故障排除', + helpTb1: '登录成功但显示加密值:验证个人资料密钥和 KDF 设置是否一致。', + helpTb2: 'TOTP 反复失败:同步设备时间并使用最新密钥重新扫描二维码。', + helpTb3: '密码更改失败:确保当前密码正确且新密码至少 12 个字符。', + helpTb4: '同步冲突:刷新密码库并一次重试一个操作。', + langSwitch: 'English' + } + }; + + function t(key) { return i18n[state.lang][key] || key; } + function esc(v) { return String(v == null ? '' : v).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); } function sessionKey() { return 'nodewarden.web.session.v2'; } function setMsg(t, ty) { state.msg = t || ''; state.msgType = ty || 'ok'; render(); } function clearMsg() { state.msg = ''; } - function renderMsg() { return state.msg ? '
' + esc(state.msg) + '
' : ''; } + function renderMsg() { return state.msg ? '
' + esc(state.msg) + '
' : ''; } function saveSession() { if (state.session) localStorage.setItem(sessionKey(), JSON.stringify(state.session)); else localStorage.removeItem(sessionKey()); } function loadSession() { try { var r = localStorage.getItem(sessionKey()); if (!r) return null; var p = JSON.parse(r); if (!p || !p.accessToken || !p.refreshToken) return null; return p; } catch (e) { return null; } } function bytesToBase64(bytes) { var s=''; for (var i=0;i
' - + ' ' - + '

Sign In

' + + '
' + + '
'+t('langSwitch')+'
' + + '
' + + '
' + + ' ' + + '
'+t('brand')+'
' + + '
'+t('subtitle')+'
' + + '
' + renderMsg() + '
' - + '
' - + '
' - + '
' + + '
' + + '
' + + ' ' + '
' + + ' ' + (state.pendingLogin ? '' - + '

Two-step verification

Password is already verified.
' - + (state.loginTotpError?'
'+esc(state.loginTotpError)+'
':'') - + '
' + + '

'+t('totpVerify')+'

'+t('totpVerifySub')+'
' + + (state.loginTotpError?'
'+esc(state.loginTotpError)+'
':'') + + '
' + '
' : '') - + '
' - + '
'; + + ' ' + + ''; } function renderRegisterScreen(){ return '' - + '
' - + ' ' - + '

Register

' + + '
' + + '
'+t('langSwitch')+'
' + + '
' + + '
' + + ' ' + + '
'+t('register')+'
' + + '
'+t('brand')+'
' + + '
' + renderMsg() + '
' - + '
' - + '
' - + '
' - + '
' - + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + ' ' + '
' - + '
' - + '
'; + + ' ' + + ' ' + + ''; } function renderVaultTab(){ @@ -525,10 +746,10 @@ function renderWebClientHTML(): string { var nameText=(c.decName||c.name||c.id); rows += '
'+esc(nameText)+'
'+esc(c.id)+'
'; } - if(!rows) rows='
No items in this folder.
'; + if(!rows) rows='
'+t('noItems')+'
'; var c0=selectedCipher(); - var detail='
Select an item to view details.
'; + var detail='
'+t('selectItem')+'
'; if(c0){ var login = c0.login||{}; var fields=Array.isArray(c0.fields)?c0.fields:[]; @@ -549,8 +770,8 @@ function renderWebClientHTML(): string { return '' + renderMsg() + '
' - + '

Vault

' - + '
' + + '

'+t('vault')+'

' + + '
' + '
' + '
'+rows+'
'+detail+'
'; } @@ -561,26 +782,26 @@ function renderWebClientHTML(): string { var qr='https://api.qrserver.com/v1/create-qr-code/?size=180x180&data='+encodeURIComponent(buildTotpUri(secret)); return '' + renderMsg() - + '

Settings

' - + '

Profile

' - + '

Master Password

After success, current sessions are revoked and you must log in again.
' - + '

TOTP Setup

TOTP QR
Disable action prompts for master password.
'; + + '

'+t('settings')+'

' + + '

'+t('profile')+'

' + + '

'+t('changePwd')+'

After success, current sessions are revoked and you must log in again.
' + + '

'+t('totpSetup')+'

TOTP QR
Disable action prompts for master password.
'; } function renderTotpDisableModal(){ if(!state.totpDisableOpen) return ''; return '' - + '

Disable TOTP

Enter master password to disable two-step verification.
' - + (state.totpDisableError?'
'+esc(state.totpDisableError)+'
':'') - + '
' + + '

'+t('disableTotp')+'

'+t('totpDisableSub')+'
' + + (state.totpDisableError?'
'+esc(state.totpDisableError)+'
':'') + + '
' + '
'; } function renderHelpTab(){ return '' - + '

Help & Support

' - + '

Upstream Sync

  • Track upstream with a fork and scheduled sync workflow (recommended).
  • Before merge: compare API routes, migration files, and auth logic changes.
  • After merge: run local dev migration tests, then deploy Worker after validation.
' - + '

Common Errors

  • 401 Unauthorized: token expired or revoked, login again.
  • 403 Account disabled: admin must unban user in User Management.
  • 403 Invite invalid: invite expired/used/revoked, create a new invite.
  • 429 Too many requests: wait retry seconds and avoid burst writes.
' - + '

Troubleshooting

  • Login OK but encrypted values shown: verify profile key and KDF settings are consistent.
  • TOTP fails repeatedly: sync device time and re-scan QR using latest secret.
  • Password change failed: ensure current password is correct and new password has at least 12 chars.
  • Sync conflicts: refresh vault and retry one operation at a time.
'; + + '

'+t('help')+'

' + + '

'+t('helpSync')+'

  • '+t('helpSync1')+'
  • '+t('helpSync2')+'
  • '+t('helpSync3')+'
' + + '

'+t('helpErr')+'

  • '+t('helpErr1')+'
  • '+t('helpErr2')+'
  • '+t('helpErr3')+'
  • '+t('helpErr4')+'
' + + '

'+t('helpTb')+'

  • '+t('helpTb1')+'
  • '+t('helpTb2')+'
  • '+t('helpTb3')+'
  • '+t('helpTb4')+'
'; } function renderAdminTab(){ @@ -588,8 +809,8 @@ function renderWebClientHTML(): string { for(var i=0;i'+esc(u.name||'')+''+esc(u.role)+''+esc(u.status)+'' - + (canAct?'':'') - + (canAct?' ':'') + + (canAct?'':'') + + (canAct?' ':'') + ''; } if(!usersRows) usersRows='No users found.'; @@ -598,8 +819,8 @@ function renderWebClientHTML(): string { for(var j=0;j'+esc(inv.status)+''+esc(inv.expiresAt)+'' - + '' - + (inv.status==='active'?' ':'') + + '' + + (inv.status==='active'?' ':'') + ''; } if(!inviteRows) inviteRows='No invites found.'; @@ -607,39 +828,45 @@ function renderWebClientHTML(): string { return '' + renderMsg() + '
' - + '

User Management

' - + '' + + '

'+t('admin')+'

' + + '' + '
' - + '

Create Invite

' - + '

Users

'+usersRows+'
EmailNameRoleStatusAction
' - + '

Invites

'+inviteRows+'
CodeStatusExpires AtAction
'; + + '

'+t('createInvite')+'

' + + '

'+t('users')+'

'+usersRows+'
'+t('email')+''+t('name')+''+t('role')+''+t('status')+''+t('action')+'
' + + '

'+t('invites')+'

'+inviteRows+'
Code'+t('status')+'Expires At'+t('action')+'
'; } function renderApp(){ var isAdmin=state.profile&&state.profile.role==='admin'; var showFolders=state.tab==='vault'; - var folders='' - + ''; + var folders='' + + ''; for(var i=0;i'+esc(folderName)+''; } var content = state.tab==='vault'?renderVaultTab():state.tab==='settings'?renderSettingsTab():(state.tab==='admin'&&isAdmin)?renderAdminTab():renderHelpTab(); - var layoutClass=showFolders?'vault-layout':'normal-layout'; + return '' - + '
' - + ' ' - + (showFolders?(' '):'') + + '' + + '
' + + (showFolders?(' '):'') + '
'+content+'
' - + '
'+renderTotpDisableModal()+'
'; + + '
'+renderTotpDisableModal(); } function render(){ - if(state.phase==='loading'){ app.innerHTML='
NW
Loading NodeWarden...
'; return; } + if(state.phase==='loading'){ app.innerHTML='
'+t('loading')+'
'; return; } if(state.phase==='register'){ app.innerHTML=renderRegisterScreen(); return; } if(state.phase==='login'){ app.innerHTML=renderLoginScreen(); return; } app.innerHTML=renderApp(); @@ -785,6 +1012,7 @@ function renderWebClientHTML(): string { app.addEventListener('click', function(ev){ var n=ev.target; while(n&&n!==app&&!n.getAttribute('data-action')) n=n.parentElement; if(!n||n===app) return; var a=n.getAttribute('data-action'); if(!a) return; + if(a==='toggle-lang'){ state.lang = state.lang === 'zh' ? 'en' : 'zh'; render(); return; } if(a==='goto-login'){ state.phase='login'; clearMsg(); render(); return; } if(a==='goto-register'){ state.phase='register'; clearMsg(); render(); return; } if(a==='logout'){ if(window.confirm('Log out now?')) logout(); return; }