diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 0d17abab2..0300d63ea 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -94,6 +94,19 @@ "pagination_controls": { "total": "Total: {{totalCount}}" }, + "reset_cross_signing": { + "button": "Allow cross-signing reset", + "description": "If you have lost access to all your verified devices and your security key, you can reset your cross-signing keys. Deleting cross-signing keys is permanent, but will allow you to go through the verification process again and set up new keys.", + "failure": { + "description": "This might be a temporary problem, so please try again later. If the problem persists, please contact your server administrator.", + "title": "Failed to allow cross-signing" + }, + "heading": "Cross-signing reset", + "success": { + "description": "A client can now temporarily reset your account cross-signing keys. Follow the instructions in your client to complete the process.", + "title": "Cross-signing reset temporarily allowed" + } + }, "selectable_session": { "label": "Select session" }, diff --git a/frontend/src/components/UserProfile/CrossSigningReset.tsx b/frontend/src/components/UserProfile/CrossSigningReset.tsx new file mode 100644 index 000000000..40ad17e3a --- /dev/null +++ b/frontend/src/components/UserProfile/CrossSigningReset.tsx @@ -0,0 +1,95 @@ +// 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 { atom, useAtom } from "jotai"; +import { atomFamily } from "jotai/utils"; +import { atomWithMutation } from "jotai-urql"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; + +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 allowCrossSigningResetFamily = atomFamily((id: string) => { + const allowCrossSigingReset = atomWithMutation( + ALLOW_CROSS_SIGING_RESET_MUTATION, + ); + + // A proxy atom which pre-sets the id variable in the mutation + const allowCrossSigningResetAtom = atom( + (get) => get(allowCrossSigingReset), + (_get, set) => set(allowCrossSigingReset, { userId: id }), + ); + + return allowCrossSigningResetAtom; +}); + +const CrossSigningReset: React.FC<{ userId: string }> = ({ userId }) => { + const { t } = useTranslation(); + const [result, allowReset] = useAtom(allowCrossSigningResetFamily(userId)); + const [inProgress, setInProgress] = useState(false); + + const onClick = (): void => { + if (inProgress) return; + setInProgress(true); + allowReset().finally(() => setInProgress(false)); + }; + + 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/UserProfile/UserProfile.tsx b/frontend/src/components/UserProfile/UserProfile.tsx index f7683f1ba..0eb30126a 100644 --- a/frontend/src/components/UserProfile/UserProfile.tsx +++ b/frontend/src/components/UserProfile/UserProfile.tsx @@ -12,8 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { Separator } from "@vector-im/compound-web"; + import BlockList from "../BlockList/BlockList"; +import CrossSigningReset from "./CrossSigningReset"; import UserEmailList from "./UserEmailList"; import UserName from "./UserName"; @@ -22,6 +25,8 @@ const UserProfile: React.FC<{ userId: string }> = ({ userId }) => { + + ); }; diff --git a/frontend/src/gql/gql.ts b/frontend/src/gql/gql.ts index cf1d6d2c4..a64f704b6 100644 --- a/frontend/src/gql/gql.ts +++ b/frontend/src/gql/gql.ts @@ -53,6 +53,8 @@ const documents = { types.UserGreetingDocument, "\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 query UserPrimaryEmail($userId: ID!) {\n user(id: $userId) {\n id\n primaryEmail {\n id\n }\n }\n }\n": @@ -213,6 +215,12 @@ 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. */ diff --git a/frontend/src/gql/graphql.ts b/frontend/src/gql/graphql.ts index c061a8525..57a4a8467 100644 --- a/frontend/src/gql/graphql.ts +++ b/frontend/src/gql/graphql.ts @@ -1438,6 +1438,18 @@ 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; @@ -3168,6 +3180,75 @@ 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: [