import { useQuery } from '@tanstack/react-query'; import { useState } from 'react'; import { Button } from '#/components/atoms/button.jsx'; import { Notice } from '#/components/atoms/notice.jsx'; import { TextField } from '#/components/molecules/text-field.jsx'; import { documentHandle, resolveDid } from '#/lib/identity.js'; import { beginSignIn, clientId } from '#/lib/oauth.js'; import { SCOPES } from '#/lib/scopes.js'; /** * The account whose site is serving this page, or null where the page is served * from anywhere else. The field is not filled from it: a reader signs in as * whoever they are, which on a shared deployment is rarely the host. It is kept * for the fallback below, where a handle that resolves nowhere is the host's * own and its DID does resolve. */ function useHostAccount() { return useQuery({ queryKey: ['host-account'], queryFn: async () => { const response = await fetch('/.well-known/atproto-did'); if (!response.ok) return null; const did = (await response.text()).trim(); if (!did.startsWith('did:')) return null; return { did, handle: documentHandle(await resolveDid(did)) }; }, retry: false, staleTime: Number.POSITIVE_INFINITY, }); } /** * Whether this copy of the app is registered for the origin it is served from. * * An OAuth client is named by the URL its registration is served from, so a * copy deployed for one origin cannot sign in from another. The check is * skipped on loopback, where the client_id carries its own registration. */ function useRegistration() { const expected = clientId(SCOPES.read); return useQuery({ queryKey: ['registration', expected], queryFn: async () => { if (!expected.startsWith('https://')) return { ok: true }; const response = await fetch('/client-metadata.json'); if (!response.ok) return { ok: false, found: null }; const found = (await response.json()).client_id; return { ok: found === expected, found }; }, retry: false, staleTime: Number.POSITIVE_INFINITY, }); } export function SignInPage() { const { data: host } = useHostAccount(); const { data: registration } = useRegistration(); const [identifier, setIdentifier] = useState(''); const [access, setAccess] = useState('read'); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); async function submit(event) { event.preventDefault(); setError(null); setBusy(true); try { await beginSignIn(identifier, { scope: SCOPES[access] }); } catch (err) { // The origin publishes its own account's DID, which resolves wherever the // handle beside it does not. A handle on a domain with no public DNS is // the case: a local stack, or an account mid-handle-change. if (host && identifier.trim().toLowerCase() === host.handle) { try { await beginSignIn(host.did, { scope: SCOPES[access] }); return; } catch { // The DID failed too, so the first message is the useful one. } } setError(err.message); setBusy(false); } } return (
Sign in to browse the permissioned spaces your account holds: the records in each one, who writes to them, and what the authority says about them.
Your PDS asks you to approve this app before it sees anything. The access token stays in this browser and is bound to a key this page cannot read, so a copy of it is no use anywhere else.