1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-08-07 17:03:01 +03:00

Move the cross signing reset UI in its own page

This commit is contained in:
Quentin Gliech
2024-02-16 19:26:38 +01:00
parent 0d4b941b43
commit aefcc3cae2
16 changed files with 330 additions and 239 deletions

View File

@@ -183,6 +183,7 @@ pub(crate) async fn get(
"org.matrix.sessions_list".to_owned(), "org.matrix.sessions_list".to_owned(),
"org.matrix.session_view".to_owned(), "org.matrix.session_view".to_owned(),
"org.matrix.session_end".to_owned(), "org.matrix.session_end".to_owned(),
"org.matrix.cross_signing_reset".to_owned(),
], ],
}) })
} }

View File

@@ -475,6 +475,9 @@ pub enum AccountAction {
OrgMatrixSessionEnd { device_id: String }, OrgMatrixSessionEnd { device_id: String },
#[serde(rename = "session_end")] #[serde(rename = "session_end")]
SessionEnd { device_id: String }, SessionEnd { device_id: String },
#[serde(rename = "org.matrix.cross_signing_reset")]
OrgMatrixCrossSigningReset,
} }
/// `GET /account/` /// `GET /account/`

View File

@@ -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<React.SVGAttributes<SVGElement>>;
destructive?: boolean;
};
export const ButtonLink: LinkComponent<Props> = forwardRef<
HTMLAnchorElement,
Parameters<typeof useLinkProps>[0] & Props
>(({ children, kind, size, destructive, Icon, ...props }, ref) => {
const linkProps = useLinkProps(props);
return (
<Button
as="a"
kind={kind}
size={size}
destructive={destructive}
Icon={Icon}
ref={ref}
{...linkProps}
>
{children}
</Button>
);
}) as LinkComponent<Props>;

View File

@@ -14,24 +14,21 @@
import { LinkComponent, useLinkProps } from "@tanstack/react-router"; import { LinkComponent, useLinkProps } from "@tanstack/react-router";
import { Link as CompoundLink } from "@vector-im/compound-web"; import { Link as CompoundLink } from "@vector-im/compound-web";
import cx from "classnames";
import { forwardRef } from "react"; import { forwardRef } from "react";
import styles from "./Link.module.css"; type Props = {
kind?: "primary" | "critical";
};
export const Link: LinkComponent = forwardRef< export const Link: LinkComponent<Props> = forwardRef<
HTMLAnchorElement, HTMLAnchorElement,
Parameters<typeof useLinkProps>[0] Parameters<typeof useLinkProps>[0] & Props
>(({ children, ...props }, ref) => { >(({ children, kind, ...props }, ref) => {
const { className, ...newProps } = useLinkProps(props); const linkProps = useLinkProps(props);
return ( return (
<CompoundLink <CompoundLink kind={kind} ref={ref} {...linkProps}>
kind="primary" {children}
ref={ref} </CompoundLink>
className={cx(className, styles.linkButton)}
children={children}
{...newProps}
/>
); );
}) as LinkComponent; }) as LinkComponent<Props>;

View File

@@ -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);
}

View File

@@ -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";

View File

@@ -36,7 +36,7 @@ exports[`<UnverifiedEmailAlert /> > renders a warning when there are unverified
You have 2 unverified email addresses. You have 2 unverified email addresses.
<a <a
class="_link_1mzip_17 active _linkButton_379d57" class="_link_1mzip_17 active"
data-kind="primary" data-kind="primary"
data-status="active" data-status="active"
href="/#emails" href="/#emails"

View File

@@ -1,79 +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.
import { Alert, Button, H3, Text } from "@vector-im/compound-web";
import { useTranslation } from "react-i18next";
import { useMutation } from "urql";
import { graphql } from "../../gql";
import BlockList from "../BlockList";
import LoadingSpinner from "../LoadingSpinner";
const ALLOW_CROSS_SIGING_RESET_MUTATION = graphql(/* GraphQL */ `
mutation AllowCrossSigningReset($userId: ID!) {
allowUserCrossSigningReset(input: { userId: $userId }) {
user {
id
}
}
}
`);
const CrossSigningReset: React.FC<{ userId: string }> = ({ userId }) => {
const { t } = useTranslation();
const [result, allowReset] = useMutation(ALLOW_CROSS_SIGING_RESET_MUTATION);
const onClick = (): void => {
allowReset({ userId });
};
return (
<BlockList>
<H3>{t("frontend.reset_cross_signing.heading")}</H3>
{!result.data && !result.error && (
<>
<Text className="text-justify">
{t("frontend.reset_cross_signing.description")}
</Text>
<Button
kind="destructive"
disabled={result.fetching}
onClick={onClick}
>
{!!result.fetching && <LoadingSpinner inline />}
{t("frontend.reset_cross_signing.button")}
</Button>
</>
)}
{result.data && (
<Alert
type="info"
title={t("frontend.reset_cross_signing.success.title")}
>
{t("frontend.reset_cross_signing.success.description")}
</Alert>
)}
{result.error && (
<Alert
type="critical"
title={t("frontend.reset_cross_signing.failure.title")}
>
{t("frontend.reset_cross_signing.failure.description")}
</Alert>
)}
</BlockList>
);
};
export default CrossSigningReset;

View File

@@ -22,7 +22,7 @@ exports[`BrowserSessionsOverview > renders with sessions 1`] = `
</p> </p>
</div> </div>
<a <a
class="_link_1mzip_17 _linkButton_379d57" class="_link_1mzip_17"
data-kind="primary" data-kind="primary"
href="/sessions/browsers" href="/sessions/browsers"
rel="noreferrer noopener" rel="noreferrer noopener"

View File

@@ -49,8 +49,6 @@ const documents = {
types.UserGreeting_UserFragmentDoc, types.UserGreeting_UserFragmentDoc,
"\n mutation AddEmail($userId: ID!, $email: String!) {\n addEmail(input: { userId: $userId, email: $email }) {\n status\n violations\n email {\n id\n ...UserEmail_email\n }\n }\n }\n": "\n mutation AddEmail($userId: ID!, $email: String!) {\n addEmail(input: { userId: $userId, email: $email }) {\n status\n violations\n email {\n id\n ...UserEmail_email\n }\n }\n }\n":
types.AddEmailDocument, types.AddEmailDocument,
"\n mutation AllowCrossSigningReset($userId: ID!) {\n allowUserCrossSigningReset(input: { userId: $userId }) {\n user {\n id\n }\n }\n }\n":
types.AllowCrossSigningResetDocument,
"\n query UserEmailListQuery(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n\n emails(first: $first, after: $after, last: $last, before: $before) {\n edges {\n cursor\n node {\n id\n ...UserEmail_email\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n": "\n query UserEmailListQuery(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n\n emails(first: $first, after: $after, last: $last, before: $before) {\n edges {\n cursor\n node {\n id\n ...UserEmail_email\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n":
types.UserEmailListQueryDocument, types.UserEmailListQueryDocument,
"\n fragment UserEmailList_user on User {\n id\n primaryEmail {\n id\n }\n }\n": "\n fragment UserEmailList_user on User {\n id\n primaryEmail {\n id\n }\n }\n":
@@ -79,12 +77,14 @@ const documents = {
types.CurrentUserGreetingDocument, types.CurrentUserGreetingDocument,
"\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n": "\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n":
types.OAuth2ClientQueryDocument, types.OAuth2ClientQueryDocument,
"\n query CurrentViewerQuery {\n viewer {\n __typename\n ... on User {\n id\n }\n }\n }\n": "\n query CurrentViewerQuery {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n }\n":
types.CurrentViewerQueryDocument, types.CurrentViewerQueryDocument,
"\n query DeviceRedirectQuery($deviceId: String!, $userId: ID!) {\n session(deviceId: $deviceId, userId: $userId) {\n __typename\n ... on Node {\n id\n }\n }\n }\n": "\n query DeviceRedirectQuery($deviceId: String!, $userId: ID!) {\n session(deviceId: $deviceId, userId: $userId) {\n __typename\n ... on Node {\n id\n }\n }\n }\n":
types.DeviceRedirectQueryDocument, types.DeviceRedirectQueryDocument,
"\n query VerifyEmailQuery($id: ID!) {\n userEmail(id: $id) {\n ...UserEmail_verifyEmail\n }\n }\n": "\n query VerifyEmailQuery($id: ID!) {\n userEmail(id: $id) {\n ...UserEmail_verifyEmail\n }\n }\n":
types.VerifyEmailQueryDocument, types.VerifyEmailQueryDocument,
"\n mutation AllowCrossSigningReset($userId: ID!) {\n allowUserCrossSigningReset(input: { userId: $userId }) {\n user {\n id\n }\n }\n }\n":
types.AllowCrossSigningResetDocument,
"\n query CurrentViewerSessionQuery {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n }\n }\n }\n": "\n query CurrentViewerSessionQuery {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n }\n }\n }\n":
types.CurrentViewerSessionQueryDocument, types.CurrentViewerSessionQueryDocument,
}; };
@@ -211,12 +211,6 @@ export function graphql(
export function graphql( export function graphql(
source: "\n mutation AddEmail($userId: ID!, $email: String!) {\n addEmail(input: { userId: $userId, email: $email }) {\n status\n violations\n email {\n id\n ...UserEmail_email\n }\n }\n }\n", source: "\n mutation AddEmail($userId: ID!, $email: String!) {\n addEmail(input: { userId: $userId, email: $email }) {\n status\n violations\n email {\n id\n ...UserEmail_email\n }\n }\n }\n",
): (typeof documents)["\n mutation AddEmail($userId: ID!, $email: String!) {\n addEmail(input: { userId: $userId, email: $email }) {\n status\n violations\n email {\n id\n ...UserEmail_email\n }\n }\n }\n"]; ): (typeof documents)["\n mutation AddEmail($userId: ID!, $email: String!) {\n addEmail(input: { userId: $userId, email: $email }) {\n status\n violations\n email {\n id\n ...UserEmail_email\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: "\n mutation AllowCrossSigningReset($userId: ID!) {\n allowUserCrossSigningReset(input: { userId: $userId }) {\n user {\n id\n }\n }\n }\n",
): (typeof documents)["\n mutation AllowCrossSigningReset($userId: ID!) {\n allowUserCrossSigningReset(input: { userId: $userId }) {\n user {\n id\n }\n }\n }\n"];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
@@ -305,8 +299,8 @@ export function graphql(
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function graphql( export function graphql(
source: "\n query CurrentViewerQuery {\n viewer {\n __typename\n ... on User {\n id\n }\n }\n }\n", source: "\n query CurrentViewerQuery {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n }\n",
): (typeof documents)["\n query CurrentViewerQuery {\n viewer {\n __typename\n ... on User {\n id\n }\n }\n }\n"]; ): (typeof documents)["\n query CurrentViewerQuery {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n }\n"];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
@@ -319,6 +313,12 @@ export function graphql(
export function graphql( export function graphql(
source: "\n query VerifyEmailQuery($id: ID!) {\n userEmail(id: $id) {\n ...UserEmail_verifyEmail\n }\n }\n", source: "\n query VerifyEmailQuery($id: ID!) {\n userEmail(id: $id) {\n ...UserEmail_verifyEmail\n }\n }\n",
): (typeof documents)["\n query VerifyEmailQuery($id: ID!) {\n userEmail(id: $id) {\n ...UserEmail_verifyEmail\n }\n }\n"]; ): (typeof documents)["\n query VerifyEmailQuery($id: ID!) {\n userEmail(id: $id) {\n ...UserEmail_verifyEmail\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: "\n mutation AllowCrossSigningReset($userId: ID!) {\n allowUserCrossSigningReset(input: { userId: $userId }) {\n user {\n id\n }\n }\n }\n",
): (typeof documents)["\n mutation AllowCrossSigningReset($userId: ID!) {\n allowUserCrossSigningReset(input: { userId: $userId }) {\n user {\n id\n }\n }\n }\n"];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */

View File

@@ -1423,18 +1423,6 @@ export type AddEmailMutation = {
}; };
}; };
export type AllowCrossSigningResetMutationVariables = Exact<{
userId: Scalars["ID"]["input"];
}>;
export type AllowCrossSigningResetMutation = {
__typename?: "Mutation";
allowUserCrossSigningReset: {
__typename?: "AllowUserCrossSigningResetPayload";
user?: { __typename?: "User"; id: string } | null;
};
};
export type UserEmailListQueryQueryVariables = Exact<{ export type UserEmailListQueryQueryVariables = Exact<{
userId: Scalars["ID"]["input"]; userId: Scalars["ID"]["input"];
first?: InputMaybe<Scalars["Int"]["input"]>; first?: InputMaybe<Scalars["Int"]["input"]>;
@@ -1698,7 +1686,9 @@ export type CurrentViewerQueryQueryVariables = Exact<{ [key: string]: never }>;
export type CurrentViewerQueryQuery = { export type CurrentViewerQueryQuery = {
__typename?: "Query"; __typename?: "Query";
viewer: { __typename: "Anonymous" } | { __typename: "User"; id: string }; viewer:
| { __typename: "Anonymous"; id: string }
| { __typename: "User"; id: string };
}; };
export type DeviceRedirectQueryQueryVariables = Exact<{ export type DeviceRedirectQueryQueryVariables = Exact<{
@@ -1729,6 +1719,18 @@ export type VerifyEmailQueryQuery = {
| null; | 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<{ export type CurrentViewerSessionQueryQueryVariables = Exact<{
[key: string]: never; [key: string]: never;
}>; }>;
@@ -3160,75 +3162,6 @@ export const AddEmailDocument = {
}, },
], ],
} as unknown as DocumentNode<AddEmailMutation, AddEmailMutationVariables>; } as unknown as DocumentNode<AddEmailMutation, AddEmailMutationVariables>;
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 = { export const UserEmailListQueryDocument = {
kind: "Document", kind: "Document",
definitions: [ definitions: [
@@ -4576,7 +4509,7 @@ export const CurrentViewerQueryDocument = {
kind: "InlineFragment", kind: "InlineFragment",
typeCondition: { typeCondition: {
kind: "NamedType", kind: "NamedType",
name: { kind: "Name", value: "User" }, name: { kind: "Name", value: "Node" },
}, },
selectionSet: { selectionSet: {
kind: "SelectionSet", kind: "SelectionSet",
@@ -4748,6 +4681,75 @@ export const VerifyEmailQueryDocument = {
VerifyEmailQueryQuery, VerifyEmailQueryQuery,
VerifyEmailQueryQueryVariables 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 = { export const CurrentViewerSessionQueryDocument = {
kind: "Document", kind: "Document",
definitions: [ definitions: [

View File

@@ -11,6 +11,7 @@
// Import Routes // Import Routes
import { Route as rootRoute } from './routes/__root' 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 AccountImport } from './routes/_account'
import { Route as AccountIndexImport } from './routes/_account.index' import { Route as AccountIndexImport } from './routes/_account.index'
import { Route as DevicesIdImport } from './routes/devices.$id' import { Route as DevicesIdImport } from './routes/devices.$id'
@@ -22,6 +23,11 @@ import { Route as AccountSessionsIdImport } from './routes/_account.sessions.$id
// Create/Update Routes // Create/Update Routes
const ResetCrossSigningRoute = ResetCrossSigningImport.update({
path: '/reset-cross-signing',
getParentRoute: () => rootRoute,
} as any)
const AccountRoute = AccountImport.update({ const AccountRoute = AccountImport.update({
id: '/_account', id: '/_account',
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
@@ -70,6 +76,10 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AccountImport preLoaderRoute: typeof AccountImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/reset-cross-signing': {
preLoaderRoute: typeof ResetCrossSigningImport
parentRoute: typeof rootRoute
}
'/clients/$id': { '/clients/$id': {
preLoaderRoute: typeof ClientsIdImport preLoaderRoute: typeof ClientsIdImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
@@ -110,6 +120,7 @@ export const routeTree = rootRoute.addChildren([
AccountSessionsBrowsersRoute, AccountSessionsBrowsersRoute,
AccountSessionsIndexRoute, AccountSessionsIndexRoute,
]), ]),
ResetCrossSigningRoute,
ClientsIdRoute, ClientsIdRoute,
DevicesIdRoute, DevicesIdRoute,
EmailsIdVerifyRoute, EmailsIdVerifyRoute,

View File

@@ -40,6 +40,9 @@ const actionSchema = z
action: z.enum(["session_end", "org.matrix.session_end"]), action: z.enum(["session_end", "org.matrix.session_end"]),
device_id: z.string().optional(), device_id: z.string().optional(),
}), }),
z.object({
action: z.literal("org.matrix.cross_signing_reset"),
}),
z.object({ z.object({
action: z.undefined(), action: z.undefined(),
}), }),
@@ -53,7 +56,7 @@ export const Route = createRootRouteWithContext<{
}>()({ }>()({
validateSearch: (search): Action => actionSchema.parse(search), validateSearch: (search): Action => actionSchema.parse(search),
beforeLoad: ({ search }) => { beforeLoad({ search }) {
switch (search.action) { switch (search.action) {
case "profile": case "profile":
case "org.matrix.profile": case "org.matrix.profile":
@@ -80,6 +83,12 @@ export const Route = createRootRouteWithContext<{
params: { id: search.device_id }, params: { id: search.device_id },
}); });
throw redirect({ to: "/sessions" }); throw redirect({ to: "/sessions" });
case "org.matrix.cross_signing_reset":
throw redirect({
to: "/reset-cross-signing",
search: { deepLink: true },
});
} }
}, },

View File

@@ -13,14 +13,15 @@
// limitations under the License. // limitations under the License.
import { createFileRoute, notFound } from "@tanstack/react-router"; 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 { H3, Separator } from "@vector-im/compound-web";
import { Suspense } from "react"; import { Suspense } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useQuery } from "urql"; import { useQuery } from "urql";
import BlockList from "../components/BlockList/BlockList"; import BlockList from "../components/BlockList/BlockList";
import { ButtonLink } from "../components/ButtonLink";
import LoadingSpinner from "../components/LoadingSpinner"; import LoadingSpinner from "../components/LoadingSpinner";
import CrossSigningReset from "../components/UserProfile/CrossSigningReset";
import UserEmailList from "../components/UserProfile/UserEmailList"; import UserEmailList from "../components/UserProfile/UserEmailList";
import UserName from "../components/UserProfile/UserName"; import UserName from "../components/UserProfile/UserName";
import { graphql } from "../gql"; import { graphql } from "../gql";
@@ -71,7 +72,15 @@ function Index(): React.ReactElement {
</BlockList> </BlockList>
<Separator /> <Separator />
<CrossSigningReset userId={user.id} />
<ButtonLink
to="/reset-cross-signing"
kind="tertiary"
destructive
Icon={IconKey}
>
{t("frontend.reset_cross_signing.heading")}
</ButtonLink>
</BlockList> </BlockList>
</> </>
); );

View File

@@ -23,7 +23,7 @@ const CURRENT_VIEWER_QUERY = graphql(/* GraphQL */ `
query CurrentViewerQuery { query CurrentViewerQuery {
viewer { viewer {
__typename __typename
... on User { ... on Node {
id id
} }
} }

View File

@@ -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<typeof searchSchema>;
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 (
<BlockList>
<PageHeading
Icon={IconKey}
title={t("frontend.reset_cross_signing.heading")}
invalid
/>
{!result.data && !result.error && (
<>
<Text className="text-justify">
{t("frontend.reset_cross_signing.description")}
</Text>
<Button
kind="primary"
destructive
disabled={result.fetching}
onClick={onClick}
>
{!!result.fetching && <LoadingSpinner inline />}
{t("frontend.reset_cross_signing.button")}
</Button>
</>
)}
{result.data && (
<Alert
type="info"
title={t("frontend.reset_cross_signing.success.title")}
>
{t("frontend.reset_cross_signing.success.description")}
</Alert>
)}
{result.error && (
<Alert
type="critical"
title={t("frontend.reset_cross_signing.failure.title")}
>
{t("frontend.reset_cross_signing.failure.description")}
</Alert>
)}
{!deepLink && (
<ButtonLink
to=".."
from={Route.fullPath}
kind="tertiary"
Icon={IconArrowLeft}
>
{t("action.back")}
</ButtonLink>
)}
</BlockList>
);
}