diff --git a/api/config.ts b/api/config.ts index 7bb9d3d4..5ae743b7 100644 --- a/api/config.ts +++ b/api/config.ts @@ -237,10 +237,11 @@ export const FEDERATION_ALLOW_PRIVATE: boolean = * * - `incoming` (`GERBEUR_FEDERATION_INCOMING`): users of other instances sign * in here. Off, the login and register pages offer no other instance, - * `/manage` no list of instances to trust or block, - * `federation-service.ts` refuses their sign-ins, and the accounts that came - * from one are signed out (`getSessionState`) until it is on again. Their - * homes' events are still applied, so that none is lost meanwhile. + * `/manage` lists only the blocked ones, which this instance's users can't + * sign in to (and none with `outgoing` off too), `federation-service.ts` + * refuses their sign-ins, and the accounts that came from one are signed + * out (`getSessionState`) until it is on again. Their homes' events are + * still applied, so that none is lost meanwhile. * - `outgoing` (`GERBEUR_FEDERATION_OUTGOING`): this instance's users sign in * to others. Off, `identity-provider-service.ts` lets no instance sign them * in, and its metadata says so, which the others check before sending diff --git a/locales/en.po b/locales/en.po index 1bc40909..74d1ee77 100644 --- a/locales/en.po +++ b/locales/en.po @@ -354,7 +354,7 @@ msgstr "Already dumped by {0} {1}" msgid "Already have an account? <0>Log in" msgstr "Already have an account? <0>Log in" -#: src/components/federation/PeerManager.tsx:149 +#: src/components/federation/PeerManager.tsx:167 msgid "An instance's address, like music.example" msgstr "An instance's address, like music.example" @@ -395,11 +395,12 @@ msgstr "Back to login" msgid "Back to the login page" msgstr "Back to the login page" -#: src/components/federation/PeerManager.tsx:163 +#: src/components/federation/PeerManager.tsx:184 +#: src/components/federation/PeerManager.tsx:190 msgid "Block" msgstr "Block" -#: src/components/federation/PeerManager.tsx:33 +#: src/components/federation/PeerManager.tsx:39 msgid "Blocked" msgstr "Blocked" @@ -453,7 +454,7 @@ msgid "Cancel search" msgstr "Cancel search" #: src/components/category/CategorySelect.tsx:33 -#: src/pages/Manage.tsx:38 +#: src/pages/Manage.tsx:42 msgid "Categories" msgstr "Categories" @@ -502,7 +503,8 @@ msgstr "Chat ({unreadChatCount} unread)" msgid "Checking invite…" msgstr "Checking invite…" -#: src/components/federation/PeerManager.tsx:154 +#: src/components/federation/PeerManager.tsx:175 +#: src/components/federation/PeerManager.tsx:189 msgid "Checking…" msgstr "Checking…" @@ -590,7 +592,7 @@ msgstr "Could not rename" msgid "Could not save" msgstr "Could not save" -#: src/components/federation/PeerManager.tsx:146 +#: src/components/federation/PeerManager.tsx:164 msgid "Couldn't add that instance" msgstr "Couldn't add that instance" @@ -885,7 +887,7 @@ msgid "Failed to post reply" msgstr "Failed to post reply" #: src/components/federation/GrantList.tsx:98 -#: src/components/federation/PeerManager.tsx:143 +#: src/components/federation/PeerManager.tsx:158 #: src/components/playlist/PlaylistEditForm.tsx:185 #: src/components/profile/DescriptionEditor.tsx:99 #: src/components/profile/EmailEditor.tsx:86 @@ -1029,7 +1031,7 @@ msgstr "In the chat" msgid "Insert emoji" msgstr "Insert emoji" -#: src/pages/Manage.tsx:45 +#: src/pages/Manage.tsx:49 msgid "Instances" msgstr "Instances" @@ -1145,7 +1147,7 @@ msgstr "Loading profile…" #: src/components/category/CategoryManager.tsx:60 #: src/components/chat/ChatModal.tsx:763 #: src/components/editor/EmojiPickerPopup.tsx:125 -#: src/components/federation/PeerManager.tsx:108 +#: src/components/federation/PeerManager.tsx:120 #: src/components/playlist/PlaylistMembershipPanel.tsx:45 #: src/components/shared/ListState.tsx:38 #: src/components/shared/LoadMoreButton.tsx:32 @@ -1194,8 +1196,8 @@ msgid "Lowercase letters, numbers and single hyphens only" msgstr "Lowercase letters, numbers and single hyphens only" #: src/components/layout/UserMenu.tsx:114 -#: src/pages/Manage.tsx:29 -#: src/pages/Manage.tsx:36 +#: src/pages/Manage.tsx:32 +#: src/pages/Manage.tsx:40 #: src/pages/UserPublicProfile.tsx:164 msgid "Manage" msgstr "Manage" @@ -1538,8 +1540,8 @@ msgid "Related" msgstr "Related" #. placeholder {0}: peer.host -#: src/components/federation/PeerManager.tsx:133 -#: src/components/federation/PeerManager.tsx:134 +#: src/components/federation/PeerManager.tsx:148 +#: src/components/federation/PeerManager.tsx:149 msgid "Remove {0}" msgstr "Remove {0}" @@ -1893,11 +1895,11 @@ msgstr "Top this week" msgid "Track {0}" msgstr "Track {0}" -#: src/components/federation/PeerManager.tsx:155 +#: src/components/federation/PeerManager.tsx:176 msgid "Trust" msgstr "Trust" -#: src/components/federation/PeerManager.tsx:32 +#: src/components/federation/PeerManager.tsx:38 msgid "Trusted" msgstr "Trusted" @@ -2032,7 +2034,15 @@ msgstr "Username" msgid "Users" msgstr "Users" -#: src/components/federation/PeerManager.tsx:169 +#: src/components/federation/PeerManager.tsx:198 +msgid "Users here can't sign in to a blocked instance." +msgstr "Users here can't sign in to a blocked instance." + +#: src/components/federation/PeerManager.tsx:201 +msgid "Users of a trusted instance sign in here with their account there. Those of a blocked one can't, and users here can't sign in to it. Users of any other instance need an invite." +msgstr "Users of a trusted instance sign in here with their account there. Those of a blocked one can't, and users here can't sign in to it. Users of any other instance need an invite." + +#: src/components/federation/PeerManager.tsx:208 msgid "Users of a trusted instance sign in here with their account there. Those of a blocked one can't. Users of any other instance need an invite." msgstr "Users of a trusted instance sign in here with their account there. Those of a blocked one can't. Users of any other instance need an invite." diff --git a/locales/fr.po b/locales/fr.po index c6a80044..edc3e4c1 100644 --- a/locales/fr.po +++ b/locales/fr.po @@ -354,7 +354,7 @@ msgstr "Déjà recommandé par {0} {1}" msgid "Already have an account? <0>Log in" msgstr "Vous avez déjà un compte ? <0>Se connecter" -#: src/components/federation/PeerManager.tsx:149 +#: src/components/federation/PeerManager.tsx:167 msgid "An instance's address, like music.example" msgstr "L'adresse d'une instance, comme music.example" @@ -395,11 +395,12 @@ msgstr "Retour à la connexion" msgid "Back to the login page" msgstr "Retour à la page de connexion" -#: src/components/federation/PeerManager.tsx:163 +#: src/components/federation/PeerManager.tsx:184 +#: src/components/federation/PeerManager.tsx:190 msgid "Block" msgstr "Bloquer" -#: src/components/federation/PeerManager.tsx:33 +#: src/components/federation/PeerManager.tsx:39 msgid "Blocked" msgstr "Bloquée" @@ -453,7 +454,7 @@ msgid "Cancel search" msgstr "Annuler la recherche" #: src/components/category/CategorySelect.tsx:33 -#: src/pages/Manage.tsx:38 +#: src/pages/Manage.tsx:42 msgid "Categories" msgstr "Catégories" @@ -502,7 +503,8 @@ msgstr "{unreadChatCount} messages non lus" msgid "Checking invite…" msgstr "Vérification de l'invitation…" -#: src/components/federation/PeerManager.tsx:154 +#: src/components/federation/PeerManager.tsx:175 +#: src/components/federation/PeerManager.tsx:189 msgid "Checking…" msgstr "Vérification…" @@ -590,7 +592,7 @@ msgstr "Renommage impossible" msgid "Could not save" msgstr "Sauvegarde impossible" -#: src/components/federation/PeerManager.tsx:146 +#: src/components/federation/PeerManager.tsx:164 msgid "Couldn't add that instance" msgstr "Impossible d'ajouter cette instance" @@ -885,7 +887,7 @@ msgid "Failed to post reply" msgstr "Impossible de publier la réponse" #: src/components/federation/GrantList.tsx:98 -#: src/components/federation/PeerManager.tsx:143 +#: src/components/federation/PeerManager.tsx:158 #: src/components/playlist/PlaylistEditForm.tsx:185 #: src/components/profile/DescriptionEditor.tsx:99 #: src/components/profile/EmailEditor.tsx:86 @@ -1029,7 +1031,7 @@ msgstr "Dans la tribune" msgid "Insert emoji" msgstr "Insérer un emoji" -#: src/pages/Manage.tsx:45 +#: src/pages/Manage.tsx:49 msgid "Instances" msgstr "Instances" @@ -1145,7 +1147,7 @@ msgstr "Chargement du profil…" #: src/components/category/CategoryManager.tsx:60 #: src/components/chat/ChatModal.tsx:763 #: src/components/editor/EmojiPickerPopup.tsx:125 -#: src/components/federation/PeerManager.tsx:108 +#: src/components/federation/PeerManager.tsx:120 #: src/components/playlist/PlaylistMembershipPanel.tsx:45 #: src/components/shared/ListState.tsx:38 #: src/components/shared/LoadMoreButton.tsx:32 @@ -1194,8 +1196,8 @@ msgid "Lowercase letters, numbers and single hyphens only" msgstr "Minuscules, chiffres et tirets simples seulement" #: src/components/layout/UserMenu.tsx:114 -#: src/pages/Manage.tsx:29 -#: src/pages/Manage.tsx:36 +#: src/pages/Manage.tsx:32 +#: src/pages/Manage.tsx:40 #: src/pages/UserPublicProfile.tsx:164 msgid "Manage" msgstr "Administration" @@ -1538,8 +1540,8 @@ msgid "Related" msgstr "Connexe" #. placeholder {0}: peer.host -#: src/components/federation/PeerManager.tsx:133 -#: src/components/federation/PeerManager.tsx:134 +#: src/components/federation/PeerManager.tsx:148 +#: src/components/federation/PeerManager.tsx:149 msgid "Remove {0}" msgstr "Retirer {0}" @@ -1893,11 +1895,11 @@ msgstr "Les plus votées de la semaine" msgid "Track {0}" msgstr "Piste {0}" -#: src/components/federation/PeerManager.tsx:155 +#: src/components/federation/PeerManager.tsx:176 msgid "Trust" msgstr "Approuver" -#: src/components/federation/PeerManager.tsx:32 +#: src/components/federation/PeerManager.tsx:38 msgid "Trusted" msgstr "Approuvée" @@ -2032,7 +2034,15 @@ msgstr "Nom d'utilisateur" msgid "Users" msgstr "Utilisateurs" -#: src/components/federation/PeerManager.tsx:169 +#: src/components/federation/PeerManager.tsx:198 +msgid "Users here can't sign in to a blocked instance." +msgstr "Les utilisateurs d'ici ne peuvent pas se connecter à une instance bloquée." + +#: src/components/federation/PeerManager.tsx:201 +msgid "Users of a trusted instance sign in here with their account there. Those of a blocked one can't, and users here can't sign in to it. Users of any other instance need an invite." +msgstr "Les utilisateurs d'une instance approuvée se connectent ici avec leur compte de là-bas. Ceux d'une instance bloquée ne le peuvent pas, et les utilisateurs d'ici ne peuvent pas s'y connecter. Ceux de toute autre instance ont besoin d'une invitation." + +#: src/components/federation/PeerManager.tsx:208 msgid "Users of a trusted instance sign in here with their account there. Those of a blocked one can't. Users of any other instance need an invite." msgstr "Les utilisateurs d'une instance approuvée se connectent ici avec leur compte de là-bas. Ceux d'une instance bloquée ne le peuvent pas. Ceux de toute autre instance ont besoin d'une invitation." diff --git a/src/components/federation/PeerManager.tsx b/src/components/federation/PeerManager.tsx index 6cb920b5..7516e4c5 100644 --- a/src/components/federation/PeerManager.tsx +++ b/src/components/federation/PeerManager.tsx @@ -1,6 +1,8 @@ /** * Renders the list of the other gerbeur instances an admin trusts or blocks, - * on `/manage`, for those who hold `federation:manage`. + * on `/manage`, for those who hold `federation:manage`. Where users of other + * instances don't sign in, only blocking is left: it keeps this instance's + * users from signing in to one. * * @module */ @@ -11,6 +13,10 @@ import { Trans } from "@lingui/react/macro"; import { FormProvider } from "react-hook-form"; import { API_URL } from "../../config/api.ts"; +import { + FEDERATION_INCOMING, + FEDERATION_OUTGOING, +} from "../../config/instance.ts"; import type { Wire } from "../../../shared/api.ts"; import type { FederationPeer, @@ -42,6 +48,8 @@ interface Values { * (`PUT /api/federation/peers`), and a button that takes it off the lists * (`DELETE /api/federation/peers/:host`), then a field that adds one. The * server checks that it answers as a gerbeur, and the permission again. + * Without {@link FEDERATION_INCOMING} it lists and adds only blocked + * instances; the trusted ones are kept for when it is back. */ export function PeerManager(): React.JSX.Element { const { authFetch } = useAuth(); @@ -101,6 +109,10 @@ export function PeerManager(): React.JSX.Element { form.reset(); }); + const shown = FEDERATION_INCOMING + ? peers + : peers?.filter((peer) => peer.status === "blocked") ?? null; + return (
{peers === null && !error && ( @@ -108,9 +120,9 @@ export function PeerManager(): React.JSX.Element { Loading…

)} - {peers && peers.length > 0 && ( + {shown && shown.length > 0 && (
- {peers.map((peer) => ( + {shown.map((peer) => ( - change(() => save(peer.issuer, status))} - /> + {FEDERATION_INCOMING && ( + + change(() => save(peer.issuer, status))} + /> + )} + {FEDERATION_INCOMING + ? ( + <> + Checking…}> + Trust + + + + ) + : ( + Checking…}> + Block + + )}

- - Users of a trusted instance sign in here with their account there. - Those of a blocked one can't. Users of any other instance need an - invite. - + {!FEDERATION_INCOMING + ? Users here can't sign in to a blocked instance. + : FEDERATION_OUTGOING + ? ( + + Users of a trusted instance sign in here with their account there. + Those of a blocked one can't, and users here can't sign in to it. + Users of any other instance need an invite. + + ) + : ( + + Users of a trusted instance sign in here with their account there. + Those of a blocked one can't. Users of any other instance need an + invite. + + )}

); diff --git a/src/pages/Manage.tsx b/src/pages/Manage.tsx index 0463df64..1f168977 100644 --- a/src/pages/Manage.tsx +++ b/src/pages/Manage.tsx @@ -16,20 +16,24 @@ import { PageHeader } from "../components/layout/PageHeader.tsx"; import { PageGroup } from "../components/layout/PageGroup.tsx"; import { CategoryManager } from "../components/category/CategoryManager.tsx"; import { PeerManager } from "../components/federation/PeerManager.tsx"; -import { FEDERATION_INCOMING } from "../config/instance.ts"; +import { + FEDERATION_INCOMING, + FEDERATION_OUTGOING, +} from "../config/instance.ts"; import { NotFound } from "./NotFound.tsx"; /** * Renders the category manager and the instances the site trusts or blocks, * each to those who hold its permission, which the API checks again, the - * instances only while users of other instances may sign in here; without + * instances only while federated sign-in goes at least one way; without * either, the page isn't found. */ export function Manage(): React.JSX.Element { useDocumentTitle(t`Manage`); const { user } = useRequiredAuth(); const categories = can(user, "category:manage"); - const instances = FEDERATION_INCOMING && can(user, "federation:manage"); + const instances = (FEDERATION_INCOMING || FEDERATION_OUTGOING) && + can(user, "federation:manage"); if (!categories && !instances) return ; return ( diff --git a/src/test/pages/Manage.test.tsx b/src/test/pages/Manage.test.tsx index a048f97c..a8eb5464 100644 --- a/src/test/pages/Manage.test.tsx +++ b/src/test/pages/Manage.test.tsx @@ -1,4 +1,4 @@ -import { describe, expect, test } from "vitest"; +import { afterEach, describe, expect, test, vi } from "vitest"; import { screen, waitFor, within } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; import { Manage } from "../../pages/Manage.tsx"; @@ -6,6 +6,20 @@ import { calls, fail, route } from "../fetch.ts"; import { makeCategory, makeSession, wire } from "../fixtures.ts"; import { renderApp } from "../render.tsx"; +const instance = vi.hoisted(() => ({ + federationIncoming: true, + federationOutgoing: true, +})); +vi.mock("../../config/instance.ts", async (importOriginal) => ({ + ...await importOriginal(), + get FEDERATION_INCOMING() { + return instance.federationIncoming; + }, + get FEDERATION_OUTGOING() { + return instance.federationOutgoing; + }, +})); + const admin = makeSession({ id: "ann", username: "ann", role: "admin" }); function open(session = admin) { @@ -16,6 +30,11 @@ function open(session = admin) { } describe("Manage", () => { + afterEach(() => { + instance.federationIncoming = true; + instance.federationOutgoing = true; + }); + test("an admin manages categories", async () => { const music = makeCategory({ id: "c1", slug: "music", name: "Music" }); let list = [music]; @@ -128,6 +147,52 @@ describe("Manage", () => { ); }); + test("where users of other instances don't sign in, an admin only blocks instances", async () => { + instance.federationIncoming = false; + route("GET /api/categories", () => []); + const peer = (name: string, host: string, status: string) => ({ + issuer: `https://${host}`, + host, + name, + status, + createdAt: new Date().toISOString(), + }); + route("GET /api/federation/peers", () => [ + peer("Films", "films.example", "trusted"), + peer("Spam", "spam.example", "blocked"), + ]); + route( + "PUT /api/federation/peers", + () => peer("Music", "music.example", "blocked"), + ); + const user = open(); + expect(await screen.findByText("Spam")).toBeInTheDocument(); + expect(screen.queryByText("Films")).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Trust" })).not + .toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Blocked" })).not + .toBeInTheDocument(); + await user.type( + screen.getByPlaceholderText("An instance's address, like music.example"), + "music.example{Enter}", + ); + await waitFor(() => + expect(calls("PUT /api/federation/peers")[0]?.body).toEqual({ + instance: "music.example", + status: "blocked", + }) + ); + }); + + test("without federated sign-in either way, there are no instances to manage", async () => { + instance.federationIncoming = false; + instance.federationOutgoing = false; + route("GET /api/categories", () => []); + open(); + expect(await screen.findByText("Categories")).toBeInTheDocument(); + expect(screen.queryByText("Instances")).not.toBeInTheDocument(); + }); + test("without category:manage, the page isn't found", async () => { open(makeSession({ id: "mod", role: "moderator" })); expect(await screen.findByText("This page does not exist."))