feat(web): automatic re-attach on connect + AccountPanel fallback action (re-attach spec §3.4)

This commit is contained in:
Jannis Braun
2026-07-03 02:19:51 +02:00
parent d45366c4ff
commit 344a429e98
6 changed files with 341 additions and 8 deletions
@@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from 'react';
import { useState, useEffect, useRef, useMemo } from 'react';
import { useAuthStore } from '../../../stores/authStore';
import { useUIStore } from '../../../stores/uiStore';
import { useInstanceStore } from '../../../stores/instanceStore';
@@ -77,6 +77,45 @@ export function AccountPanel() {
const instances = useInstanceStore((s) => s.instances);
const changePassword = useAuthStore((s) => s.changePassword);
// ── Detached-account re-attach (fallback path, re-attach spec §3.4) ──
// Explicit action shown only when this client also holds an active connection
// to the account's home domain. Two-step armed confirm names both identities
// before minting the proof. The primary/automatic path lives in instanceStore.
const [reattachArmed, setReattachArmed] = useState(false);
const [reattaching, setReattaching] = useState(false);
const [reattachError, setReattachError] = useState<string | null>(null);
const homeConnection = useMemo(() => {
if (!user?.homeInstance) return null;
const homeDomain = user.homeInstance.replace(/^https?:\/\//, '').replace(/\/+$/, '').toLowerCase();
return instances.find(
(i) => i.status === 'connected'
&& i.origin.replace(/^https?:\/\//, '').replace(/\/+$/, '').toLowerCase() === homeDomain,
) ?? null;
}, [instances, user?.homeInstance]);
const handleReattach = async () => {
if (!homeConnection) return;
if (!reattachArmed) {
setReattachArmed(true);
return;
}
setReattaching(true);
setReattachError(null);
try {
// Target domain = THIS instance (where the detached account lives).
const { token } = await homeConnection.api.auth.attachProof(window.location.host);
const res = await api.users.reattach({ token });
useAuthStore.getState().setUser(res.user);
addToast(`Account re-linked with ${homeConnection.username}`, 'success', 3000);
} catch (err) {
setReattachError(err instanceof Error ? err.message : 'Re-attach failed');
} finally {
setReattaching(false);
setReattachArmed(false);
}
};
if (!user) return null;
const effectiveDisplayName = displayName.trim() || user.username;
@@ -269,6 +308,25 @@ export function AccountPanel() {
<span className="font-medium text-txt-primary">This account is detached from its home instance.</span>{' '}
{user.homeInstance} was reset or is no longer available, so this account now operates locally on
this instance your profile and password are managed here.
{homeConnection && (
<>
{' '}As <span className="font-medium text-txt-primary">{homeConnection.username}</span> on{' '}
{user.homeInstance}, you can re-link this account profile and presence will sync from there again.
<button
type="button"
onClick={handleReattach}
disabled={reattaching}
className="mt-2 block rounded-md bg-accent-amber/20 hover:bg-accent-amber/30 disabled:opacity-50 text-txt-primary px-3 py-1.5 text-xs font-medium transition-colors"
>
{reattaching
? 'Re-attaching…'
: reattachArmed
? `Confirm re-attach as ${homeConnection.username}`
: `Re-attach to ${user.homeInstance}`}
</button>
{reattachError && <div className="mt-1.5 text-accent-rose">{reattachError}</div>}
</>
)}
</div>
)}
{/* ── Profile Customization ── */}