From 90da97c945cb13684e4b07e99e33fa01ce8e8bea Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Thu, 26 Feb 2026 04:26:19 +0800 Subject: [PATCH] feat: enhance registration and password management UI with additional state handling --- src/handlers/web.ts | 69 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 6 deletions(-) diff --git a/src/handlers/web.ts b/src/handlers/web.ts index 229a3b0..f07fc03 100644 --- a/src/handlers/web.ts +++ b/src/handlers/web.ts @@ -234,6 +234,10 @@ function renderWebClientHTML(): string { msg: '', msgType: 'ok', inviteCode: '', + registerName: '', + registerEmail: '', + registerPassword: '', + registerPassword2: '', session: null, profile: null, tab: 'vault', @@ -348,7 +352,7 @@ function renderWebClientHTML(): string { var it=Number(d.kdfIterations||defaultKdfIterations); var mk=await pbkdf2(password,email.toLowerCase(),it,32); var h=await pbkdf2(mk,password,1,32); - return { hash: bytesToBase64(h), masterKey: mk }; + return { hash: bytesToBase64(h), masterKey: mk, kdfIterations: it }; } function logout(){ @@ -376,6 +380,20 @@ function renderWebClientHTML(): string { function randomBase32Secret(len){ var a='ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; var b=crypto.getRandomValues(new Uint8Array(len)); var o=''; for(var i=0;i
' @@ -404,9 +422,9 @@ function renderWebClientHTML(): string { + '

Register

' + renderMsg() + '
' - + '
' - + '
' - + '
' + + '
' + + '
' + + '
' + '
' + '
' + '
' @@ -458,6 +476,7 @@ function renderWebClientHTML(): string { return '' + renderMsg() + '

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.
'; } function renderTotpDisableModal(){ @@ -471,8 +490,9 @@ function renderWebClientHTML(): string { function renderHelpTab(){ return '' - + '

Upstream Sync

  • Use fork + GitHub Actions scheduled sync.
  • Or use manual Sync fork from repository page.
  • Deploy updated branch in Cloudflare Worker after sync.
' - + '

Common Errors

  • 401 Unauthorized: login again.
  • 429 Too many requests: wait and retry.
  • 403 Invite invalid: check invite status and expiry.
  • Disabled user cannot login.
'; + + '

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.
'; } function renderAdminTab(){ @@ -543,6 +563,7 @@ function renderWebClientHTML(): string { async function onRegister(form){ clearMsg(); var fd=new FormData(form); var name=String(fd.get('name')||'').trim(); var email=String(fd.get('email')||'').trim().toLowerCase(); var p=String(fd.get('password')||''); var p2=String(fd.get('password2')||''); var invite=String(fd.get('inviteCode')||'').trim(); + state.registerName=name; state.registerEmail=email; state.registerPassword=p; state.registerPassword2=p2; state.inviteCode=invite; if(!email||!p) return setMsg('Please input email and password.', 'err'); if(p.length<12) return setMsg('Master password must be at least 12 chars.', 'err'); if(p!==p2) return setMsg('Passwords do not match.', 'err'); @@ -552,6 +573,7 @@ function renderWebClientHTML(): string { var pub=new Uint8Array(await crypto.subtle.exportKey('spki',kp.publicKey)); var prv=new Uint8Array(await crypto.subtle.exportKey('pkcs8',kp.privateKey)); var encPrv=await encryptBw(prv,sym.slice(0,32),sym.slice(32,64)); var resp=await fetch('/api/accounts/register',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email:email,name:name,masterPasswordHash:bytesToBase64(hash),key:encKey,kdf:0,kdfIterations:it,inviteCode:invite||undefined,keys:{publicKey:bytesToBase64(pub),encryptedPrivateKey:encPrv}})}); var j=await jsonOrNull(resp); if(!resp.ok) return setMsg((j&&(j.error||j.error_description))||'Register failed.', 'err'); + state.registerName=''; state.registerEmail=''; state.registerPassword=''; state.registerPassword2=''; state.inviteCode=''; state.phase='login'; state.loginEmail=email; state.loginPassword=''; setMsg('Registration succeeded. Please sign in.', 'ok'); }catch(e){ setMsg(e&&e.message?e.message:String(e), 'err'); } } @@ -595,6 +617,40 @@ function renderWebClientHTML(): string { setMsg('Login success.', 'ok'); } async function onSaveProfile(form){ var fd=new FormData(form); var n=String(fd.get('name')||'').trim(); var em=String(fd.get('email')||'').trim().toLowerCase(); var r=await authFetch('/api/accounts/profile',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:n,email:em})}); var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Save profile failed.', 'err'); state.profile=j; render(); setMsg('Profile updated.', 'ok'); } + async function onChangePassword(form){ + var fd=new FormData(form); + var currentPassword=String(fd.get('currentPassword')||''); + var newPassword=String(fd.get('newPassword')||''); + var newPassword2=String(fd.get('newPassword2')||''); + if(!currentPassword||!newPassword) return setMsg('Current/new password is required.', 'err'); + if(newPassword.length<12) return setMsg('New master password must be at least 12 chars.', 'err'); + if(newPassword!==newPassword2) return setMsg('New passwords do not match.', 'err'); + if(newPassword===currentPassword) return setMsg('New password must be different.', 'err'); + var email=String(state.profile&&state.profile.email?state.profile.email:'').toLowerCase(); + if(!email) return setMsg('Profile email missing.', 'err'); + try{ + var current=await deriveLoginHash(email,currentPassword); + var userSym=buildSymmetricKeyBytes(); + if(!userSym){ + var oldEk=await hkdfExpand(current.masterKey,'enc',32); + var oldEm=await hkdfExpand(current.masterKey,'mac',32); + userSym=await decryptBw(state.profile.key,oldEk,oldEm); + } + if(!userSym||userSym.length<64) return setMsg('Unable to load vault key for password rotation.', 'err'); + var nextMasterKey=await pbkdf2(newPassword,email,current.kdfIterations,32); + var nextHash=await pbkdf2(nextMasterKey,newPassword,1,32); + var nextEk=await hkdfExpand(nextMasterKey,'enc',32); + var nextEm=await hkdfExpand(nextMasterKey,'mac',32); + var newKey=await encryptBw(userSym.slice(0,64),nextEk,nextEm); + var r=await authFetch('/api/accounts/password',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({currentPasswordHash:current.hash,newMasterPasswordHash:bytesToBase64(nextHash),newKey:newKey,kdf:0,kdfIterations:current.kdfIterations})}); + var j=await jsonOrNull(r); + if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Change master password failed.', 'err'); + logout(); + setMsg('Master password changed. Please log in again.', 'ok'); + }catch(e){ + setMsg('Change master password failed: '+(e&&e.message?e.message:String(e)), 'err'); + } + } async function onEnableTotp(form){ var fd=new FormData(form); state.totpSetupSecret=String(fd.get('secret')||'').toUpperCase().replace(/[\\s-]/g,'').replace(/=+$/g,''); state.totpSetupToken=String(fd.get('token')||'').trim(); if(!state.totpSetupSecret) return setMsg('TOTP secret is required.', 'err'); if(!state.totpSetupToken) return setMsg('TOTP token is required.', 'err'); var r=await authFetch('/api/accounts/totp',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({enabled:true,secret:state.totpSetupSecret,token:state.totpSetupToken})}); var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Enable TOTP failed.', 'err'); state.totpSetupToken=''; render(); setMsg('TOTP enabled.', 'ok'); } function onDisableTotp(){ state.totpDisableOpen=true; state.totpDisablePassword=''; state.totpDisableError=''; render(); } async function onDisableTotpSubmit(form){ @@ -627,6 +683,7 @@ function renderWebClientHTML(): string { if(form.id==='loginForm') return void onLoginPassword(form); if(form.id==='loginTotpForm') return void onLoginTotp(form); if(form.id==='profileForm') return void onSaveProfile(form); + if(form.id==='passwordForm') return void onChangePassword(form); if(form.id==='totpEnableForm') return void onEnableTotp(form); if(form.id==='totpDisableForm') return void onDisableTotpSubmit(form); if(form.id==='inviteForm') return void onCreateInvite(form);