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();