From aefcc3cae2ef56653ec8c98f0825a7817df5ef51 Mon Sep 17 00:00:00 2001
From: Quentin Gliech
Date: Fri, 16 Feb 2024 19:26:38 +0100
Subject: [PATCH] Move the cross signing reset UI in its own page
---
crates/handlers/src/oauth2/discovery.rs | 1 +
crates/router/src/endpoints.rs | 3 +
frontend/src/components/ButtonLink.tsx | 45 +++++
frontend/src/components/{Link => }/Link.tsx | 25 ++-
frontend/src/components/Link/Link.module.css | 31 ----
frontend/src/components/Link/index.ts | 15 --
.../UnverifiedEmailAlert.test.tsx.snap | 2 +-
.../UserProfile/CrossSigningReset.tsx | 79 --------
.../BrowserSessionsOverview.test.tsx.snap | 2 +-
frontend/src/gql/gql.ts | 22 +--
frontend/src/gql/graphql.ts | 168 +++++++++---------
frontend/src/routeTree.gen.ts | 11 ++
frontend/src/routes/__root.tsx | 11 +-
frontend/src/routes/_account.index.tsx | 13 +-
frontend/src/routes/devices.$id.tsx | 2 +-
frontend/src/routes/reset-cross-signing.tsx | 139 +++++++++++++++
16 files changed, 330 insertions(+), 239 deletions(-)
create mode 100644 frontend/src/components/ButtonLink.tsx
rename frontend/src/components/{Link => }/Link.tsx (65%)
delete mode 100644 frontend/src/components/Link/Link.module.css
delete mode 100644 frontend/src/components/Link/index.ts
delete mode 100644 frontend/src/components/UserProfile/CrossSigningReset.tsx
create mode 100644 frontend/src/routes/reset-cross-signing.tsx
diff --git a/crates/handlers/src/oauth2/discovery.rs b/crates/handlers/src/oauth2/discovery.rs
index 56b51af5..3c39a103 100644
--- a/crates/handlers/src/oauth2/discovery.rs
+++ b/crates/handlers/src/oauth2/discovery.rs
@@ -183,6 +183,7 @@ pub(crate) async fn get(
"org.matrix.sessions_list".to_owned(),
"org.matrix.session_view".to_owned(),
"org.matrix.session_end".to_owned(),
+ "org.matrix.cross_signing_reset".to_owned(),
],
})
}
diff --git a/crates/router/src/endpoints.rs b/crates/router/src/endpoints.rs
index d7807e06..17527b48 100644
--- a/crates/router/src/endpoints.rs
+++ b/crates/router/src/endpoints.rs
@@ -475,6 +475,9 @@ pub enum AccountAction {
OrgMatrixSessionEnd { device_id: String },
#[serde(rename = "session_end")]
SessionEnd { device_id: String },
+
+ #[serde(rename = "org.matrix.cross_signing_reset")]
+ OrgMatrixCrossSigningReset,
}
/// `GET /account/`
diff --git a/frontend/src/components/ButtonLink.tsx b/frontend/src/components/ButtonLink.tsx
new file mode 100644
index 00000000..4a03372e
--- /dev/null
+++ b/frontend/src/components/ButtonLink.tsx
@@ -0,0 +1,45 @@
+// Copyright 2024 The Matrix.org Foundation C.I.C.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { LinkComponent, useLinkProps } from "@tanstack/react-router";
+import { Button } from "@vector-im/compound-web";
+import { forwardRef } from "react";
+
+type Props = {
+ kind?: "primary" | "secondary" | "tertiary";
+ size?: "sm" | "lg";
+ Icon?: React.ComponentType>;
+ destructive?: boolean;
+};
+
+export const ButtonLink: LinkComponent = forwardRef<
+ HTMLAnchorElement,
+ Parameters[0] & Props
+>(({ children, kind, size, destructive, Icon, ...props }, ref) => {
+ const linkProps = useLinkProps(props);
+
+ return (
+
+ );
+}) as LinkComponent;
diff --git a/frontend/src/components/Link/Link.tsx b/frontend/src/components/Link.tsx
similarity index 65%
rename from frontend/src/components/Link/Link.tsx
rename to frontend/src/components/Link.tsx
index 2d6de97f..07a1d5b4 100644
--- a/frontend/src/components/Link/Link.tsx
+++ b/frontend/src/components/Link.tsx
@@ -14,24 +14,21 @@
import { LinkComponent, useLinkProps } from "@tanstack/react-router";
import { Link as CompoundLink } from "@vector-im/compound-web";
-import cx from "classnames";
import { forwardRef } from "react";
-import styles from "./Link.module.css";
+type Props = {
+ kind?: "primary" | "critical";
+};
-export const Link: LinkComponent = forwardRef<
+export const Link: LinkComponent = forwardRef<
HTMLAnchorElement,
- Parameters[0]
->(({ children, ...props }, ref) => {
- const { className, ...newProps } = useLinkProps(props);
+ Parameters[0] & Props
+>(({ children, kind, ...props }, ref) => {
+ const linkProps = useLinkProps(props);
return (
-
+
+ {children}
+
);
-}) as LinkComponent;
+}) as LinkComponent;
diff --git a/frontend/src/components/Link/Link.module.css b/frontend/src/components/Link/Link.module.css
deleted file mode 100644
index 757988dd..00000000
--- a/frontend/src/components/Link/Link.module.css
+++ /dev/null
@@ -1,31 +0,0 @@
-/* Copyright 2023 The Matrix.org Foundation C.I.C.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-.link-button {
- display: inline-block;
- text-decoration: underline;
- color: var(--cpd-color-text-primary);
- font-weight: var(--cpd-font-weight-medium);
- border-radius: var(--cpd-radius-pill-effect);
- padding-inline: 0.25rem;
-}
-
-.link-button:hover {
- background: var(--cpd-color-gray-300);
-}
-
-.link-button:active {
- color: var(--cpd-color-text-on-solid-primary);
-}
diff --git a/frontend/src/components/Link/index.ts b/frontend/src/components/Link/index.ts
deleted file mode 100644
index 7389ee40..00000000
--- a/frontend/src/components/Link/index.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright 2023 The Matrix.org Foundation C.I.C.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-export { Link } from "./Link";
diff --git a/frontend/src/components/UnverifiedEmailAlert/__snapshots__/UnverifiedEmailAlert.test.tsx.snap b/frontend/src/components/UnverifiedEmailAlert/__snapshots__/UnverifiedEmailAlert.test.tsx.snap
index cb149f96..3cbca6f8 100644
--- a/frontend/src/components/UnverifiedEmailAlert/__snapshots__/UnverifiedEmailAlert.test.tsx.snap
+++ b/frontend/src/components/UnverifiedEmailAlert/__snapshots__/UnverifiedEmailAlert.test.tsx.snap
@@ -36,7 +36,7 @@ exports[` > renders a warning when there are unverified
You have 2 unverified email addresses.
= ({ userId }) => {
- const { t } = useTranslation();
- const [result, allowReset] = useMutation(ALLOW_CROSS_SIGING_RESET_MUTATION);
-
- const onClick = (): void => {
- allowReset({ userId });
- };
-
- return (
-
- {t("frontend.reset_cross_signing.heading")}
- {!result.data && !result.error && (
- <>
-
- {t("frontend.reset_cross_signing.description")}
-
-
- >
- )}
- {result.data && (
-
- {t("frontend.reset_cross_signing.success.description")}
-
- )}
- {result.error && (
-
- {t("frontend.reset_cross_signing.failure.description")}
-
- )}
-
- );
-};
-
-export default CrossSigningReset;
diff --git a/frontend/src/components/UserSessionsOverview/__snapshots__/BrowserSessionsOverview.test.tsx.snap b/frontend/src/components/UserSessionsOverview/__snapshots__/BrowserSessionsOverview.test.tsx.snap
index a7ef3a68..5a060079 100644
--- a/frontend/src/components/UserSessionsOverview/__snapshots__/BrowserSessionsOverview.test.tsx.snap
+++ b/frontend/src/components/UserSessionsOverview/__snapshots__/BrowserSessionsOverview.test.tsx.snap
@@ -22,7 +22,7 @@ exports[`BrowserSessionsOverview > renders with sessions 1`] = `
;
-
-export type AllowCrossSigningResetMutation = {
- __typename?: "Mutation";
- allowUserCrossSigningReset: {
- __typename?: "AllowUserCrossSigningResetPayload";
- user?: { __typename?: "User"; id: string } | null;
- };
-};
-
export type UserEmailListQueryQueryVariables = Exact<{
userId: Scalars["ID"]["input"];
first?: InputMaybe;
@@ -1698,7 +1686,9 @@ export type CurrentViewerQueryQueryVariables = Exact<{ [key: string]: never }>;
export type CurrentViewerQueryQuery = {
__typename?: "Query";
- viewer: { __typename: "Anonymous" } | { __typename: "User"; id: string };
+ viewer:
+ | { __typename: "Anonymous"; id: string }
+ | { __typename: "User"; id: string };
};
export type DeviceRedirectQueryQueryVariables = Exact<{
@@ -1729,6 +1719,18 @@ export type VerifyEmailQueryQuery = {
| null;
};
+export type AllowCrossSigningResetMutationVariables = Exact<{
+ userId: Scalars["ID"]["input"];
+}>;
+
+export type AllowCrossSigningResetMutation = {
+ __typename?: "Mutation";
+ allowUserCrossSigningReset: {
+ __typename?: "AllowUserCrossSigningResetPayload";
+ user?: { __typename?: "User"; id: string } | null;
+ };
+};
+
export type CurrentViewerSessionQueryQueryVariables = Exact<{
[key: string]: never;
}>;
@@ -3160,75 +3162,6 @@ export const AddEmailDocument = {
},
],
} as unknown as DocumentNode;
-export const AllowCrossSigningResetDocument = {
- kind: "Document",
- definitions: [
- {
- kind: "OperationDefinition",
- operation: "mutation",
- name: { kind: "Name", value: "AllowCrossSigningReset" },
- variableDefinitions: [
- {
- kind: "VariableDefinition",
- variable: {
- kind: "Variable",
- name: { kind: "Name", value: "userId" },
- },
- type: {
- kind: "NonNullType",
- type: { kind: "NamedType", name: { kind: "Name", value: "ID" } },
- },
- },
- ],
- selectionSet: {
- kind: "SelectionSet",
- selections: [
- {
- kind: "Field",
- name: { kind: "Name", value: "allowUserCrossSigningReset" },
- arguments: [
- {
- kind: "Argument",
- name: { kind: "Name", value: "input" },
- value: {
- kind: "ObjectValue",
- fields: [
- {
- kind: "ObjectField",
- name: { kind: "Name", value: "userId" },
- value: {
- kind: "Variable",
- name: { kind: "Name", value: "userId" },
- },
- },
- ],
- },
- },
- ],
- selectionSet: {
- kind: "SelectionSet",
- selections: [
- {
- kind: "Field",
- name: { kind: "Name", value: "user" },
- selectionSet: {
- kind: "SelectionSet",
- selections: [
- { kind: "Field", name: { kind: "Name", value: "id" } },
- ],
- },
- },
- ],
- },
- },
- ],
- },
- },
- ],
-} as unknown as DocumentNode<
- AllowCrossSigningResetMutation,
- AllowCrossSigningResetMutationVariables
->;
export const UserEmailListQueryDocument = {
kind: "Document",
definitions: [
@@ -4576,7 +4509,7 @@ export const CurrentViewerQueryDocument = {
kind: "InlineFragment",
typeCondition: {
kind: "NamedType",
- name: { kind: "Name", value: "User" },
+ name: { kind: "Name", value: "Node" },
},
selectionSet: {
kind: "SelectionSet",
@@ -4748,6 +4681,75 @@ export const VerifyEmailQueryDocument = {
VerifyEmailQueryQuery,
VerifyEmailQueryQueryVariables
>;
+export const AllowCrossSigningResetDocument = {
+ kind: "Document",
+ definitions: [
+ {
+ kind: "OperationDefinition",
+ operation: "mutation",
+ name: { kind: "Name", value: "AllowCrossSigningReset" },
+ variableDefinitions: [
+ {
+ kind: "VariableDefinition",
+ variable: {
+ kind: "Variable",
+ name: { kind: "Name", value: "userId" },
+ },
+ type: {
+ kind: "NonNullType",
+ type: { kind: "NamedType", name: { kind: "Name", value: "ID" } },
+ },
+ },
+ ],
+ selectionSet: {
+ kind: "SelectionSet",
+ selections: [
+ {
+ kind: "Field",
+ name: { kind: "Name", value: "allowUserCrossSigningReset" },
+ arguments: [
+ {
+ kind: "Argument",
+ name: { kind: "Name", value: "input" },
+ value: {
+ kind: "ObjectValue",
+ fields: [
+ {
+ kind: "ObjectField",
+ name: { kind: "Name", value: "userId" },
+ value: {
+ kind: "Variable",
+ name: { kind: "Name", value: "userId" },
+ },
+ },
+ ],
+ },
+ },
+ ],
+ selectionSet: {
+ kind: "SelectionSet",
+ selections: [
+ {
+ kind: "Field",
+ name: { kind: "Name", value: "user" },
+ selectionSet: {
+ kind: "SelectionSet",
+ selections: [
+ { kind: "Field", name: { kind: "Name", value: "id" } },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ ],
+} as unknown as DocumentNode<
+ AllowCrossSigningResetMutation,
+ AllowCrossSigningResetMutationVariables
+>;
export const CurrentViewerSessionQueryDocument = {
kind: "Document",
definitions: [
diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts
index 46ba9112..7357818c 100644
--- a/frontend/src/routeTree.gen.ts
+++ b/frontend/src/routeTree.gen.ts
@@ -11,6 +11,7 @@
// Import Routes
import { Route as rootRoute } from './routes/__root'
+import { Route as ResetCrossSigningImport } from './routes/reset-cross-signing'
import { Route as AccountImport } from './routes/_account'
import { Route as AccountIndexImport } from './routes/_account.index'
import { Route as DevicesIdImport } from './routes/devices.$id'
@@ -22,6 +23,11 @@ import { Route as AccountSessionsIdImport } from './routes/_account.sessions.$id
// Create/Update Routes
+const ResetCrossSigningRoute = ResetCrossSigningImport.update({
+ path: '/reset-cross-signing',
+ getParentRoute: () => rootRoute,
+} as any)
+
const AccountRoute = AccountImport.update({
id: '/_account',
getParentRoute: () => rootRoute,
@@ -70,6 +76,10 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AccountImport
parentRoute: typeof rootRoute
}
+ '/reset-cross-signing': {
+ preLoaderRoute: typeof ResetCrossSigningImport
+ parentRoute: typeof rootRoute
+ }
'/clients/$id': {
preLoaderRoute: typeof ClientsIdImport
parentRoute: typeof rootRoute
@@ -110,6 +120,7 @@ export const routeTree = rootRoute.addChildren([
AccountSessionsBrowsersRoute,
AccountSessionsIndexRoute,
]),
+ ResetCrossSigningRoute,
ClientsIdRoute,
DevicesIdRoute,
EmailsIdVerifyRoute,
diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx
index d24c00d2..d93f87fe 100644
--- a/frontend/src/routes/__root.tsx
+++ b/frontend/src/routes/__root.tsx
@@ -40,6 +40,9 @@ const actionSchema = z
action: z.enum(["session_end", "org.matrix.session_end"]),
device_id: z.string().optional(),
}),
+ z.object({
+ action: z.literal("org.matrix.cross_signing_reset"),
+ }),
z.object({
action: z.undefined(),
}),
@@ -53,7 +56,7 @@ export const Route = createRootRouteWithContext<{
}>()({
validateSearch: (search): Action => actionSchema.parse(search),
- beforeLoad: ({ search }) => {
+ beforeLoad({ search }) {
switch (search.action) {
case "profile":
case "org.matrix.profile":
@@ -80,6 +83,12 @@ export const Route = createRootRouteWithContext<{
params: { id: search.device_id },
});
throw redirect({ to: "/sessions" });
+
+ case "org.matrix.cross_signing_reset":
+ throw redirect({
+ to: "/reset-cross-signing",
+ search: { deepLink: true },
+ });
}
},
diff --git a/frontend/src/routes/_account.index.tsx b/frontend/src/routes/_account.index.tsx
index 6a96a108..f22db457 100644
--- a/frontend/src/routes/_account.index.tsx
+++ b/frontend/src/routes/_account.index.tsx
@@ -13,14 +13,15 @@
// limitations under the License.
import { createFileRoute, notFound } from "@tanstack/react-router";
+import IconKey from "@vector-im/compound-design-tokens/icons/key.svg?react";
import { H3, Separator } from "@vector-im/compound-web";
import { Suspense } from "react";
import { useTranslation } from "react-i18next";
import { useQuery } from "urql";
import BlockList from "../components/BlockList/BlockList";
+import { ButtonLink } from "../components/ButtonLink";
import LoadingSpinner from "../components/LoadingSpinner";
-import CrossSigningReset from "../components/UserProfile/CrossSigningReset";
import UserEmailList from "../components/UserProfile/UserEmailList";
import UserName from "../components/UserProfile/UserName";
import { graphql } from "../gql";
@@ -71,7 +72,15 @@ function Index(): React.ReactElement {
-
+
+
+ {t("frontend.reset_cross_signing.heading")}
+
>
);
diff --git a/frontend/src/routes/devices.$id.tsx b/frontend/src/routes/devices.$id.tsx
index 68d0e0a0..0ef4e815 100644
--- a/frontend/src/routes/devices.$id.tsx
+++ b/frontend/src/routes/devices.$id.tsx
@@ -23,7 +23,7 @@ const CURRENT_VIEWER_QUERY = graphql(/* GraphQL */ `
query CurrentViewerQuery {
viewer {
__typename
- ... on User {
+ ... on Node {
id
}
}
diff --git a/frontend/src/routes/reset-cross-signing.tsx b/frontend/src/routes/reset-cross-signing.tsx
new file mode 100644
index 00000000..a88f740f
--- /dev/null
+++ b/frontend/src/routes/reset-cross-signing.tsx
@@ -0,0 +1,139 @@
+// Copyright 2024 The Matrix.org Foundation C.I.C.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { createFileRoute, notFound } from "@tanstack/react-router";
+import IconArrowLeft from "@vector-im/compound-design-tokens/icons/arrow-left.svg?react";
+import IconKey from "@vector-im/compound-design-tokens/icons/key.svg?react";
+import { Alert, Button, Text } from "@vector-im/compound-web";
+import { useTranslation } from "react-i18next";
+import { useMutation, useQuery } from "urql";
+import { z } from "zod";
+
+import BlockList from "../components/BlockList";
+import { ButtonLink } from "../components/ButtonLink";
+import LoadingSpinner from "../components/LoadingSpinner";
+import PageHeading from "../components/PageHeading";
+import { graphql } from "../gql";
+
+const searchSchema = z.object({
+ deepLink: z.boolean().optional(),
+});
+
+type Search = z.infer;
+
+const CURRENT_VIEWER_QUERY = graphql(/* GraphQL */ `
+ query CurrentViewerQuery {
+ viewer {
+ __typename
+ ... on Node {
+ id
+ }
+ }
+ }
+`);
+
+const ALLOW_CROSS_SIGING_RESET_MUTATION = graphql(/* GraphQL */ `
+ mutation AllowCrossSigningReset($userId: ID!) {
+ allowUserCrossSigningReset(input: { userId: $userId }) {
+ user {
+ id
+ }
+ }
+ }
+`);
+
+export const Route = createFileRoute("/reset-cross-signing")({
+ async loader({ context, abortController: { signal } }) {
+ const viewer = await context.client.query(
+ CURRENT_VIEWER_QUERY,
+ {},
+ { fetchOptions: { signal } },
+ );
+ if (viewer.error) throw viewer.error;
+ if (viewer.data?.viewer.__typename !== "User") throw notFound();
+ },
+
+ validateSearch: (search): Search => searchSchema.parse(search),
+
+ component: ResetCrossSigning,
+});
+
+function ResetCrossSigning(): React.ReactNode {
+ const { deepLink } = Route.useSearch();
+ const { t } = useTranslation();
+ const [viewer] = useQuery({ query: CURRENT_VIEWER_QUERY });
+ if (viewer.error) throw viewer.error;
+ if (viewer.data?.viewer.__typename !== "User") throw notFound();
+ const userId = viewer.data.viewer.id;
+
+ const [result, allowReset] = useMutation(ALLOW_CROSS_SIGING_RESET_MUTATION);
+
+ const onClick = (): void => {
+ allowReset({ userId });
+ };
+
+ return (
+
+
+
+ {!result.data && !result.error && (
+ <>
+
+ {t("frontend.reset_cross_signing.description")}
+
+
+ >
+ )}
+ {result.data && (
+
+ {t("frontend.reset_cross_signing.success.description")}
+
+ )}
+ {result.error && (
+
+ {t("frontend.reset_cross_signing.failure.description")}
+
+ )}
+
+ {!deepLink && (
+
+ {t("action.back")}
+
+ )}
+
+ );
+}