Receiving, a home's keys that can't be fetched answer 502, which the sender retries, instead of 401, which it dropped; a bad update answers 400 rather than 502; a blocked instance's events answer 403; events are limited to a burst of 120 per instance, and a token naming an unknown key refetches the keys at most once a minute. An avatar must be on its issuer's origin, and its URL is versioned by its file's write time instead of updatedAt, so a profile edit no longer makes clients download it again. An instance that doesn't answer can be blocked, keeping its name, though not trusted. 303 backend tests and 703 frontend tests pass, the federation tests 8 times in a row, and deno check, tsc, eslint, doc:lint and i18n:compile --strict are clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
85 lines
2.9 KiB
TypeScript
85 lines
2.9 KiB
TypeScript
/**
|
|
* Renders the page another gerbeur instance sends a user back to once they
|
|
* let it sign them in here, `/federation/callback`.
|
|
*
|
|
* @module
|
|
*/
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { Link, useNavigate, useSearchParams } from "react-router";
|
|
import { t } from "@lingui/core/macro";
|
|
import { Trans } from "@lingui/react/macro";
|
|
|
|
import { API_URL } from "../config/api.ts";
|
|
import type { Wire } from "../../shared/api.ts";
|
|
import type { FederationSessionRequest } from "../../shared/federation.ts";
|
|
import type { AuthResponse } from "../../shared/user.ts";
|
|
import { useAuth } from "../hooks/auth/useAuth.ts";
|
|
import { forgetSignIn, startedHere } from "../utils/auth/federation.ts";
|
|
import { useDocumentTitle } from "../hooks/shared/useDocumentTitle.ts";
|
|
import { PageShell } from "../components/layout/PageShell.tsx";
|
|
import { ErrorCard } from "../components/shared/ErrorCard.tsx";
|
|
import { expectOk, friendlyFetchError } from "../utils/shared/api.ts";
|
|
import { deserializeAuthResponse } from "../utils/shared/deserialize.ts";
|
|
|
|
/**
|
|
* Renders `/federation/callback?code=&state=`. It finishes the sign-in
|
|
* (`POST /api/federation/sessions`) if this tab started it (`startedHere`),
|
|
* and once: a code works only once, and a second request would fail the
|
|
* sign-in. On success the user lands on `/`, signed in.
|
|
*/
|
|
export function FederationCallback(): React.JSX.Element {
|
|
useDocumentTitle(t`Log in`);
|
|
const [params] = useSearchParams();
|
|
const navigate = useNavigate();
|
|
const { login } = useAuth();
|
|
const code = params.get("code");
|
|
const state = params.get("state");
|
|
const [error, setError] = useState<string | null>(() =>
|
|
!code || !state
|
|
? t`This sign-in link is incomplete.`
|
|
: !startedHere(state)
|
|
? t`This sign-in was started elsewhere. Start again from the login page.`
|
|
: null
|
|
);
|
|
const sent = useRef(false);
|
|
|
|
useEffect(() => {
|
|
if (sent.current || error || !code || !state) return;
|
|
sent.current = true;
|
|
forgetSignIn();
|
|
fetch(`${API_URL}/api/federation/sessions`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ code, state } satisfies FederationSessionRequest),
|
|
})
|
|
.then((res) => expectOk<Wire<AuthResponse>>(res))
|
|
.then((data) => {
|
|
login(deserializeAuthResponse(data));
|
|
navigate("/", { replace: true });
|
|
})
|
|
.catch((err) => setError(friendlyFetchError(err)));
|
|
}, [code, state, error, login, navigate]);
|
|
|
|
return (
|
|
<PageShell centered>
|
|
{error
|
|
? (
|
|
<div className="page-error-wrap">
|
|
<ErrorCard title={t`Login failed`} message={error} />
|
|
<p className="auth-card-footer">
|
|
<Link to="/login">
|
|
<Trans>Back to the login page</Trans>
|
|
</Link>
|
|
</p>
|
|
</div>
|
|
)
|
|
: (
|
|
<p className="page-loading">
|
|
<Trans>Signing you in…</Trans>
|
|
</p>
|
|
)}
|
|
</PageShell>
|
|
);
|
|
}
|