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
frontend: lazy-load all routes
This commit is contained in:
@@ -53,10 +53,10 @@ const documents = {
|
|||||||
"\n query CurrentViewerQuery {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n }\n": types.CurrentViewerQueryDocument,
|
"\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 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 query VerifyEmailQuery($id: ID!) {\n userEmail(id: $id) {\n ...UserEmail_verifyEmail\n }\n }\n": types.VerifyEmailQueryDocument,
|
||||||
"\n query PasswordChangeQuery {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n }\n": types.PasswordChangeQueryDocument,
|
|
||||||
"\n mutation ChangePassword(\n $userId: ID!\n $oldPassword: String!\n $newPassword: String!\n ) {\n setPassword(\n input: {\n userId: $userId\n currentPassword: $oldPassword\n newPassword: $newPassword\n }\n ) {\n status\n }\n }\n": types.ChangePasswordDocument,
|
"\n mutation ChangePassword(\n $userId: ID!\n $oldPassword: String!\n $newPassword: String!\n ) {\n setPassword(\n input: {\n userId: $userId\n currentPassword: $oldPassword\n newPassword: $newPassword\n }\n ) {\n status\n }\n }\n": types.ChangePasswordDocument,
|
||||||
"\n query PasswordRecoveryQuery {\n siteConfig {\n id\n ...PasswordCreationDoubleInput_siteConfig\n }\n }\n": types.PasswordRecoveryQueryDocument,
|
"\n query PasswordChangeQuery {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n }\n": types.PasswordChangeQueryDocument,
|
||||||
"\n mutation RecoverPassword($ticket: String!, $newPassword: String!) {\n setPasswordByRecovery(\n input: { ticket: $ticket, newPassword: $newPassword }\n ) {\n status\n }\n }\n": types.RecoverPasswordDocument,
|
"\n mutation RecoverPassword($ticket: String!, $newPassword: String!) {\n setPasswordByRecovery(\n input: { ticket: $ticket, newPassword: $newPassword }\n ) {\n status\n }\n }\n": types.RecoverPasswordDocument,
|
||||||
|
"\n query PasswordRecoveryQuery {\n siteConfig {\n id\n ...PasswordCreationDoubleInput_siteConfig\n }\n }\n": types.PasswordRecoveryQueryDocument,
|
||||||
"\n mutation AllowCrossSigningReset($userId: ID!) {\n allowUserCrossSigningReset(input: { userId: $userId }) {\n user {\n id\n }\n }\n }\n": types.AllowCrossSigningResetDocument,
|
"\n mutation AllowCrossSigningReset($userId: ID!) {\n allowUserCrossSigningReset(input: { userId: $userId }) {\n user {\n id\n }\n }\n }\n": types.AllowCrossSigningResetDocument,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -234,10 +234,6 @@ export function graphql(source: "\n query DeviceRedirectQuery($deviceId: String
|
|||||||
* 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(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"];
|
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 query PasswordChangeQuery {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n }\n"): (typeof documents)["\n query PasswordChangeQuery {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n ...PasswordCreationDoubleInput_siteConfig\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.
|
||||||
*/
|
*/
|
||||||
@@ -245,11 +241,15 @@ export function graphql(source: "\n mutation ChangePassword(\n $userId: ID!\
|
|||||||
/**
|
/**
|
||||||
* 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(source: "\n query PasswordRecoveryQuery {\n siteConfig {\n id\n ...PasswordCreationDoubleInput_siteConfig\n }\n }\n"): (typeof documents)["\n query PasswordRecoveryQuery {\n siteConfig {\n id\n ...PasswordCreationDoubleInput_siteConfig\n }\n }\n"];
|
export function graphql(source: "\n query PasswordChangeQuery {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n }\n"): (typeof documents)["\n query PasswordChangeQuery {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n ...PasswordCreationDoubleInput_siteConfig\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.
|
||||||
*/
|
*/
|
||||||
export function graphql(source: "\n mutation RecoverPassword($ticket: String!, $newPassword: String!) {\n setPasswordByRecovery(\n input: { ticket: $ticket, newPassword: $newPassword }\n ) {\n status\n }\n }\n"): (typeof documents)["\n mutation RecoverPassword($ticket: String!, $newPassword: String!) {\n setPasswordByRecovery(\n input: { ticket: $ticket, newPassword: $newPassword }\n ) {\n status\n }\n }\n"];
|
export function graphql(source: "\n mutation RecoverPassword($ticket: String!, $newPassword: String!) {\n setPasswordByRecovery(\n input: { ticket: $ticket, newPassword: $newPassword }\n ) {\n status\n }\n }\n"): (typeof documents)["\n mutation RecoverPassword($ticket: String!, $newPassword: String!) {\n setPasswordByRecovery(\n input: { ticket: $ticket, newPassword: $newPassword }\n ) {\n status\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 query PasswordRecoveryQuery {\n siteConfig {\n id\n ...PasswordCreationDoubleInput_siteConfig\n }\n }\n"): (typeof documents)["\n query PasswordRecoveryQuery {\n siteConfig {\n id\n ...PasswordCreationDoubleInput_siteConfig\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.
|
||||||
*/
|
*/
|
||||||
|
@@ -1697,14 +1697,6 @@ export type VerifyEmailQueryQuery = { __typename?: 'Query', userEmail?: (
|
|||||||
& { ' $fragmentRefs'?: { 'UserEmail_VerifyEmailFragment': UserEmail_VerifyEmailFragment } }
|
& { ' $fragmentRefs'?: { 'UserEmail_VerifyEmailFragment': UserEmail_VerifyEmailFragment } }
|
||||||
) | null };
|
) | null };
|
||||||
|
|
||||||
export type PasswordChangeQueryQueryVariables = Exact<{ [key: string]: never; }>;
|
|
||||||
|
|
||||||
|
|
||||||
export type PasswordChangeQueryQuery = { __typename?: 'Query', viewer: { __typename: 'Anonymous', id: string } | { __typename: 'User', id: string }, siteConfig: (
|
|
||||||
{ __typename?: 'SiteConfig' }
|
|
||||||
& { ' $fragmentRefs'?: { 'PasswordCreationDoubleInput_SiteConfigFragment': PasswordCreationDoubleInput_SiteConfigFragment } }
|
|
||||||
) };
|
|
||||||
|
|
||||||
export type ChangePasswordMutationVariables = Exact<{
|
export type ChangePasswordMutationVariables = Exact<{
|
||||||
userId: Scalars['ID']['input'];
|
userId: Scalars['ID']['input'];
|
||||||
oldPassword: Scalars['String']['input'];
|
oldPassword: Scalars['String']['input'];
|
||||||
@@ -1714,11 +1706,11 @@ export type ChangePasswordMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type ChangePasswordMutation = { __typename?: 'Mutation', setPassword: { __typename?: 'SetPasswordPayload', status: SetPasswordStatus } };
|
export type ChangePasswordMutation = { __typename?: 'Mutation', setPassword: { __typename?: 'SetPasswordPayload', status: SetPasswordStatus } };
|
||||||
|
|
||||||
export type PasswordRecoveryQueryQueryVariables = Exact<{ [key: string]: never; }>;
|
export type PasswordChangeQueryQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
export type PasswordRecoveryQueryQuery = { __typename?: 'Query', siteConfig: (
|
export type PasswordChangeQueryQuery = { __typename?: 'Query', viewer: { __typename: 'Anonymous', id: string } | { __typename: 'User', id: string }, siteConfig: (
|
||||||
{ __typename?: 'SiteConfig', id: string }
|
{ __typename?: 'SiteConfig' }
|
||||||
& { ' $fragmentRefs'?: { 'PasswordCreationDoubleInput_SiteConfigFragment': PasswordCreationDoubleInput_SiteConfigFragment } }
|
& { ' $fragmentRefs'?: { 'PasswordCreationDoubleInput_SiteConfigFragment': PasswordCreationDoubleInput_SiteConfigFragment } }
|
||||||
) };
|
) };
|
||||||
|
|
||||||
@@ -1730,6 +1722,14 @@ export type RecoverPasswordMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type RecoverPasswordMutation = { __typename?: 'Mutation', setPasswordByRecovery: { __typename?: 'SetPasswordPayload', status: SetPasswordStatus } };
|
export type RecoverPasswordMutation = { __typename?: 'Mutation', setPasswordByRecovery: { __typename?: 'SetPasswordPayload', status: SetPasswordStatus } };
|
||||||
|
|
||||||
|
export type PasswordRecoveryQueryQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
|
export type PasswordRecoveryQueryQuery = { __typename?: 'Query', siteConfig: (
|
||||||
|
{ __typename?: 'SiteConfig', id: string }
|
||||||
|
& { ' $fragmentRefs'?: { 'PasswordCreationDoubleInput_SiteConfigFragment': PasswordCreationDoubleInput_SiteConfigFragment } }
|
||||||
|
) };
|
||||||
|
|
||||||
export type AllowCrossSigningResetMutationVariables = Exact<{
|
export type AllowCrossSigningResetMutationVariables = Exact<{
|
||||||
userId: Scalars['ID']['input'];
|
userId: Scalars['ID']['input'];
|
||||||
}>;
|
}>;
|
||||||
@@ -1777,8 +1777,8 @@ export const OAuth2ClientQueryDocument = {"kind":"Document","definitions":[{"kin
|
|||||||
export const CurrentViewerQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CurrentViewerQuery"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"viewer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Node"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode<CurrentViewerQueryQuery, CurrentViewerQueryQueryVariables>;
|
export const CurrentViewerQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CurrentViewerQuery"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"viewer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Node"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode<CurrentViewerQueryQuery, CurrentViewerQueryQueryVariables>;
|
||||||
export const DeviceRedirectQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"DeviceRedirectQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"deviceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"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":"session"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"deviceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"deviceId"}}},{"kind":"Argument","name":{"kind":"Name","value":"userId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Node"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode<DeviceRedirectQueryQuery, DeviceRedirectQueryQueryVariables>;
|
export const DeviceRedirectQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"DeviceRedirectQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"deviceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"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":"session"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"deviceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"deviceId"}}},{"kind":"Argument","name":{"kind":"Name","value":"userId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Node"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode<DeviceRedirectQueryQuery, DeviceRedirectQueryQueryVariables>;
|
||||||
export const VerifyEmailQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"VerifyEmailQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userEmail"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"UserEmail_verifyEmail"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserEmail_verifyEmail"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"UserEmail"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}}]} as unknown as DocumentNode<VerifyEmailQueryQuery, VerifyEmailQueryQueryVariables>;
|
export const VerifyEmailQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"VerifyEmailQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userEmail"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"UserEmail_verifyEmail"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserEmail_verifyEmail"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"UserEmail"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}}]} as unknown as DocumentNode<VerifyEmailQueryQuery, VerifyEmailQueryQueryVariables>;
|
||||||
export const PasswordChangeQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PasswordChangeQuery"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"viewer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Node"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"siteConfig"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PasswordCreationDoubleInput_siteConfig"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PasswordCreationDoubleInput_siteConfig"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SiteConfig"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"minimumPasswordComplexity"}}]}}]} as unknown as DocumentNode<PasswordChangeQueryQuery, PasswordChangeQueryQueryVariables>;
|
|
||||||
export const ChangePasswordDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ChangePassword"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"oldPassword"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"newPassword"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setPassword"},"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"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"currentPassword"},"value":{"kind":"Variable","name":{"kind":"Name","value":"oldPassword"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"newPassword"},"value":{"kind":"Variable","name":{"kind":"Name","value":"newPassword"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]} as unknown as DocumentNode<ChangePasswordMutation, ChangePasswordMutationVariables>;
|
export const ChangePasswordDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ChangePassword"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"oldPassword"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"newPassword"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setPassword"},"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"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"currentPassword"},"value":{"kind":"Variable","name":{"kind":"Name","value":"oldPassword"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"newPassword"},"value":{"kind":"Variable","name":{"kind":"Name","value":"newPassword"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]} as unknown as DocumentNode<ChangePasswordMutation, ChangePasswordMutationVariables>;
|
||||||
export const PasswordRecoveryQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PasswordRecoveryQuery"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"siteConfig"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"PasswordCreationDoubleInput_siteConfig"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PasswordCreationDoubleInput_siteConfig"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SiteConfig"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"minimumPasswordComplexity"}}]}}]} as unknown as DocumentNode<PasswordRecoveryQueryQuery, PasswordRecoveryQueryQueryVariables>;
|
export const PasswordChangeQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PasswordChangeQuery"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"viewer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Node"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"siteConfig"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PasswordCreationDoubleInput_siteConfig"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PasswordCreationDoubleInput_siteConfig"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SiteConfig"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"minimumPasswordComplexity"}}]}}]} as unknown as DocumentNode<PasswordChangeQueryQuery, PasswordChangeQueryQueryVariables>;
|
||||||
export const RecoverPasswordDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RecoverPassword"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"ticket"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"newPassword"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setPasswordByRecovery"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"ticket"},"value":{"kind":"Variable","name":{"kind":"Name","value":"ticket"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"newPassword"},"value":{"kind":"Variable","name":{"kind":"Name","value":"newPassword"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]} as unknown as DocumentNode<RecoverPasswordMutation, RecoverPasswordMutationVariables>;
|
export const RecoverPasswordDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RecoverPassword"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"ticket"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"newPassword"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setPasswordByRecovery"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"ticket"},"value":{"kind":"Variable","name":{"kind":"Name","value":"ticket"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"newPassword"},"value":{"kind":"Variable","name":{"kind":"Name","value":"newPassword"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]} as unknown as DocumentNode<RecoverPasswordMutation, RecoverPasswordMutationVariables>;
|
||||||
|
export const PasswordRecoveryQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PasswordRecoveryQuery"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"siteConfig"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"PasswordCreationDoubleInput_siteConfig"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PasswordCreationDoubleInput_siteConfig"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SiteConfig"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"minimumPasswordComplexity"}}]}}]} as unknown as DocumentNode<PasswordRecoveryQueryQuery, PasswordRecoveryQueryQueryVariables>;
|
||||||
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 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>;
|
@@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
// This file is auto-generated by TanStack Router
|
// This file is auto-generated by TanStack Router
|
||||||
|
|
||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
// Import Routes
|
// Import Routes
|
||||||
|
|
||||||
import { Route as rootRoute } from './routes/__root'
|
import { Route as rootRoute } from './routes/__root'
|
||||||
@@ -19,27 +21,36 @@ import { Route as ClientsIdImport } from './routes/clients.$id'
|
|||||||
import { Route as PasswordRecoveryIndexImport } from './routes/password.recovery.index'
|
import { Route as PasswordRecoveryIndexImport } from './routes/password.recovery.index'
|
||||||
import { Route as PasswordChangeIndexImport } from './routes/password.change.index'
|
import { Route as PasswordChangeIndexImport } from './routes/password.change.index'
|
||||||
import { Route as AccountSessionsIndexImport } from './routes/_account.sessions.index'
|
import { Route as AccountSessionsIndexImport } from './routes/_account.sessions.index'
|
||||||
import { Route as PasswordChangeSuccessImport } from './routes/password.change.success'
|
|
||||||
import { Route as EmailsIdVerifyImport } from './routes/emails.$id.verify'
|
import { Route as EmailsIdVerifyImport } from './routes/emails.$id.verify'
|
||||||
import { Route as AccountSessionsBrowsersImport } from './routes/_account.sessions.browsers'
|
import { Route as AccountSessionsBrowsersImport } from './routes/_account.sessions.browsers'
|
||||||
import { Route as AccountSessionsIdImport } from './routes/_account.sessions.$id'
|
import { Route as AccountSessionsIdImport } from './routes/_account.sessions.$id'
|
||||||
|
|
||||||
|
// Create Virtual Routes
|
||||||
|
|
||||||
|
const PasswordChangeSuccessLazyImport = createFileRoute(
|
||||||
|
'/password/change/success',
|
||||||
|
)()
|
||||||
|
|
||||||
// Create/Update Routes
|
// Create/Update Routes
|
||||||
|
|
||||||
const ResetCrossSigningRoute = ResetCrossSigningImport.update({
|
const ResetCrossSigningRoute = ResetCrossSigningImport.update({
|
||||||
path: '/reset-cross-signing',
|
path: '/reset-cross-signing',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any).lazy(() =>
|
||||||
|
import('./routes/reset-cross-signing.lazy').then((d) => d.Route),
|
||||||
|
)
|
||||||
|
|
||||||
const AccountRoute = AccountImport.update({
|
const AccountRoute = AccountImport.update({
|
||||||
id: '/_account',
|
id: '/_account',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any).lazy(() => import('./routes/_account.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const AccountIndexRoute = AccountIndexImport.update({
|
const AccountIndexRoute = AccountIndexImport.update({
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => AccountRoute,
|
getParentRoute: () => AccountRoute,
|
||||||
} as any)
|
} as any).lazy(() =>
|
||||||
|
import('./routes/_account.index.lazy').then((d) => d.Route),
|
||||||
|
)
|
||||||
|
|
||||||
const DevicesSplatRoute = DevicesSplatImport.update({
|
const DevicesSplatRoute = DevicesSplatImport.update({
|
||||||
path: '/devices/$',
|
path: '/devices/$',
|
||||||
@@ -49,7 +60,7 @@ const DevicesSplatRoute = DevicesSplatImport.update({
|
|||||||
const ClientsIdRoute = ClientsIdImport.update({
|
const ClientsIdRoute = ClientsIdImport.update({
|
||||||
path: '/clients/$id',
|
path: '/clients/$id',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any).lazy(() => import('./routes/clients.$id.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const PasswordRecoveryIndexRoute = PasswordRecoveryIndexImport.update({
|
const PasswordRecoveryIndexRoute = PasswordRecoveryIndexImport.update({
|
||||||
path: '/password/recovery/',
|
path: '/password/recovery/',
|
||||||
@@ -68,27 +79,37 @@ const PasswordChangeIndexRoute = PasswordChangeIndexImport.update({
|
|||||||
const AccountSessionsIndexRoute = AccountSessionsIndexImport.update({
|
const AccountSessionsIndexRoute = AccountSessionsIndexImport.update({
|
||||||
path: '/sessions/',
|
path: '/sessions/',
|
||||||
getParentRoute: () => AccountRoute,
|
getParentRoute: () => AccountRoute,
|
||||||
} as any)
|
} as any).lazy(() =>
|
||||||
|
import('./routes/_account.sessions.index.lazy').then((d) => d.Route),
|
||||||
|
)
|
||||||
|
|
||||||
const PasswordChangeSuccessRoute = PasswordChangeSuccessImport.update({
|
const PasswordChangeSuccessLazyRoute = PasswordChangeSuccessLazyImport.update({
|
||||||
path: '/password/change/success',
|
path: '/password/change/success',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any).lazy(() =>
|
||||||
|
import('./routes/password.change.success.lazy').then((d) => d.Route),
|
||||||
|
)
|
||||||
|
|
||||||
const EmailsIdVerifyRoute = EmailsIdVerifyImport.update({
|
const EmailsIdVerifyRoute = EmailsIdVerifyImport.update({
|
||||||
path: '/emails/$id/verify',
|
path: '/emails/$id/verify',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any).lazy(() =>
|
||||||
|
import('./routes/emails.$id.verify.lazy').then((d) => d.Route),
|
||||||
|
)
|
||||||
|
|
||||||
const AccountSessionsBrowsersRoute = AccountSessionsBrowsersImport.update({
|
const AccountSessionsBrowsersRoute = AccountSessionsBrowsersImport.update({
|
||||||
path: '/sessions/browsers',
|
path: '/sessions/browsers',
|
||||||
getParentRoute: () => AccountRoute,
|
getParentRoute: () => AccountRoute,
|
||||||
} as any)
|
} as any).lazy(() =>
|
||||||
|
import('./routes/_account.sessions.browsers.lazy').then((d) => d.Route),
|
||||||
|
)
|
||||||
|
|
||||||
const AccountSessionsIdRoute = AccountSessionsIdImport.update({
|
const AccountSessionsIdRoute = AccountSessionsIdImport.update({
|
||||||
path: '/sessions/$id',
|
path: '/sessions/$id',
|
||||||
getParentRoute: () => AccountRoute,
|
getParentRoute: () => AccountRoute,
|
||||||
} as any)
|
} as any).lazy(() =>
|
||||||
|
import('./routes/_account.sessions.$id.lazy').then((d) => d.Route),
|
||||||
|
)
|
||||||
|
|
||||||
// Populate the FileRoutesByPath interface
|
// Populate the FileRoutesByPath interface
|
||||||
|
|
||||||
@@ -154,7 +175,7 @@ declare module '@tanstack/react-router' {
|
|||||||
id: '/password/change/success'
|
id: '/password/change/success'
|
||||||
path: '/password/change/success'
|
path: '/password/change/success'
|
||||||
fullPath: '/password/change/success'
|
fullPath: '/password/change/success'
|
||||||
preLoaderRoute: typeof PasswordChangeSuccessImport
|
preLoaderRoute: typeof PasswordChangeSuccessLazyImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
'/_account/sessions/': {
|
'/_account/sessions/': {
|
||||||
@@ -194,7 +215,7 @@ export const routeTree = rootRoute.addChildren({
|
|||||||
ClientsIdRoute,
|
ClientsIdRoute,
|
||||||
DevicesSplatRoute,
|
DevicesSplatRoute,
|
||||||
EmailsIdVerifyRoute,
|
EmailsIdVerifyRoute,
|
||||||
PasswordChangeSuccessRoute,
|
PasswordChangeSuccessLazyRoute,
|
||||||
PasswordChangeIndexRoute,
|
PasswordChangeIndexRoute,
|
||||||
PasswordRecoveryIndexRoute,
|
PasswordRecoveryIndexRoute,
|
||||||
})
|
})
|
||||||
@@ -251,7 +272,7 @@ export const routeTree = rootRoute.addChildren({
|
|||||||
"filePath": "emails.$id.verify.tsx"
|
"filePath": "emails.$id.verify.tsx"
|
||||||
},
|
},
|
||||||
"/password/change/success": {
|
"/password/change/success": {
|
||||||
"filePath": "password.change.success.tsx"
|
"filePath": "password.change.success.lazy.tsx"
|
||||||
},
|
},
|
||||||
"/_account/sessions/": {
|
"/_account/sessions/": {
|
||||||
"filePath": "_account.sessions.index.tsx",
|
"filePath": "_account.sessions.index.tsx",
|
||||||
|
101
frontend/src/routes/_account.index.lazy.tsx
Normal file
101
frontend/src/routes/_account.index.lazy.tsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
// 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 {
|
||||||
|
createLazyFileRoute,
|
||||||
|
notFound,
|
||||||
|
useNavigate,
|
||||||
|
} from "@tanstack/react-router";
|
||||||
|
import IconKey from "@vector-im/compound-design-tokens/assets/web/icons/key";
|
||||||
|
import { Alert, Separator } from "@vector-im/compound-web";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useQuery } from "urql";
|
||||||
|
|
||||||
|
import AccountManagementPasswordPreview from "../components/AccountManagementPasswordPreview";
|
||||||
|
import BlockList from "../components/BlockList/BlockList";
|
||||||
|
import { ButtonLink } from "../components/ButtonLink";
|
||||||
|
import LoadingSpinner from "../components/LoadingSpinner";
|
||||||
|
import UserEmail from "../components/UserEmail";
|
||||||
|
import AddEmailForm from "../components/UserProfile/AddEmailForm";
|
||||||
|
import UserEmailList from "../components/UserProfile/UserEmailList";
|
||||||
|
|
||||||
|
import { QUERY } from "./_account.index";
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/_account/")({
|
||||||
|
component: Index,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Index(): React.ReactElement {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [result] = useQuery({ query: QUERY });
|
||||||
|
if (result.error) throw result.error;
|
||||||
|
const user = result.data?.viewer;
|
||||||
|
if (user?.__typename !== "User") throw notFound();
|
||||||
|
const siteConfig = result.data?.siteConfig;
|
||||||
|
if (!siteConfig) throw Error(); // This should never happen
|
||||||
|
|
||||||
|
// When adding an email, we want to go to the email verification form
|
||||||
|
const onAdd = async (id: string): Promise<void> => {
|
||||||
|
await navigate({ to: "/emails/$id/verify", params: { id } });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BlockList>
|
||||||
|
{/* This wrapper is only needed for the anchor link */}
|
||||||
|
<div className="flex flex-col gap-4" id="emails">
|
||||||
|
{user.primaryEmail ? (
|
||||||
|
<UserEmail
|
||||||
|
email={user.primaryEmail}
|
||||||
|
isPrimary
|
||||||
|
siteConfig={siteConfig}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Alert
|
||||||
|
type="critical"
|
||||||
|
title={t("frontend.user_email_list.no_primary_email_alert")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Suspense fallback={<LoadingSpinner mini className="self-center" />}>
|
||||||
|
<UserEmailList siteConfig={siteConfig} user={user} />
|
||||||
|
</Suspense>
|
||||||
|
|
||||||
|
{siteConfig.emailChangeAllowed && (
|
||||||
|
<AddEmailForm userId={user.id} onAdd={onAdd} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{siteConfig.passwordLoginEnabled && (
|
||||||
|
<AccountManagementPasswordPreview siteConfig={siteConfig} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<ButtonLink
|
||||||
|
to="/reset-cross-signing"
|
||||||
|
kind="tertiary"
|
||||||
|
destructive
|
||||||
|
Icon={IconKey}
|
||||||
|
>
|
||||||
|
{t("frontend.reset_cross_signing.heading")}
|
||||||
|
</ButtonLink>
|
||||||
|
</BlockList>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -12,29 +12,12 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import {
|
import { createFileRoute, notFound, redirect } from "@tanstack/react-router";
|
||||||
createFileRoute,
|
|
||||||
notFound,
|
|
||||||
redirect,
|
|
||||||
useNavigate,
|
|
||||||
} from "@tanstack/react-router";
|
|
||||||
import IconKey from "@vector-im/compound-design-tokens/assets/web/icons/key";
|
|
||||||
import { Alert, Separator } from "@vector-im/compound-web";
|
|
||||||
import { Suspense } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useQuery } from "urql";
|
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
|
|
||||||
import AccountManagementPasswordPreview from "../components/AccountManagementPasswordPreview";
|
|
||||||
import BlockList from "../components/BlockList/BlockList";
|
|
||||||
import { ButtonLink } from "../components/ButtonLink";
|
|
||||||
import LoadingSpinner from "../components/LoadingSpinner";
|
|
||||||
import UserEmail from "../components/UserEmail";
|
|
||||||
import AddEmailForm from "../components/UserProfile/AddEmailForm";
|
|
||||||
import UserEmailList from "../components/UserProfile/UserEmailList";
|
|
||||||
import { graphql } from "../gql";
|
import { graphql } from "../gql";
|
||||||
|
|
||||||
const QUERY = graphql(/* GraphQL */ `
|
export const QUERY = graphql(/* GraphQL */ `
|
||||||
query UserProfileQuery {
|
query UserProfileQuery {
|
||||||
viewer {
|
viewer {
|
||||||
__typename
|
__typename
|
||||||
@@ -137,68 +120,4 @@ export const Route = createFileRoute("/_account/")({
|
|||||||
if (result.error) throw result.error;
|
if (result.error) throw result.error;
|
||||||
if (result.data?.viewer.__typename !== "User") throw notFound();
|
if (result.data?.viewer.__typename !== "User") throw notFound();
|
||||||
},
|
},
|
||||||
component: Index,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function Index(): React.ReactElement {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [result] = useQuery({ query: QUERY });
|
|
||||||
if (result.error) throw result.error;
|
|
||||||
const user = result.data?.viewer;
|
|
||||||
if (user?.__typename !== "User") throw notFound();
|
|
||||||
const siteConfig = result.data?.siteConfig;
|
|
||||||
if (!siteConfig) throw Error(); // This should never happen
|
|
||||||
|
|
||||||
// When adding an email, we want to go to the email verification form
|
|
||||||
const onAdd = async (id: string): Promise<void> => {
|
|
||||||
await navigate({ to: "/emails/$id/verify", params: { id } });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<BlockList>
|
|
||||||
{/* This wrapper is only needed for the anchor link */}
|
|
||||||
<div className="flex flex-col gap-4" id="emails">
|
|
||||||
{user.primaryEmail ? (
|
|
||||||
<UserEmail
|
|
||||||
email={user.primaryEmail}
|
|
||||||
isPrimary
|
|
||||||
siteConfig={siteConfig}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Alert
|
|
||||||
type="critical"
|
|
||||||
title={t("frontend.user_email_list.no_primary_email_alert")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Suspense fallback={<LoadingSpinner mini className="self-center" />}>
|
|
||||||
<UserEmailList siteConfig={siteConfig} user={user} />
|
|
||||||
</Suspense>
|
|
||||||
|
|
||||||
{siteConfig.emailChangeAllowed && (
|
|
||||||
<AddEmailForm userId={user.id} onAdd={onAdd} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
{siteConfig.passwordLoginEnabled && (
|
|
||||||
<AccountManagementPasswordPreview siteConfig={siteConfig} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<ButtonLink
|
|
||||||
to="/reset-cross-signing"
|
|
||||||
kind="tertiary"
|
|
||||||
destructive
|
|
||||||
Icon={IconKey}
|
|
||||||
>
|
|
||||||
{t("frontend.reset_cross_signing.heading")}
|
|
||||||
</ButtonLink>
|
|
||||||
</BlockList>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
70
frontend/src/routes/_account.lazy.tsx
Normal file
70
frontend/src/routes/_account.lazy.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// 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 { Outlet, createLazyFileRoute, notFound } from "@tanstack/react-router";
|
||||||
|
import { Heading } from "@vector-im/compound-web";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useQuery } from "urql";
|
||||||
|
|
||||||
|
import { useEndBrowserSession } from "../components/BrowserSession";
|
||||||
|
import Layout from "../components/Layout";
|
||||||
|
import NavBar from "../components/NavBar";
|
||||||
|
import NavItem from "../components/NavItem";
|
||||||
|
import EndSessionButton from "../components/Session/EndSessionButton";
|
||||||
|
import UnverifiedEmailAlert from "../components/UnverifiedEmailAlert";
|
||||||
|
import UserGreeting from "../components/UserGreeting";
|
||||||
|
|
||||||
|
import { QUERY } from "./_account";
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/_account")({
|
||||||
|
component: Account,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Account(): React.ReactElement {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [result] = useQuery({
|
||||||
|
query: QUERY,
|
||||||
|
});
|
||||||
|
if (result.error) throw result.error;
|
||||||
|
const session = result.data?.viewerSession;
|
||||||
|
if (session?.__typename !== "BrowserSession") throw notFound();
|
||||||
|
const siteConfig = result.data?.siteConfig;
|
||||||
|
if (!siteConfig) throw Error(); // This should never happen
|
||||||
|
const onSessionEnd = useEndBrowserSession(session.id, true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout wide>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<header className="flex justify-between mb-4">
|
||||||
|
<Heading size="lg" weight="semibold">
|
||||||
|
{t("frontend.account.title")}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<EndSessionButton endSession={onSessionEnd} />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<UserGreeting user={session.user} siteConfig={siteConfig} />
|
||||||
|
|
||||||
|
<UnverifiedEmailAlert user={session.user} />
|
||||||
|
|
||||||
|
<NavBar>
|
||||||
|
<NavItem to="/">{t("frontend.nav.settings")}</NavItem>
|
||||||
|
<NavItem to="/sessions">{t("frontend.nav.devices")}</NavItem>
|
||||||
|
</NavBar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Outlet />
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
72
frontend/src/routes/_account.sessions.$id.lazy.tsx
Normal file
72
frontend/src/routes/_account.sessions.$id.lazy.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// 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 { createLazyFileRoute, notFound } from "@tanstack/react-router";
|
||||||
|
import { Alert } from "@vector-im/compound-web";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useQuery } from "urql";
|
||||||
|
|
||||||
|
import { Link } from "../components/Link";
|
||||||
|
import BrowserSessionDetail from "../components/SessionDetail/BrowserSessionDetail";
|
||||||
|
import CompatSessionDetail from "../components/SessionDetail/CompatSessionDetail";
|
||||||
|
import OAuth2SessionDetail from "../components/SessionDetail/OAuth2SessionDetail";
|
||||||
|
|
||||||
|
import { QUERY } from "./_account.sessions.$id";
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/_account/sessions/$id")({
|
||||||
|
notFoundComponent: NotFound,
|
||||||
|
component: SessionDetail,
|
||||||
|
});
|
||||||
|
|
||||||
|
function NotFound(): React.ReactElement {
|
||||||
|
const { id } = Route.useParams();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
type="critical"
|
||||||
|
title={t("frontend.session_detail.alert.title", { deviceId: id })}
|
||||||
|
>
|
||||||
|
{t("frontend.session_detail.alert.text")}
|
||||||
|
<Link to="/sessions" search={{ first: 6 }}>
|
||||||
|
{t("frontend.session_detail.alert.button")}
|
||||||
|
</Link>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SessionDetail(): React.ReactElement {
|
||||||
|
const { id } = Route.useParams();
|
||||||
|
const [result] = useQuery({ query: QUERY, variables: { id } });
|
||||||
|
if (result.error) throw result.error;
|
||||||
|
const node = result.data?.node;
|
||||||
|
if (!node) throw notFound();
|
||||||
|
const currentSessionId = result.data?.viewerSession?.id;
|
||||||
|
|
||||||
|
switch (node.__typename) {
|
||||||
|
case "CompatSession":
|
||||||
|
return <CompatSessionDetail session={node} />;
|
||||||
|
case "Oauth2Session":
|
||||||
|
return <OAuth2SessionDetail session={node} />;
|
||||||
|
case "BrowserSession":
|
||||||
|
return (
|
||||||
|
<BrowserSessionDetail
|
||||||
|
session={node}
|
||||||
|
isCurrent={node.id === currentSessionId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
throw new Error("Unknown session type");
|
||||||
|
}
|
||||||
|
}
|
@@ -13,32 +13,10 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { createFileRoute, notFound } from "@tanstack/react-router";
|
import { createFileRoute, notFound } from "@tanstack/react-router";
|
||||||
import { Alert } from "@vector-im/compound-web";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useQuery } from "urql";
|
|
||||||
|
|
||||||
import { Link } from "../components/Link";
|
|
||||||
import BrowserSessionDetail from "../components/SessionDetail/BrowserSessionDetail";
|
|
||||||
import CompatSessionDetail from "../components/SessionDetail/CompatSessionDetail";
|
|
||||||
import OAuth2SessionDetail from "../components/SessionDetail/OAuth2SessionDetail";
|
|
||||||
import { graphql } from "../gql";
|
import { graphql } from "../gql";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_account/sessions/$id")({
|
export const QUERY = graphql(/* GraphQL */ `
|
||||||
async loader({ context, params, abortController: { signal } }) {
|
|
||||||
const result = await context.client.query(
|
|
||||||
QUERY,
|
|
||||||
{ id: params.id },
|
|
||||||
{ fetchOptions: { signal } },
|
|
||||||
);
|
|
||||||
if (result.error) throw result.error;
|
|
||||||
if (!result.data?.node) throw notFound();
|
|
||||||
},
|
|
||||||
|
|
||||||
notFoundComponent: NotFound,
|
|
||||||
component: SessionDetail,
|
|
||||||
});
|
|
||||||
|
|
||||||
const QUERY = graphql(/* GraphQL */ `
|
|
||||||
query SessionDetailQuery($id: ID!) {
|
query SessionDetailQuery($id: ID!) {
|
||||||
viewerSession {
|
viewerSession {
|
||||||
... on Node {
|
... on Node {
|
||||||
@@ -56,44 +34,14 @@ const QUERY = graphql(/* GraphQL */ `
|
|||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
function NotFound(): React.ReactElement {
|
export const Route = createFileRoute("/_account/sessions/$id")({
|
||||||
const { id } = Route.useParams();
|
async loader({ context, params, abortController: { signal } }) {
|
||||||
const { t } = useTranslation();
|
const result = await context.client.query(
|
||||||
|
QUERY,
|
||||||
return (
|
{ id: params.id },
|
||||||
<Alert
|
{ fetchOptions: { signal } },
|
||||||
type="critical"
|
|
||||||
title={t("frontend.session_detail.alert.title", { deviceId: id })}
|
|
||||||
>
|
|
||||||
{t("frontend.session_detail.alert.text")}
|
|
||||||
<Link to="/sessions" search={{ first: 6 }}>
|
|
||||||
{t("frontend.session_detail.alert.button")}
|
|
||||||
</Link>
|
|
||||||
</Alert>
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
function SessionDetail(): React.ReactElement {
|
|
||||||
const { id } = Route.useParams();
|
|
||||||
const [result] = useQuery({ query: QUERY, variables: { id } });
|
|
||||||
if (result.error) throw result.error;
|
if (result.error) throw result.error;
|
||||||
const node = result.data?.node;
|
if (!result.data?.node) throw notFound();
|
||||||
if (!node) throw notFound();
|
},
|
||||||
const currentSessionId = result.data?.viewerSession?.id;
|
});
|
||||||
|
|
||||||
switch (node.__typename) {
|
|
||||||
case "CompatSession":
|
|
||||||
return <CompatSessionDetail session={node} />;
|
|
||||||
case "Oauth2Session":
|
|
||||||
return <OAuth2SessionDetail session={node} />;
|
|
||||||
case "BrowserSession":
|
|
||||||
return (
|
|
||||||
<BrowserSessionDetail
|
|
||||||
session={node}
|
|
||||||
isCurrent={node.id === currentSessionId}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
throw new Error("Unknown session type");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
131
frontend/src/routes/_account.sessions.browsers.lazy.tsx
Normal file
131
frontend/src/routes/_account.sessions.browsers.lazy.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
// 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 { createLazyFileRoute, notFound } from "@tanstack/react-router";
|
||||||
|
import { H5 } from "@vector-im/compound-web";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useQuery } from "urql";
|
||||||
|
|
||||||
|
import BlockList from "../components/BlockList";
|
||||||
|
import BrowserSession from "../components/BrowserSession";
|
||||||
|
import { ButtonLink } from "../components/ButtonLink";
|
||||||
|
import EmptyState from "../components/EmptyState";
|
||||||
|
import Filter from "../components/Filter";
|
||||||
|
import { type BackwardPagination, usePages } from "../pagination";
|
||||||
|
|
||||||
|
import { QUERY } from "./_account.sessions.browsers";
|
||||||
|
|
||||||
|
const PAGE_SIZE = 6;
|
||||||
|
const DEFAULT_PAGE: BackwardPagination = { last: PAGE_SIZE };
|
||||||
|
|
||||||
|
const getNintyDaysAgo = (): string => {
|
||||||
|
const date = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
|
||||||
|
// Round down to the start of the day to avoid rerendering/requerying
|
||||||
|
date.setHours(0, 0, 0, 0);
|
||||||
|
return date.toISOString();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/_account/sessions/browsers")({
|
||||||
|
component: BrowserSessions,
|
||||||
|
});
|
||||||
|
|
||||||
|
function BrowserSessions(): React.ReactElement {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { inactive, ...pagination } = Route.useLoaderDeps();
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
lastActive: inactive ? { before: getNintyDaysAgo() } : undefined,
|
||||||
|
...pagination,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [list] = useQuery({ query: QUERY, variables });
|
||||||
|
if (list.error) throw list.error;
|
||||||
|
const currentSession =
|
||||||
|
list.data?.viewerSession.__typename === "BrowserSession"
|
||||||
|
? list.data.viewerSession
|
||||||
|
: null;
|
||||||
|
if (currentSession === null) throw notFound();
|
||||||
|
|
||||||
|
const [backwardPage, forwardPage] = usePages(
|
||||||
|
pagination,
|
||||||
|
currentSession.user.browserSessions.pageInfo,
|
||||||
|
PAGE_SIZE,
|
||||||
|
);
|
||||||
|
|
||||||
|
// We reverse the list as we are paginating backwards
|
||||||
|
const edges = [...currentSession.user.browserSessions.edges].reverse();
|
||||||
|
return (
|
||||||
|
<BlockList>
|
||||||
|
<H5>{t("frontend.browser_sessions_overview.heading")}</H5>
|
||||||
|
|
||||||
|
<div className="flex gap-2 items-start">
|
||||||
|
<Filter
|
||||||
|
to="/sessions/browsers"
|
||||||
|
enabled={inactive}
|
||||||
|
search={{ ...DEFAULT_PAGE, inactive: inactive ? undefined : true }}
|
||||||
|
>
|
||||||
|
{t("frontend.last_active.inactive_90_days")}
|
||||||
|
</Filter>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{edges.map((n) => (
|
||||||
|
<BrowserSession
|
||||||
|
key={n.cursor}
|
||||||
|
session={n.node}
|
||||||
|
isCurrent={currentSession.id === n.node.id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{currentSession.user.browserSessions.totalCount === 0 && (
|
||||||
|
<EmptyState>
|
||||||
|
{inactive
|
||||||
|
? t(
|
||||||
|
"frontend.browser_sessions_overview.no_active_sessions.inactive_90_days",
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
"frontend.browser_sessions_overview.no_active_sessions.default",
|
||||||
|
)}
|
||||||
|
</EmptyState>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Only show the pagination buttons if there are pages to go to */}
|
||||||
|
{(forwardPage || backwardPage) && (
|
||||||
|
<div className="flex *:flex-1">
|
||||||
|
<ButtonLink
|
||||||
|
kind="secondary"
|
||||||
|
size="sm"
|
||||||
|
disabled={!forwardPage}
|
||||||
|
to="/sessions/browsers"
|
||||||
|
search={forwardPage || pagination}
|
||||||
|
>
|
||||||
|
{t("common.previous")}
|
||||||
|
</ButtonLink>
|
||||||
|
|
||||||
|
{/* Spacer */}
|
||||||
|
<div />
|
||||||
|
|
||||||
|
<ButtonLink
|
||||||
|
kind="secondary"
|
||||||
|
size="sm"
|
||||||
|
disabled={!backwardPage}
|
||||||
|
to="/sessions/browsers"
|
||||||
|
search={backwardPage || pagination}
|
||||||
|
>
|
||||||
|
{t("common.next")}
|
||||||
|
</ButtonLink>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</BlockList>
|
||||||
|
);
|
||||||
|
}
|
@@ -13,28 +13,19 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { createFileRoute, notFound } from "@tanstack/react-router";
|
import { createFileRoute, notFound } from "@tanstack/react-router";
|
||||||
import { H5 } from "@vector-im/compound-web";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useQuery } from "urql";
|
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
|
|
||||||
import BlockList from "../components/BlockList";
|
|
||||||
import BrowserSession from "../components/BrowserSession";
|
|
||||||
import { ButtonLink } from "../components/ButtonLink";
|
|
||||||
import EmptyState from "../components/EmptyState";
|
|
||||||
import Filter from "../components/Filter";
|
|
||||||
import { graphql } from "../gql";
|
import { graphql } from "../gql";
|
||||||
import {
|
import {
|
||||||
BackwardPagination,
|
type Pagination,
|
||||||
Pagination,
|
type BackwardPagination,
|
||||||
paginationSchema,
|
paginationSchema,
|
||||||
usePages,
|
|
||||||
} from "../pagination";
|
} from "../pagination";
|
||||||
|
|
||||||
const PAGE_SIZE = 6;
|
const PAGE_SIZE = 6;
|
||||||
const DEFAULT_PAGE: BackwardPagination = { last: PAGE_SIZE };
|
const DEFAULT_PAGE: BackwardPagination = { last: PAGE_SIZE };
|
||||||
|
|
||||||
const QUERY = graphql(/* GraphQL */ `
|
export const QUERY = graphql(/* GraphQL */ `
|
||||||
query BrowserSessionList(
|
query BrowserSessionList(
|
||||||
$first: Int
|
$first: Int
|
||||||
$after: String
|
$after: String
|
||||||
@@ -119,95 +110,5 @@ export const Route = createFileRoute("/_account/sessions/browsers")({
|
|||||||
throw notFound();
|
throw notFound();
|
||||||
},
|
},
|
||||||
|
|
||||||
component: BrowserSessions,
|
component: () => <div>Hello /_account/sessions/browsers!</div>,
|
||||||
});
|
});
|
||||||
|
|
||||||
function BrowserSessions(): React.ReactElement {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { inactive, ...pagination } = Route.useLoaderDeps();
|
|
||||||
|
|
||||||
const variables = {
|
|
||||||
lastActive: inactive ? { before: getNintyDaysAgo() } : undefined,
|
|
||||||
...pagination,
|
|
||||||
};
|
|
||||||
|
|
||||||
const [list] = useQuery({ query: QUERY, variables });
|
|
||||||
if (list.error) throw list.error;
|
|
||||||
const currentSession =
|
|
||||||
list.data?.viewerSession.__typename === "BrowserSession"
|
|
||||||
? list.data.viewerSession
|
|
||||||
: null;
|
|
||||||
if (currentSession === null) throw notFound();
|
|
||||||
|
|
||||||
const [backwardPage, forwardPage] = usePages(
|
|
||||||
pagination,
|
|
||||||
currentSession.user.browserSessions.pageInfo,
|
|
||||||
PAGE_SIZE,
|
|
||||||
);
|
|
||||||
|
|
||||||
// We reverse the list as we are paginating backwards
|
|
||||||
const edges = [...currentSession.user.browserSessions.edges].reverse();
|
|
||||||
return (
|
|
||||||
<BlockList>
|
|
||||||
<H5>{t("frontend.browser_sessions_overview.heading")}</H5>
|
|
||||||
|
|
||||||
<div className="flex gap-2 items-start">
|
|
||||||
<Filter
|
|
||||||
to={Route.fullPath}
|
|
||||||
enabled={inactive}
|
|
||||||
search={{ ...DEFAULT_PAGE, inactive: inactive ? undefined : true }}
|
|
||||||
>
|
|
||||||
{t("frontend.last_active.inactive_90_days")}
|
|
||||||
</Filter>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{edges.map((n) => (
|
|
||||||
<BrowserSession
|
|
||||||
key={n.cursor}
|
|
||||||
session={n.node}
|
|
||||||
isCurrent={currentSession.id === n.node.id}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{currentSession.user.browserSessions.totalCount === 0 && (
|
|
||||||
<EmptyState>
|
|
||||||
{inactive
|
|
||||||
? t(
|
|
||||||
"frontend.browser_sessions_overview.no_active_sessions.inactive_90_days",
|
|
||||||
)
|
|
||||||
: t(
|
|
||||||
"frontend.browser_sessions_overview.no_active_sessions.default",
|
|
||||||
)}
|
|
||||||
</EmptyState>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Only show the pagination buttons if there are pages to go to */}
|
|
||||||
{(forwardPage || backwardPage) && (
|
|
||||||
<div className="flex *:flex-1">
|
|
||||||
<ButtonLink
|
|
||||||
kind="secondary"
|
|
||||||
size="sm"
|
|
||||||
disabled={!forwardPage}
|
|
||||||
to={Route.fullPath}
|
|
||||||
search={forwardPage || pagination}
|
|
||||||
>
|
|
||||||
{t("common.previous")}
|
|
||||||
</ButtonLink>
|
|
||||||
|
|
||||||
{/* Spacer */}
|
|
||||||
<div />
|
|
||||||
|
|
||||||
<ButtonLink
|
|
||||||
kind="secondary"
|
|
||||||
size="sm"
|
|
||||||
disabled={!backwardPage}
|
|
||||||
to={Route.fullPath}
|
|
||||||
search={backwardPage || pagination}
|
|
||||||
>
|
|
||||||
{t("common.next")}
|
|
||||||
</ButtonLink>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</BlockList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
150
frontend/src/routes/_account.sessions.index.lazy.tsx
Normal file
150
frontend/src/routes/_account.sessions.index.lazy.tsx
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
// 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 { createLazyFileRoute, notFound } from "@tanstack/react-router";
|
||||||
|
import { H3, Separator } from "@vector-im/compound-web";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useQuery } from "urql";
|
||||||
|
|
||||||
|
import BlockList from "../components/BlockList";
|
||||||
|
import { ButtonLink } from "../components/ButtonLink";
|
||||||
|
import CompatSession from "../components/CompatSession";
|
||||||
|
import EmptyState from "../components/EmptyState";
|
||||||
|
import Filter from "../components/Filter";
|
||||||
|
import OAuth2Session from "../components/OAuth2Session";
|
||||||
|
import BrowserSessionsOverview from "../components/UserSessionsOverview/BrowserSessionsOverview";
|
||||||
|
import { type BackwardPagination, usePages } from "../pagination";
|
||||||
|
|
||||||
|
import { QUERY, LIST_QUERY } from "./_account.sessions.index";
|
||||||
|
|
||||||
|
const PAGE_SIZE = 6;
|
||||||
|
const DEFAULT_PAGE: BackwardPagination = { last: PAGE_SIZE };
|
||||||
|
|
||||||
|
// A type-safe way to ensure we've handled all session types
|
||||||
|
const unknownSessionType = (type: never): never => {
|
||||||
|
throw new Error(`Unknown session type: ${type}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNintyDaysAgo = (): string => {
|
||||||
|
const date = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
|
||||||
|
// Round down to the start of the day to avoid rerendering/requerying
|
||||||
|
date.setHours(0, 0, 0, 0);
|
||||||
|
return date.toISOString();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/_account/sessions/")({
|
||||||
|
component: Sessions,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Sessions(): React.ReactElement {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { inactive, ...pagination } = Route.useLoaderDeps();
|
||||||
|
const [overview] = useQuery({ query: QUERY });
|
||||||
|
if (overview.error) throw overview.error;
|
||||||
|
const user =
|
||||||
|
overview.data?.viewer.__typename === "User" ? overview.data.viewer : null;
|
||||||
|
if (user === null) throw notFound();
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
lastActive: inactive ? { before: getNintyDaysAgo() } : undefined,
|
||||||
|
...pagination,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [list] = useQuery({ query: LIST_QUERY, variables });
|
||||||
|
if (list.error) throw list.error;
|
||||||
|
const appSessions =
|
||||||
|
list.data?.viewer.__typename === "User"
|
||||||
|
? list.data.viewer.appSessions
|
||||||
|
: null;
|
||||||
|
if (appSessions === null) throw notFound();
|
||||||
|
|
||||||
|
const [backwardPage, forwardPage] = usePages(
|
||||||
|
pagination,
|
||||||
|
appSessions.pageInfo,
|
||||||
|
PAGE_SIZE,
|
||||||
|
);
|
||||||
|
|
||||||
|
// We reverse the list as we are paginating backwards
|
||||||
|
const edges = [...appSessions.edges].reverse();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BlockList>
|
||||||
|
<H3>{t("frontend.user_sessions_overview.heading")}</H3>
|
||||||
|
<BrowserSessionsOverview user={user} />
|
||||||
|
<Separator />
|
||||||
|
<div className="flex gap-2 justify-start items-center">
|
||||||
|
<Filter
|
||||||
|
to="/sessions"
|
||||||
|
enabled={inactive}
|
||||||
|
search={{ ...DEFAULT_PAGE, inactive: inactive ? undefined : true }}
|
||||||
|
>
|
||||||
|
{t("frontend.last_active.inactive_90_days")}
|
||||||
|
</Filter>
|
||||||
|
</div>
|
||||||
|
{edges.map((session) => {
|
||||||
|
const type = session.node.__typename;
|
||||||
|
switch (type) {
|
||||||
|
case "Oauth2Session":
|
||||||
|
return (
|
||||||
|
<OAuth2Session key={session.cursor} session={session.node} />
|
||||||
|
);
|
||||||
|
case "CompatSession":
|
||||||
|
return (
|
||||||
|
<CompatSession key={session.cursor} session={session.node} />
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
unknownSessionType(type);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
|
||||||
|
{appSessions.totalCount === 0 && (
|
||||||
|
<EmptyState>
|
||||||
|
{inactive
|
||||||
|
? t(
|
||||||
|
"frontend.user_sessions_overview.no_active_sessions.inactive_90_days",
|
||||||
|
)
|
||||||
|
: t("frontend.user_sessions_overview.no_active_sessions.default")}
|
||||||
|
</EmptyState>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Only show the pagination buttons if there are pages to go to */}
|
||||||
|
{(forwardPage || backwardPage) && (
|
||||||
|
<div className="flex *:flex-1">
|
||||||
|
<ButtonLink
|
||||||
|
kind="secondary"
|
||||||
|
size="sm"
|
||||||
|
disabled={!forwardPage}
|
||||||
|
to="/sessions"
|
||||||
|
search={{ inactive, ...(forwardPage || pagination) }}
|
||||||
|
>
|
||||||
|
{t("common.previous")}
|
||||||
|
</ButtonLink>
|
||||||
|
|
||||||
|
{/* Spacer */}
|
||||||
|
<div />
|
||||||
|
|
||||||
|
<ButtonLink
|
||||||
|
kind="secondary"
|
||||||
|
size="sm"
|
||||||
|
disabled={!backwardPage}
|
||||||
|
to="/sessions"
|
||||||
|
search={{ inactive, ...(backwardPage || pagination) }}
|
||||||
|
>
|
||||||
|
{t("common.next")}
|
||||||
|
</ButtonLink>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</BlockList>
|
||||||
|
);
|
||||||
|
}
|
@@ -13,30 +13,19 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { createFileRoute, notFound } from "@tanstack/react-router";
|
import { createFileRoute, notFound } from "@tanstack/react-router";
|
||||||
import { H3, Separator } from "@vector-im/compound-web";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useQuery } from "urql";
|
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
|
|
||||||
import BlockList from "../components/BlockList";
|
|
||||||
import { ButtonLink } from "../components/ButtonLink";
|
|
||||||
import CompatSession from "../components/CompatSession";
|
|
||||||
import EmptyState from "../components/EmptyState";
|
|
||||||
import Filter from "../components/Filter";
|
|
||||||
import OAuth2Session from "../components/OAuth2Session";
|
|
||||||
import BrowserSessionsOverview from "../components/UserSessionsOverview/BrowserSessionsOverview";
|
|
||||||
import { graphql } from "../gql";
|
import { graphql } from "../gql";
|
||||||
import {
|
import {
|
||||||
BackwardPagination,
|
type BackwardPagination,
|
||||||
Pagination,
|
type Pagination,
|
||||||
paginationSchema,
|
paginationSchema,
|
||||||
usePages,
|
|
||||||
} from "../pagination";
|
} from "../pagination";
|
||||||
|
|
||||||
const PAGE_SIZE = 6;
|
const PAGE_SIZE = 6;
|
||||||
const DEFAULT_PAGE: BackwardPagination = { last: PAGE_SIZE };
|
const DEFAULT_PAGE: BackwardPagination = { last: PAGE_SIZE };
|
||||||
|
|
||||||
const QUERY = graphql(/* GraphQL */ `
|
export const QUERY = graphql(/* GraphQL */ `
|
||||||
query SessionsOverviewQuery {
|
query SessionsOverviewQuery {
|
||||||
viewer {
|
viewer {
|
||||||
__typename
|
__typename
|
||||||
@@ -49,7 +38,7 @@ const QUERY = graphql(/* GraphQL */ `
|
|||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const LIST_QUERY = graphql(/* GraphQL */ `
|
export const LIST_QUERY = graphql(/* GraphQL */ `
|
||||||
query AppSessionsListQuery(
|
query AppSessionsListQuery(
|
||||||
$before: String
|
$before: String
|
||||||
$after: String
|
$after: String
|
||||||
@@ -92,11 +81,6 @@ const LIST_QUERY = graphql(/* GraphQL */ `
|
|||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// A type-safe way to ensure we've handled all session types
|
|
||||||
const unknownSessionType = (type: never): never => {
|
|
||||||
throw new Error(`Unknown session type: ${type}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const searchSchema = z.object({
|
const searchSchema = z.object({
|
||||||
inactive: z.literal(true).optional().catch(undefined),
|
inactive: z.literal(true).optional().catch(undefined),
|
||||||
});
|
});
|
||||||
@@ -139,108 +123,4 @@ export const Route = createFileRoute("/_account/sessions/")({
|
|||||||
if (overview.data?.viewer?.__typename !== "User") throw notFound();
|
if (overview.data?.viewer?.__typename !== "User") throw notFound();
|
||||||
if (list.data?.viewer?.__typename !== "User") throw notFound();
|
if (list.data?.viewer?.__typename !== "User") throw notFound();
|
||||||
},
|
},
|
||||||
|
|
||||||
component: Sessions,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function Sessions(): React.ReactElement {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { inactive, ...pagination } = Route.useLoaderDeps();
|
|
||||||
const [overview] = useQuery({ query: QUERY });
|
|
||||||
if (overview.error) throw overview.error;
|
|
||||||
const user =
|
|
||||||
overview.data?.viewer.__typename === "User" ? overview.data.viewer : null;
|
|
||||||
if (user === null) throw notFound();
|
|
||||||
|
|
||||||
const variables = {
|
|
||||||
lastActive: inactive ? { before: getNintyDaysAgo() } : undefined,
|
|
||||||
...pagination,
|
|
||||||
};
|
|
||||||
|
|
||||||
const [list] = useQuery({ query: LIST_QUERY, variables });
|
|
||||||
if (list.error) throw list.error;
|
|
||||||
const appSessions =
|
|
||||||
list.data?.viewer.__typename === "User"
|
|
||||||
? list.data.viewer.appSessions
|
|
||||||
: null;
|
|
||||||
if (appSessions === null) throw notFound();
|
|
||||||
|
|
||||||
const [backwardPage, forwardPage] = usePages(
|
|
||||||
pagination,
|
|
||||||
appSessions.pageInfo,
|
|
||||||
PAGE_SIZE,
|
|
||||||
);
|
|
||||||
|
|
||||||
// We reverse the list as we are paginating backwards
|
|
||||||
const edges = [...appSessions.edges].reverse();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BlockList>
|
|
||||||
<H3>{t("frontend.user_sessions_overview.heading")}</H3>
|
|
||||||
<BrowserSessionsOverview user={user} />
|
|
||||||
<Separator />
|
|
||||||
<div className="flex gap-2 justify-start items-center">
|
|
||||||
<Filter
|
|
||||||
to={Route.fullPath}
|
|
||||||
enabled={inactive}
|
|
||||||
search={{ ...DEFAULT_PAGE, inactive: inactive ? undefined : true }}
|
|
||||||
>
|
|
||||||
{t("frontend.last_active.inactive_90_days")}
|
|
||||||
</Filter>
|
|
||||||
</div>
|
|
||||||
{edges.map((session) => {
|
|
||||||
const type = session.node.__typename;
|
|
||||||
switch (type) {
|
|
||||||
case "Oauth2Session":
|
|
||||||
return (
|
|
||||||
<OAuth2Session key={session.cursor} session={session.node} />
|
|
||||||
);
|
|
||||||
case "CompatSession":
|
|
||||||
return (
|
|
||||||
<CompatSession key={session.cursor} session={session.node} />
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
unknownSessionType(type);
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
|
|
||||||
{appSessions.totalCount === 0 && (
|
|
||||||
<EmptyState>
|
|
||||||
{inactive
|
|
||||||
? t(
|
|
||||||
"frontend.user_sessions_overview.no_active_sessions.inactive_90_days",
|
|
||||||
)
|
|
||||||
: t("frontend.user_sessions_overview.no_active_sessions.default")}
|
|
||||||
</EmptyState>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Only show the pagination buttons if there are pages to go to */}
|
|
||||||
{(forwardPage || backwardPage) && (
|
|
||||||
<div className="flex *:flex-1">
|
|
||||||
<ButtonLink
|
|
||||||
kind="secondary"
|
|
||||||
size="sm"
|
|
||||||
disabled={!forwardPage}
|
|
||||||
to={Route.fullPath}
|
|
||||||
search={{ inactive, ...(forwardPage || pagination) }}
|
|
||||||
>
|
|
||||||
{t("common.previous")}
|
|
||||||
</ButtonLink>
|
|
||||||
|
|
||||||
{/* Spacer */}
|
|
||||||
<div />
|
|
||||||
|
|
||||||
<ButtonLink
|
|
||||||
kind="secondary"
|
|
||||||
size="sm"
|
|
||||||
disabled={!backwardPage}
|
|
||||||
to={Route.fullPath}
|
|
||||||
search={{ inactive, ...(backwardPage || pagination) }}
|
|
||||||
>
|
|
||||||
{t("common.next")}
|
|
||||||
</ButtonLink>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</BlockList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
@@ -12,21 +12,11 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Outlet, createFileRoute, notFound } from "@tanstack/react-router";
|
import { createFileRoute, notFound } from "@tanstack/react-router";
|
||||||
import { Heading } from "@vector-im/compound-web";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useQuery } from "urql";
|
|
||||||
|
|
||||||
import { useEndBrowserSession } from "../components/BrowserSession";
|
|
||||||
import Layout from "../components/Layout";
|
|
||||||
import NavBar from "../components/NavBar";
|
|
||||||
import NavItem from "../components/NavItem";
|
|
||||||
import EndSessionButton from "../components/Session/EndSessionButton";
|
|
||||||
import UnverifiedEmailAlert from "../components/UnverifiedEmailAlert";
|
|
||||||
import UserGreeting from "../components/UserGreeting";
|
|
||||||
import { graphql } from "../gql";
|
import { graphql } from "../gql";
|
||||||
|
|
||||||
const QUERY = graphql(/* GraphQL */ `
|
export const QUERY = graphql(/* GraphQL */ `
|
||||||
query CurrentUserGreeting {
|
query CurrentUserGreeting {
|
||||||
viewerSession {
|
viewerSession {
|
||||||
__typename
|
__typename
|
||||||
@@ -60,43 +50,4 @@ export const Route = createFileRoute("/_account")({
|
|||||||
if (result.data?.viewerSession.__typename !== "BrowserSession")
|
if (result.data?.viewerSession.__typename !== "BrowserSession")
|
||||||
throw notFound();
|
throw notFound();
|
||||||
},
|
},
|
||||||
component: Account,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function Account(): React.ReactElement {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [result] = useQuery({
|
|
||||||
query: QUERY,
|
|
||||||
});
|
|
||||||
if (result.error) throw result.error;
|
|
||||||
const session = result.data?.viewerSession;
|
|
||||||
if (session?.__typename !== "BrowserSession") throw notFound();
|
|
||||||
const siteConfig = result.data?.siteConfig;
|
|
||||||
if (!siteConfig) throw Error(); // This should never happen
|
|
||||||
const onSessionEnd = useEndBrowserSession(session.id, true);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Layout wide>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<header className="flex justify-between mb-4">
|
|
||||||
<Heading size="lg" weight="semibold">
|
|
||||||
{t("frontend.account.title")}
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
<EndSessionButton endSession={onSessionEnd} />
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<UserGreeting user={session.user} siteConfig={siteConfig} />
|
|
||||||
|
|
||||||
<UnverifiedEmailAlert user={session.user} />
|
|
||||||
|
|
||||||
<NavBar>
|
|
||||||
<NavItem to="/">{t("frontend.nav.settings")}</NavItem>
|
|
||||||
<NavItem to="/sessions">{t("frontend.nav.devices")}</NavItem>
|
|
||||||
</NavBar>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Outlet />
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
42
frontend/src/routes/clients.$id.lazy.tsx
Normal file
42
frontend/src/routes/clients.$id.lazy.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// 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 { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
import { useQuery } from "urql";
|
||||||
|
|
||||||
|
import OAuth2ClientDetail from "../components/Client/OAuth2ClientDetail";
|
||||||
|
import Layout from "../components/Layout";
|
||||||
|
|
||||||
|
import { QUERY } from "./clients.$id";
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/clients/$id")({
|
||||||
|
component: ClientDetail,
|
||||||
|
});
|
||||||
|
|
||||||
|
function ClientDetail(): React.ReactElement {
|
||||||
|
const { id } = Route.useParams();
|
||||||
|
const [result] = useQuery({
|
||||||
|
query: QUERY,
|
||||||
|
variables: { id },
|
||||||
|
});
|
||||||
|
if (result.error) throw result.error;
|
||||||
|
const client = result.data?.oauth2Client;
|
||||||
|
if (!client) throw new Error(); // Should have been caught by the loader
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<OAuth2ClientDetail client={client} />
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
@@ -13,13 +13,10 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { createFileRoute, notFound } from "@tanstack/react-router";
|
import { createFileRoute, notFound } from "@tanstack/react-router";
|
||||||
import { useQuery } from "urql";
|
|
||||||
|
|
||||||
import OAuth2ClientDetail from "../components/Client/OAuth2ClientDetail";
|
|
||||||
import Layout from "../components/Layout";
|
|
||||||
import { graphql } from "../gql";
|
import { graphql } from "../gql";
|
||||||
|
|
||||||
const QUERY = graphql(/* GraphQL */ `
|
export const QUERY = graphql(/* GraphQL */ `
|
||||||
query OAuth2ClientQuery($id: ID!) {
|
query OAuth2ClientQuery($id: ID!) {
|
||||||
oauth2Client(id: $id) {
|
oauth2Client(id: $id) {
|
||||||
...OAuth2Client_detail
|
...OAuth2Client_detail
|
||||||
@@ -37,22 +34,4 @@ export const Route = createFileRoute("/clients/$id")({
|
|||||||
if (result.error) throw result.error;
|
if (result.error) throw result.error;
|
||||||
if (!result.data?.oauth2Client) throw notFound();
|
if (!result.data?.oauth2Client) throw notFound();
|
||||||
},
|
},
|
||||||
component: ClientDetail,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function ClientDetail(): React.ReactElement {
|
|
||||||
const { id } = Route.useParams();
|
|
||||||
const [result] = useQuery({
|
|
||||||
query: QUERY,
|
|
||||||
variables: { id },
|
|
||||||
});
|
|
||||||
if (result.error) throw result.error;
|
|
||||||
const client = result.data?.oauth2Client;
|
|
||||||
if (!client) throw new Error(); // Should have been caught by the loader
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Layout>
|
|
||||||
<OAuth2ClientDetail client={client} />
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
40
frontend/src/routes/emails.$id.verify.lazy.tsx
Normal file
40
frontend/src/routes/emails.$id.verify.lazy.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// 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 { createLazyFileRoute, notFound } from "@tanstack/react-router";
|
||||||
|
import { useQuery } from "urql";
|
||||||
|
|
||||||
|
import Layout from "../components/Layout";
|
||||||
|
import VerifyEmailComponent from "../components/VerifyEmail";
|
||||||
|
|
||||||
|
import { QUERY } from "./emails.$id.verify";
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/emails/$id/verify")({
|
||||||
|
component: EmailVerify,
|
||||||
|
});
|
||||||
|
|
||||||
|
function EmailVerify(): React.ReactElement {
|
||||||
|
const { id } = Route.useParams();
|
||||||
|
const [result] = useQuery({ query: QUERY, variables: { id } });
|
||||||
|
|
||||||
|
if (result.error) throw result.error;
|
||||||
|
const email = result.data?.userEmail;
|
||||||
|
if (email == null) throw notFound();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<VerifyEmailComponent email={email} />
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
@@ -13,13 +13,10 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { createFileRoute, notFound } from "@tanstack/react-router";
|
import { createFileRoute, notFound } from "@tanstack/react-router";
|
||||||
import { useQuery } from "urql";
|
|
||||||
|
|
||||||
import Layout from "../components/Layout";
|
|
||||||
import VerifyEmailComponent from "../components/VerifyEmail";
|
|
||||||
import { graphql } from "../gql";
|
import { graphql } from "../gql";
|
||||||
|
|
||||||
const QUERY = graphql(/* GraphQL */ `
|
export const QUERY = graphql(/* GraphQL */ `
|
||||||
query VerifyEmailQuery($id: ID!) {
|
query VerifyEmailQuery($id: ID!) {
|
||||||
userEmail(id: $id) {
|
userEmail(id: $id) {
|
||||||
...UserEmail_verifyEmail
|
...UserEmail_verifyEmail
|
||||||
@@ -39,21 +36,4 @@ export const Route = createFileRoute("/emails/$id/verify")({
|
|||||||
if (result.error) throw result.error;
|
if (result.error) throw result.error;
|
||||||
if (!result.data?.userEmail) throw notFound();
|
if (!result.data?.userEmail) throw notFound();
|
||||||
},
|
},
|
||||||
|
|
||||||
component: EmailVerify,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function EmailVerify(): React.ReactElement {
|
|
||||||
const { id } = Route.useParams();
|
|
||||||
const [result] = useQuery({ query: QUERY, variables: { id } });
|
|
||||||
|
|
||||||
if (result.error) throw result.error;
|
|
||||||
const email = result.data?.userEmail;
|
|
||||||
if (email == null) throw notFound();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Layout>
|
|
||||||
<VerifyEmailComponent email={email} />
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
@@ -33,20 +33,7 @@ import { graphql } from "../gql";
|
|||||||
import { SetPasswordStatus } from "../gql/graphql";
|
import { SetPasswordStatus } from "../gql/graphql";
|
||||||
import { translateSetPasswordError } from "../i18n/password_changes";
|
import { translateSetPasswordError } from "../i18n/password_changes";
|
||||||
|
|
||||||
const QUERY = graphql(/* GraphQL */ `
|
import { QUERY } from "./password.change.index";
|
||||||
query PasswordChangeQuery {
|
|
||||||
viewer {
|
|
||||||
__typename
|
|
||||||
... on Node {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
siteConfig {
|
|
||||||
...PasswordCreationDoubleInput_siteConfig
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
const CHANGE_PASSWORD_MUTATION = graphql(/* GraphQL */ `
|
const CHANGE_PASSWORD_MUTATION = graphql(/* GraphQL */ `
|
||||||
mutation ChangePassword(
|
mutation ChangePassword(
|
||||||
|
@@ -16,7 +16,7 @@ import { createFileRoute, notFound } from "@tanstack/react-router";
|
|||||||
|
|
||||||
import { graphql } from "../gql";
|
import { graphql } from "../gql";
|
||||||
|
|
||||||
const QUERY = graphql(/* GraphQL */ `
|
export const QUERY = graphql(/* GraphQL */ `
|
||||||
query PasswordChangeQuery {
|
query PasswordChangeQuery {
|
||||||
viewer {
|
viewer {
|
||||||
__typename
|
__typename
|
||||||
|
@@ -12,47 +12,21 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { createFileRoute, notFound } from "@tanstack/react-router";
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
import IconCheckCircle from "@vector-im/compound-design-tokens/assets/web/icons/check-circle-solid";
|
import IconCheckCircle from "@vector-im/compound-design-tokens/assets/web/icons/check-circle-solid";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useQuery } from "urql";
|
|
||||||
|
|
||||||
import BlockList from "../components/BlockList";
|
import BlockList from "../components/BlockList";
|
||||||
import { ButtonLink } from "../components/ButtonLink";
|
import { ButtonLink } from "../components/ButtonLink";
|
||||||
import Layout from "../components/Layout";
|
import Layout from "../components/Layout";
|
||||||
import PageHeading from "../components/PageHeading";
|
import PageHeading from "../components/PageHeading";
|
||||||
import { graphql } from "../gql";
|
|
||||||
|
|
||||||
const CURRENT_VIEWER_QUERY = graphql(/* GraphQL */ `
|
|
||||||
query CurrentViewerQuery {
|
|
||||||
viewer {
|
|
||||||
__typename
|
|
||||||
... on Node {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/password/change/success")({
|
|
||||||
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();
|
|
||||||
},
|
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/password/change/success")({
|
||||||
component: ChangePasswordSuccess,
|
component: ChangePasswordSuccess,
|
||||||
});
|
});
|
||||||
|
|
||||||
function ChangePasswordSuccess(): React.ReactNode {
|
function ChangePasswordSuccess(): React.ReactNode {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [viewer] = useQuery({ query: CURRENT_VIEWER_QUERY });
|
|
||||||
if (viewer.error) throw viewer.error;
|
|
||||||
if (viewer.data?.viewer.__typename !== "User") throw notFound();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
@@ -32,14 +32,7 @@ import { graphql } from "../gql";
|
|||||||
import { SetPasswordStatus } from "../gql/graphql";
|
import { SetPasswordStatus } from "../gql/graphql";
|
||||||
import { translateSetPasswordError } from "../i18n/password_changes";
|
import { translateSetPasswordError } from "../i18n/password_changes";
|
||||||
|
|
||||||
const QUERY = graphql(/* GraphQL */ `
|
import { QUERY } from "./password.recovery.index";
|
||||||
query PasswordRecoveryQuery {
|
|
||||||
siteConfig {
|
|
||||||
id
|
|
||||||
...PasswordCreationDoubleInput_siteConfig
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
const RECOVER_PASSWORD_MUTATION = graphql(/* GraphQL */ `
|
const RECOVER_PASSWORD_MUTATION = graphql(/* GraphQL */ `
|
||||||
mutation RecoverPassword($ticket: String!, $newPassword: String!) {
|
mutation RecoverPassword($ticket: String!, $newPassword: String!) {
|
||||||
|
@@ -16,7 +16,7 @@ import { createFileRoute } from "@tanstack/react-router";
|
|||||||
|
|
||||||
import { graphql } from "../gql";
|
import { graphql } from "../gql";
|
||||||
|
|
||||||
const QUERY = graphql(/* GraphQL */ `
|
export const QUERY = graphql(/* GraphQL */ `
|
||||||
query PasswordRecoveryQuery {
|
query PasswordRecoveryQuery {
|
||||||
siteConfig {
|
siteConfig {
|
||||||
id
|
id
|
||||||
|
109
frontend/src/routes/reset-cross-signing.lazy.tsx
Normal file
109
frontend/src/routes/reset-cross-signing.lazy.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
// 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 { createLazyFileRoute, notFound } from "@tanstack/react-router";
|
||||||
|
import IconArrowLeft from "@vector-im/compound-design-tokens/assets/web/icons/arrow-left";
|
||||||
|
import IconKey from "@vector-im/compound-design-tokens/assets/web/icons/key";
|
||||||
|
import { Alert, Button, Text } from "@vector-im/compound-web";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useMutation, useQuery } from "urql";
|
||||||
|
|
||||||
|
import BlockList from "../components/BlockList";
|
||||||
|
import { ButtonLink } from "../components/ButtonLink";
|
||||||
|
import Layout from "../components/Layout";
|
||||||
|
import LoadingSpinner from "../components/LoadingSpinner";
|
||||||
|
import PageHeading from "../components/PageHeading";
|
||||||
|
import { graphql } from "../gql";
|
||||||
|
|
||||||
|
import { CURRENT_VIEWER_QUERY } from "./reset-cross-signing";
|
||||||
|
|
||||||
|
const ALLOW_CROSS_SIGING_RESET_MUTATION = graphql(/* GraphQL */ `
|
||||||
|
mutation AllowCrossSigningReset($userId: ID!) {
|
||||||
|
allowUserCrossSigningReset(input: { userId: $userId }) {
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/reset-cross-signing")({
|
||||||
|
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 (
|
||||||
|
<Layout>
|
||||||
|
<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="/" kind="tertiary" Icon={IconArrowLeft}>
|
||||||
|
{t("action.back")}
|
||||||
|
</ButtonLink>
|
||||||
|
)}
|
||||||
|
</BlockList>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
@@ -12,26 +12,16 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { createFileRoute, notFound } from "@tanstack/react-router";
|
import { notFound, createFileRoute } from "@tanstack/react-router";
|
||||||
import IconArrowLeft from "@vector-im/compound-design-tokens/assets/web/icons/arrow-left";
|
|
||||||
import IconKey from "@vector-im/compound-design-tokens/assets/web/icons/key";
|
|
||||||
import { Alert, Button, Text } from "@vector-im/compound-web";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useMutation, useQuery } from "urql";
|
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
|
|
||||||
import BlockList from "../components/BlockList";
|
|
||||||
import { ButtonLink } from "../components/ButtonLink";
|
|
||||||
import Layout from "../components/Layout";
|
|
||||||
import LoadingSpinner from "../components/LoadingSpinner";
|
|
||||||
import PageHeading from "../components/PageHeading";
|
|
||||||
import { graphql } from "../gql";
|
import { graphql } from "../gql";
|
||||||
|
|
||||||
const searchSchema = z.object({
|
const searchSchema = z.object({
|
||||||
deepLink: z.boolean().optional(),
|
deepLink: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const CURRENT_VIEWER_QUERY = graphql(/* GraphQL */ `
|
export const CURRENT_VIEWER_QUERY = graphql(/* GraphQL */ `
|
||||||
query CurrentViewerQuery {
|
query CurrentViewerQuery {
|
||||||
viewer {
|
viewer {
|
||||||
__typename
|
__typename
|
||||||
@@ -42,16 +32,6 @@ const CURRENT_VIEWER_QUERY = graphql(/* GraphQL */ `
|
|||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
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")({
|
export const Route = createFileRoute("/reset-cross-signing")({
|
||||||
async loader({ context, abortController: { signal } }) {
|
async loader({ context, abortController: { signal } }) {
|
||||||
const viewer = await context.client.query(
|
const viewer = await context.client.query(
|
||||||
@@ -64,72 +44,4 @@ export const Route = createFileRoute("/reset-cross-signing")({
|
|||||||
},
|
},
|
||||||
|
|
||||||
validateSearch: searchSchema,
|
validateSearch: searchSchema,
|
||||||
|
|
||||||
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 (
|
|
||||||
<Layout>
|
|
||||||
<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="/" kind="tertiary" Icon={IconArrowLeft}>
|
|
||||||
{t("action.back")}
|
|
||||||
</ButtonLink>
|
|
||||||
)}
|
|
||||||
</BlockList>
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
Reference in New Issue
Block a user