URI '+(j+1)+': '+esc(u.decUri||u.uri||'')+'';}}
+ var cardHtml=''; if(c0.card){var cd=c0.card; cardHtml='Cardholder: '+esc(cd.decCardholderName||cd.cardholderName||'')+'
Number: '+esc(cd.decNumber||cd.number||'')+'
Brand: '+esc(cd.decBrand||cd.brand||'')+'
Exp: '+esc(cd.decExpMonth||cd.expMonth||'')+'/'+esc(cd.decExpYear||cd.expYear||'')+'
CVV: '+esc(cd.decCode||cd.code||'')+'
';}
+ var identHtml=''; if(c0.identity){var id=c0.identity; identHtml='Name: '+esc((id.decFirstName||id.firstName||'')+' '+(id.decLastName||id.lastName||''))+'
Email: '+esc(id.decEmail||id.email||'')+'
Phone: '+esc(id.decPhone||id.phone||'')+'
Company: '+esc(id.decCompany||id.company||'')+'
Username: '+esc(id.decUsername||id.username||'')+'
';}
+ detail=''
+ + 'Name: '+esc(c0.decName||c0.name||'')+'
'
+ + 'Notes: '+esc(c0.decNotes||c0.notes||'')+'
'
+ + (c0.login?('Username: '+esc(login.decUsername||login.username||'')+'
'
+ + 'Password: '+esc(login.decPassword||login.password||'')+'
'
+ + 'TOTP: '+esc(login.decTotp||login.totp||'')+'
'+uriHtml):''
+ ) + cardHtml + identHtml + fh;
+ }
+
+ return ''
+ + renderMsg()
+ + ''
+ + '
'+t('vault')+'
'
+ + '
'
+ + '
'
+ + '';
+ }
+
+ function renderSettingsTab(){
+ var p=state.profile||{};
+ var secret=currentTotpSecret();
+ var qr='https://api.qrserver.com/v1/create-qr-code/?size=180x180&data='+encodeURIComponent(buildTotpUri(secret));
+ return ''
+ + renderMsg()
+ + ''+t('settings')+'
'
+ + ''
+ + ''
+ + ''+t('totpSetup')+'
Disable action prompts for master password.
';
+ }
+ function renderTotpDisableModal(){
+ if(!state.totpDisableOpen) return '';
+ return ''
+ + ''+t('disableTotp')+'
'+t('totpDisableSub')+'
'
+ + (state.totpDisableError?'
'+esc(state.totpDisableError)+'
':'')
+ + '
'
+ + '
';
+ }
+
+ function renderHelpTab(){
+ return ''
+ + ''+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(){
+ var usersRows='';
+ for(var i=0;i'+esc(u.email)+' | '+esc(u.name||'')+' | '+esc(u.role)+' | '+esc(u.status)+' | '
+ + (canAct?'':'')
+ + (canAct?' ':'')
+ + ' | ';
+ }
+ if(!usersRows) usersRows='| No users found. |
';
+
+ var inviteRows='';
+ for(var j=0;j'+esc(inv.code)+' | '+esc(inv.status)+' | '+esc(inv.expiresAt)+' | '
+ + ''
+ + (inv.status==='active'?' ':'')
+ + ' | ';
+ }
+ if(!inviteRows) inviteRows='| No invites found. |
';
+
+ return ''
+ + renderMsg()
+ + ''
+ + '
'+t('admin')+'
'
+ + ''
+ + ''
+ + ''
+ + ''+t('users')+'
| '+t('email')+' | '+t('name')+' | '+t('role')+' | '+t('status')+' | '+t('action')+' |
'+usersRows+'
'
+ + ''+t('invites')+'
| Code | '+t('status')+' | Expires At | '+t('action')+' |
'+inviteRows+'
';
+ }
+
+ function renderApp(){
+ var isAdmin=state.profile&&state.profile.role==='admin';
+ var showFolders=state.tab==='vault';
+ 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();
+
+ return ''
+ + ''
+ + '
'
+ + '
'
+ + '
'
+ + '
'+t('langSwitch')+'
'
+ + '
'+esc(state.profile&&state.profile.email?state.profile.email:'')+''
+ + '
'
+ + '
'
+ + '
'
+ + ''
+ + (showFolders?(' '):'')
+ + ' '+content+''
+ + '
'+renderTotpDisableModal();
+ }
+
+ function render(){
+ if(state.phase==='loading'){ app.innerHTML=''; return; }
+ if(state.phase==='register'){ app.innerHTML=renderRegisterScreen(); return; }
+ if(state.phase==='login'){ app.innerHTML=renderLoginScreen(); return; }
+ app.innerHTML=renderApp();
+ }
+
+ async function init(){
+ var url=new URL(window.location.href); state.inviteCode=(url.searchParams.get('invite')||'').trim(); state.session=loadSession();
+ var st=await fetch('/setup/status'); var setup=await jsonOrNull(st); var registered=!!(setup&&setup.registered);
+ if(state.session){
+ try{ await loadProfile(); await loadVault(); await loadAdminData(); state.phase='app'; state.tab='vault'; render(); return; } catch(e){ state.session=null; saveSession(); }
+ }
+ state.phase=registered?'login':'register'; render();
+ }
+
+ 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');
+ try{
+ var it=defaultKdfIterations; var mk=await pbkdf2(p,email,it,32); var hash=await pbkdf2(mk,p,1,32); var ek=await hkdfExpand(mk,'enc',32); var em=await hkdfExpand(mk,'mac',32); var sym=crypto.getRandomValues(new Uint8Array(64)); var encKey=await encryptBw(sym,ek,em);
+ var kp=await crypto.subtle.generateKey({name:'RSA-OAEP', modulusLength:2048, publicExponent:new Uint8Array([1,0,1]), hash:'SHA-1'}, true, ['encrypt','decrypt']);
+ 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'); }
+ }
+
+ async function onLoginPassword(form){
+ clearMsg();
+ var fd=new FormData(form); state.loginEmail=String(fd.get('email')||'').trim().toLowerCase(); state.loginPassword=String(fd.get('password')||'');
+ if(!state.loginEmail||!state.loginPassword) return setMsg('Please input email and password.', 'err');
+ try{
+ var d=await deriveLoginHash(state.loginEmail,state.loginPassword);
+ var body=new URLSearchParams(); body.set('grant_type','password'); body.set('username',state.loginEmail); body.set('password',d.hash); body.set('scope','api offline_access');
+ var resp=await fetch('/identity/connect/token',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:body.toString()});
+ var j=await jsonOrNull(resp);
+ if(!resp.ok){
+ if(j&&j.TwoFactorProviders){ state.pendingLogin={email:state.loginEmail,passwordHash:d.hash,masterKey:d.masterKey}; state.loginTotpToken=''; state.loginTotpError=''; clearMsg(); render(); return; }
+ return setMsg((j&&(j.error_description||j.error))||'Login failed.', 'err');
+ }
+ await onLoginSuccess(j,d.masterKey,state.loginEmail,state.loginPassword);
+ }catch(e){ setMsg(e&&e.message?e.message:String(e), 'err'); }
+ }
+
+ async function onLoginTotp(form){
+ if(!state.pendingLogin) return setMsg('TOTP flow is not ready.', 'err');
+ var fd=new FormData(form); state.loginTotpToken=String(fd.get('totpToken')||'').trim(); if(!state.loginTotpToken){ state.loginTotpError='Please input TOTP code.'; render(); return; }
+ var b=new URLSearchParams(); b.set('grant_type','password'); b.set('username',state.pendingLogin.email); b.set('password',state.pendingLogin.passwordHash); b.set('scope','api offline_access'); b.set('twoFactorProvider','0'); b.set('twoFactorToken',state.loginTotpToken);
+ var resp=await fetch('/identity/connect/token',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:b.toString()});
+ var j=await jsonOrNull(resp); if(!resp.ok){ state.loginTotpError=(j&&(j.error_description||j.error))||'TOTP verification failed.'; render(); return; }
+ state.loginTotpError='';
+ await onLoginSuccess(j,state.pendingLogin.masterKey,state.pendingLogin.email,state.loginPassword);
+ }
+
+ async function onLoginSuccess(tokenJson, masterKey, email, password){
+ state.session={accessToken:tokenJson.access_token,refreshToken:tokenJson.refresh_token,email:email}; saveSession(); state.pendingLogin=null; state.loginTotpToken=''; state.loginTotpError='';
+ await loadProfile();
+ try{
+ var ek=await hkdfExpand(masterKey,'enc',32); var em=await hkdfExpand(masterKey,'mac',32);
+ var symKeyBytes=await decryptBw(state.profile.key,ek,em);
+ if(symKeyBytes){ state.session.symEncKey=bytesToBase64(symKeyBytes.slice(0,32)); state.session.symMacKey=bytesToBase64(symKeyBytes.slice(32,64)); saveSession(); }
+ }catch(e){ console.warn('Key derivation failed:',e); }
+ await loadVault(); await loadAdminData(); state.phase='app'; state.tab='vault';
+ 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){
+ var fd=new FormData(form); state.totpDisablePassword=String(fd.get('masterPassword')||'');
+ if(!state.totpDisablePassword){ state.totpDisableError='Please input master password.'; render(); return; }
+ try{
+ var d=await deriveLoginHash(state.profile.email,state.totpDisablePassword);
+ var r=await authFetch('/api/accounts/totp',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({enabled:false,masterPasswordHash:d.hash})});
+ var j=await jsonOrNull(r);
+ if(!r.ok){ state.totpDisableError=(j&&(j.error||j.error_description))||'Disable TOTP failed.'; render(); return; }
+ state.totpDisableOpen=false; state.totpDisablePassword=''; state.totpDisableError='';
+ render(); setMsg('TOTP disabled.', 'ok');
+ }catch(e){
+ state.totpDisableError='Disable TOTP failed: '+(e&&e.message?e.message:String(e));
+ render();
+ }
+ }
+
+ async function onBulkDelete(){ var ids=[]; for(var k in state.selectedMap){ if(state.selectedMap[k]) ids.push(k);} if(ids.length===0) return setMsg('Select items first.', 'err'); if(!window.confirm('Delete selected '+ids.length+' items?')) return; for(var i=0;istate.folders.length) return setMsg('Invalid folder selection.', 'err'); var folderId=idx===0?null:state.folders[idx-1].id; var r=await authFetch('/api/ciphers/move',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ids:ids,folderId:folderId})}); var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Bulk move failed.', 'err'); await loadVault(); render(); setMsg('Moved selected items.', 'ok'); }
+
+ async function onCreateInvite(form){ var fd=new FormData(form); var h=Number(fd.get('hours')||168); var r=await authFetch('/api/admin/invites',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({expiresInHours:h})}); var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Create invite failed.', 'err'); await loadAdminData(); render(); setMsg('Invite created.', 'ok'); }
+ async function onToggleUserStatus(id,status){ var n=status==='active'?'banned':'active'; var r=await authFetch('/api/admin/users/'+encodeURIComponent(id)+'/status',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({status:n})}); var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Update user status failed.', 'err'); await loadAdminData(); render(); setMsg('User status updated.', 'ok'); }
+ async function onDeleteUser(id){ if(!window.confirm('Delete this user and all user data?')) return; var r=await authFetch('/api/admin/users/'+encodeURIComponent(id),{method:'DELETE'}); var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Delete user failed.', 'err'); await loadAdminData(); render(); setMsg('User deleted.', 'ok'); }
+ async function onRevokeInvite(code){ var r=await authFetch('/api/admin/invites/'+encodeURIComponent(code),{method:'DELETE'}); var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Revoke invite failed.', 'err'); await loadAdminData(); render(); setMsg('Invite revoked.', 'ok'); }
+
+ app.addEventListener('submit', function(ev){
+ var form=ev.target; if(!(form instanceof HTMLFormElement)) return; ev.preventDefault();
+ if(form.id==='registerForm') return void onRegister(form);
+ 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);
+ });
+
+ 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; }
+ if(a==='totp-cancel'){ state.pendingLogin=null; state.loginTotpToken=''; state.loginTotpError=''; render(); return; }
+ if(a==='totp-disable-cancel'){ state.totpDisableOpen=false; state.totpDisablePassword=''; state.totpDisableError=''; render(); return; }
+ if(a==='tab'){ state.tab=n.getAttribute('data-tab')||'vault'; clearMsg(); render(); return; }
+ if(a==='folder-filter'){ state.folderFilterId=n.getAttribute('data-folder')||''; var filtered=filteredCiphers(); state.selectedCipherId=filtered.length?filtered[0].id:''; render(); return; }
+ if(a==='pick-cipher'){ state.selectedCipherId=n.getAttribute('data-id')||''; render(); return; }
+ if(a==='toggle-select'){ ev.stopPropagation(); state.selectedMap[n.getAttribute('data-id')]=!!n.checked; render(); return; }
+ if(a==='select-all'){ var list=filteredCiphers(); state.selectedMap={}; for(var i=0;i