Something went wrong. Try again.
A lexicon-driven AppView for ATProto.
Something went wrong. Try again.
5.1 kB · 174 lines
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175"use client";
import { useEffect, useState } from "react";import { Card, CardContent, CardDescription, CardHeader, CardTitle,} from "@/components/ui/card";import { Button } from "@/components/ui/button";import { confirmAttachAuth } from "@/lib/api";import { docsUrl } from "@/lib/docs";
const ATTACH_AUTH_STORAGE_KEY = "happyview_attach_auth";const ATTACH_AUTH_MAX_AGE_MS = 10 * 60 * 1000;
interface AttachAuthPayload { attachedDid: string; originalDid: string; timestamp?: number;}
interface SetupAttachAuthProps { attachedDid: string; attachedHandle: string | null; onComplete: () => void; onBack?: () => void;}
export function SetupAttachAuth({ attachedDid, attachedHandle, onComplete, onBack,}: SetupAttachAuthProps) { const [confirming, setConfirming] = useState(false); const [error, setError] = useState<string | null>(null);
useEffect(() => { const stored = localStorage.getItem(ATTACH_AUTH_STORAGE_KEY); if (!stored) return;
let payload: AttachAuthPayload; try { payload = JSON.parse(stored) as AttachAuthPayload; } catch { localStorage.removeItem(ATTACH_AUTH_STORAGE_KEY); return; }
if (payload.attachedDid !== attachedDid) { localStorage.removeItem(ATTACH_AUTH_STORAGE_KEY); return; }
if ( payload.timestamp && Date.now() - payload.timestamp > ATTACH_AUTH_MAX_AGE_MS ) { localStorage.removeItem(ATTACH_AUTH_STORAGE_KEY); setError("Your sign-in session expired. Please authenticate again."); return; }
localStorage.removeItem(ATTACH_AUTH_STORAGE_KEY); setConfirming(true);
confirmAttachAuth({ original_did: payload.originalDid }) .then(() => onComplete()) .catch((e) => { setError( e instanceof Error ? e.message : "Failed to restore admin session. Try authenticating again.", ); setConfirming(false); }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []);
function handleAuthenticate() { setConfirming(true); setError(null);
fetch("/auth/me", { credentials: "same-origin" }) .then((res) => { if (!res.ok) throw new Error("Failed to fetch current user"); return res.json() as Promise<{ did: string }>; }) .then(({ did: originalDid }) => { const payload: AttachAuthPayload = { attachedDid, originalDid, timestamp: Date.now(), }; localStorage.setItem(ATTACH_AUTH_STORAGE_KEY, JSON.stringify(payload));
const handle = attachedHandle ?? attachedDid; return fetch(`/auth/login?handle=${encodeURIComponent(handle)}&scope=${encodeURIComponent("atproto identity:*")}&redirect_uri=${encodeURIComponent("/setup")}`, { credentials: "same-origin", }); }) .then((resp) => { if (!resp.ok) throw new Error("Login request failed"); return resp.json() as Promise<{ url: string }>; }) .then(({ url }) => { window.location.href = url; }) .catch((e) => { setError( e instanceof Error ? e.message : "Failed to start authentication. Check your connection and try again.", ); setConfirming(false); }); }
const displayName = attachedHandle ? `@${attachedHandle}` : attachedDid;
return ( <Card> <CardHeader> <CardTitle>Sign in to verify ownership</CardTitle> {/* <CardDescription> You'll be redirected to sign in as{" "} <span className="font-medium">{displayName}</span>, then returned here automatically. <br /> </CardDescription> */} </CardHeader> <CardContent className="space-y-4"> <p className="text-muted-foreground text-sm"> You'll leave this page briefly to authenticate through the account's data server. Once verified, your admin session will be restored and you'll continue from where you left off. <br /> <a href={docsUrl("/getting-started/service-identity#linked-account")} target="_blank" rel="noopener noreferrer" className="text-primary underline underline-offset-4 hover:text-primary/80" > <small>Learn more</small> </a> </p> {error && ( <p role="alert" className="text-destructive text-sm"> {error} </p> )} {confirming ? ( <p className="text-muted-foreground text-sm" aria-live="polite"> Restoring admin session… </p> ) : ( <div className="flex justify-between"> {onBack ? ( <Button variant="ghost" onClick={onBack}> Back </Button> ) : ( <div /> )} <Button onClick={handleAuthenticate}> Authenticate as {displayName} </Button> </div> )} </CardContent> </Card> );}