From bedcd4b411483d9fbcaaf399e1d742862f5f3741 Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Sat, 15 Aug 2026 00:43:11 -0500 Subject: [PATCH] fix(account): separate email verification from email change The manage view mixed both workflows into shared code/new-email fields with ambiguous paired buttons. Verification and change are now distinct sections, each with its own inputs and one primary action, and the verification section hides once the email is verified. Co-Authored-By: Claude Fable 5 --- src/internal/account.zig | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/internal/account.zig b/src/internal/account.zig index 082106a..841c590 100644 --- a/src/internal/account.zig +++ b/src/internal/account.zig @@ -48,7 +48,7 @@ const html = \\.shell{width:min(100%,900px);margin:0 auto;padding:28px 16px 42px}a{color:inherit}.brand{display:inline-flex;margin-bottom:42px;text-decoration:none;font-weight:800}h1{margin:0;font-size:clamp(52px,15vw,112px);line-height:.88;letter-spacing:0}h2{margin:0;font-size:1.05rem}h3{margin:18px 0 7px;font-size:.95rem}p{color:var(--muted);margin:12px 0 0;max-width:68ch}strong{color:var(--text)}code{overflow-wrap:anywhere;color:var(--accent)} \\.panel{margin-top:28px;border:1px solid var(--line);border-radius:10px;background:linear-gradient(135deg,color-mix(in srgb,var(--green) 10%,transparent),transparent 46%),linear-gradient(315deg,color-mix(in srgb,var(--accent) 8%,transparent),transparent 44%),var(--panel);padding:18px} \\label{display:block;margin:14px 0 6px;font-weight:760}input,select{width:100%;font:inherit;color:var(--text);background:var(--field);border:1px solid var(--line);border-radius:9px;padding:12px}input:focus-visible,select:focus-visible,button:focus-visible{outline:3px solid var(--focus);outline-offset:2px}button{border:1px solid var(--accent);border-radius:10px;background:var(--accent);color:var(--accent-text);font:inherit;font-weight:800;padding:12px;cursor:pointer}button.secondary{background:transparent;color:var(--text);border-color:var(--line)}button.danger{background:transparent;color:var(--bad);border-color:color-mix(in srgb,var(--bad) 55%,var(--line))}button:disabled{opacity:.62;cursor:not-allowed}.login button{width:100%;margin-top:22px} - \\.status{min-height:1.5em;color:var(--muted)}.error{color:var(--bad)}.ok{color:var(--green)}.warn{color:var(--warn)}.hint{font-size:.92rem;color:var(--muted);margin:8px 0 0}.resident{display:none}.resident.on{display:block}.login.off{display:none} + \\.status{min-height:1.5em;color:var(--muted)}.error{color:var(--bad)}.ok{color:var(--green)}.warn{color:var(--warn)}.hint{font-size:.92rem;color:var(--muted);margin:8px 0 0}.resident{display:none}.resident.on{display:block}.login.off{display:none}.hidden{display:none} \\.account-card{display:grid;grid-template-columns:1.4fr repeat(3,minmax(0,1fr));gap:8px;margin-top:16px}.metric,.identity{border:1px solid var(--line);border-radius:9px;background:color-mix(in srgb,var(--field) 74%,transparent);padding:11px}.metric span,.identity span{display:block;color:var(--muted);font-size:.78rem}.metric strong,.identity strong{display:block;margin-top:4px;font-size:1.2rem;overflow-wrap:anywhere}.identity strong{font-size:1rem} \\.nav{display:flex;gap:8px;overflow-x:auto;margin:18px 0 0;padding-bottom:2px}.nav a{display:inline-flex;text-decoration:none;border:1px solid var(--line);border-radius:999px;padding:8px 11px;color:var(--text);white-space:nowrap}.nav a[aria-current="page"]{background:var(--accent);border-color:var(--accent);color:var(--accent-text);font-weight:800} \\.view{display:none;border-top:1px solid var(--line);padding-top:18px;margin-top:18px}.view.on{display:block}.section{border-top:1px solid var(--line);padding-top:16px;margin-top:18px}.row{display:grid;grid-template-columns:1fr auto;gap:10px;align-items:end}.grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px}.actions button{width:auto}.empty{border:1px dashed var(--line);border-radius:9px;padding:14px;color:var(--muted);margin-top:10px} @@ -113,7 +113,8 @@ const html = \\ \\
\\

manage

- \\

email

+ \\

email

Send yourself a verification email, then enter the code from it.

+ \\

change email

Request an update code at your current address, then enter the new address and the code.

\\

account status

Deactivation hides the account until it is reactivated. Takedowns and suspensions are operator actions, not resident controls.

\\
\\
@@ -138,7 +139,7 @@ const html = \\function showView(name=viewName()){for(const el of document.querySelectorAll('.view'))el.classList.toggle('on',el.id==='view-'+name);for(const a of document.querySelectorAll('.nav a'))a.setAttribute('aria-current',a.dataset.view===name?'page':'false')} \\document.querySelector('.nav').addEventListener('click',e=>{const a=e.target.closest('a[data-view]');if(!a)return;e.preventDefault();history.pushState(null,'',a.href);showView(a.dataset.view)}); \\addEventListener('popstate',()=>showView()); - \\function updateHeader(){ $('account-handle').textContent=account.handle; $('account-did').textContent=account.did; $('account-status').textContent=account.status||'active'; $('account-email-state').textContent=account.emailConfirmed?'verified':'needs verification'; $('email-copy').textContent=`Current email: ${account.email}${account.emailConfirmed?' (verified)':' (not verified)'}`; } + \\function updateHeader(){ $('account-handle').textContent=account.handle; $('account-did').textContent=account.did; $('account-status').textContent=account.status||'active'; $('account-email-state').textContent=account.emailConfirmed?'verified':'needs verification'; $('email-copy').textContent=`Current email: ${account.email}${account.emailConfirmed?' (verified)':' (not verified)'}`; $('verify-email-block').classList.toggle('hidden',!!account.emailConfirmed); } \\function quickChecks(sessions){const rows=[['email',account.emailConfirmed?'verified':'needs verification',account.emailConfirmed?'active':'inactive'],['app access',`${(sessions.oauthGrants||[]).filter(g=>g.active).length} active`, 'active'],['direct API tokens',`${(sessions.sessions||[]).filter(s=>s.active).length} active`, 'active'],['permissioned spaces',`${spaces.length}`, spaces.length?'active':'inactive']];$('home-checks').innerHTML=rows.map(([a,b,c])=>`
${esc(a)}
${esc(b)}
`).join('')} \\const b64ToBuf=(v)=>{const b64=v.replace(/-/g,'+').replace(/_/g,'/');const bin=atob(b64+'==='.slice((b64.length+3)%4));const out=new Uint8Array(bin.length);for(let i=0;ibtoa(String.fromCharCode(...new Uint8Array(buf))).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,''); @@ -167,9 +168,9 @@ const html = \\$('create-app-password').onclick=async()=>{try{const name=$('app-password-name').value.trim(),privileged=$('app-password-privileged').checked;if(!name)throw new Error('Choose a name for this app password.');setStatus('creating app password...');const created=await xrpc('/xrpc/com.atproto.server.createAppPassword',{}, {method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({name,privileged})});generatedAppPassword.classList.add('on');generatedAppPassword.innerHTML='Copy this app password now. It will not be shown again.';generatedAppPassword.querySelector('code').textContent=created.password;$('app-password-name').value='';setStatus('app password created','ok');await loadAccountData()}catch(err){setStatus(err.message||String(err),'error')}} \\$('load-records').onclick=()=>loadRecords(false).catch(err=>setStatus(err.message,'error'));$('next-records').onclick=()=>loadRecords(true).catch(err=>setStatus(err.message,'error')); \\$('verify-email-request').onclick=async()=>{try{await xrpc('/xrpc/com.atproto.server.requestEmailConfirmation',{}, {method:'POST'});setStatus('verification email sent','ok')}catch(err){setStatus(err.message,'error')}} - \\$('verify-email-confirm').onclick=async()=>{try{await xrpc('/xrpc/com.atproto.server.confirmEmail',{}, {method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({email:account.email,token:$('email-token').value.trim()})});setStatus('email verified','ok');await loadAccountData()}catch(err){setStatus(err.message,'error')}} - \\$('update-email-request').onclick=async()=>{try{const out=await xrpc('/xrpc/com.atproto.server.requestEmailUpdate',{}, {method:'POST'});setStatus(out.tokenRequired?'update code sent':'enter the new email and confirm','ok')}catch(err){setStatus(err.message,'error')}} - \\$('update-email-confirm').onclick=async()=>{try{const body={email:$('new-email').value.trim()};if($('email-token').value.trim())body.token=$('email-token').value.trim();await xrpc('/xrpc/com.atproto.server.updateEmail',{}, {method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(body)});setStatus('email updated','ok');await loadAccountData()}catch(err){setStatus(err.message,'error')}} + \\$('verify-email-confirm').onclick=async()=>{try{const token=$('verify-code').value.trim();if(!token)throw new Error('Enter the code from the verification email.');await xrpc('/xrpc/com.atproto.server.confirmEmail',{}, {method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({email:account.email,token})});$('verify-code').value='';setStatus('email verified','ok');await loadAccountData()}catch(err){setStatus(err.message,'error')}} + \\$('update-email-request').onclick=async()=>{try{const out=await xrpc('/xrpc/com.atproto.server.requestEmailUpdate',{}, {method:'POST'});setStatus(out.tokenRequired?'update code sent to your current email':'no code needed — enter the new email and confirm','ok')}catch(err){setStatus(err.message,'error')}} + \\$('update-email-confirm').onclick=async()=>{try{const email=$('new-email').value.trim();if(!email)throw new Error('Enter the new email address.');const body={email};if($('update-code').value.trim())body.token=$('update-code').value.trim();await xrpc('/xrpc/com.atproto.server.updateEmail',{}, {method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(body)});$('new-email').value='';$('update-code').value='';setStatus('email updated','ok');await loadAccountData()}catch(err){setStatus(err.message,'error')}} \\$('activate-account').onclick=async()=>{try{await xrpc('/xrpc/com.atproto.server.activateAccount',{}, {method:'POST'});setStatus('account reactivated','ok');await loadAccountData()}catch(err){setStatus(err.message,'error')}} \\$('deactivate-account').onclick=async()=>{try{if(!confirm('Deactivate this account? You can reactivate by signing back in.'))return;await xrpc('/xrpc/com.atproto.server.deactivateAccount',{}, {method:'POST'});setStatus('account deactivated','warn');await loadAccountData()}catch(err){setStatus(err.message,'error')}} \\showView(); -- 2.51.2