You've already forked authentication-service
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:
@@ -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(),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
@@ -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/`
|
||||
|
45
frontend/src/components/ButtonLink.tsx
Normal file
45
frontend/src/components/ButtonLink.tsx
Normal 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>;
|
@@ -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<Props> = forwardRef<
|
||||
HTMLAnchorElement,
|
||||
Parameters<typeof useLinkProps>[0]
|
||||
>(({ children, ...props }, ref) => {
|
||||
const { className, ...newProps } = useLinkProps(props);
|
||||
Parameters<typeof useLinkProps>[0] & Props
|
||||
>(({ children, kind, ...props }, ref) => {
|
||||
const linkProps = useLinkProps(props);
|
||||
|
||||
return (
|
||||
<CompoundLink
|
||||
kind="primary"
|
||||
ref={ref}
|
||||
className={cx(className, styles.linkButton)}
|
||||
children={children}
|
||||
{...newProps}
|
||||
/>
|
||||
<CompoundLink kind={kind} ref={ref} {...linkProps}>
|
||||
{children}
|
||||
</CompoundLink>
|
||||
);
|
||||
}) as LinkComponent;
|
||||
}) as LinkComponent<Props>;
|
@@ -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);
|
||||
}
|
@@ -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";
|
@@ -36,7 +36,7 @@ exports[`<UnverifiedEmailAlert /> > renders a warning when there are unverified
|
||||
You have 2 unverified email addresses.
|
||||
|
||||
<a
|
||||
class="_link_1mzip_17 active _linkButton_379d57"
|
||||
class="_link_1mzip_17 active"
|
||||
data-kind="primary"
|
||||
data-status="active"
|
||||
href="/#emails"
|
||||
|
@@ -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;
|
@@ -22,7 +22,7 @@ exports[`BrowserSessionsOverview > renders with sessions 1`] = `
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
class="_link_1mzip_17 _linkButton_379d57"
|
||||
class="_link_1mzip_17"
|
||||
data-kind="primary"
|
||||
href="/sessions/browsers"
|
||||
rel="noreferrer noopener"
|
||||
|
@@ -49,8 +49,6 @@ const documents = {
|
||||
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":
|
||||
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":
|
||||
types.UserEmailListQueryDocument,
|
||||
"\n fragment UserEmailList_user on User {\n id\n primaryEmail {\n id\n }\n }\n":
|
||||
@@ -79,12 +77,14 @@ const documents = {
|
||||
types.CurrentUserGreetingDocument,
|
||||
"\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n":
|
||||
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,
|
||||
"\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,
|
||||
"\n query VerifyEmailQuery($id: ID!) {\n userEmail(id: $id) {\n ...UserEmail_verifyEmail\n }\n }\n":
|
||||
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":
|
||||
types.CurrentViewerSessionQueryDocument,
|
||||
};
|
||||
@@ -211,12 +211,6 @@ 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",
|
||||
): (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.
|
||||
*/
|
||||
@@ -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.
|
||||
*/
|
||||
export function graphql(
|
||||
source: "\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 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 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.
|
||||
*/
|
||||
@@ -319,6 +313,12 @@ export function graphql(
|
||||
export function graphql(
|
||||
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"];
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
@@ -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<{
|
||||
userId: Scalars["ID"]["input"];
|
||||
first?: InputMaybe<Scalars["Int"]["input"]>;
|
||||
@@ -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<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 = {
|
||||
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: [
|
||||
|
@@ -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,
|
||||
|
@@ -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 },
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -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 {
|
||||
</BlockList>
|
||||
|
||||
<Separator />
|
||||
<CrossSigningReset userId={user.id} />
|
||||
|
||||
<ButtonLink
|
||||
to="/reset-cross-signing"
|
||||
kind="tertiary"
|
||||
destructive
|
||||
Icon={IconKey}
|
||||
>
|
||||
{t("frontend.reset_cross_signing.heading")}
|
||||
</ButtonLink>
|
||||
</BlockList>
|
||||
</>
|
||||
);
|
||||
|
@@ -23,7 +23,7 @@ const CURRENT_VIEWER_QUERY = graphql(/* GraphQL */ `
|
||||
query CurrentViewerQuery {
|
||||
viewer {
|
||||
__typename
|
||||
... on User {
|
||||
... on Node {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
139
frontend/src/routes/reset-cross-signing.tsx
Normal file
139
frontend/src/routes/reset-cross-signing.tsx
Normal 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>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user