1
0
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:
Quentin Gliech
2024-07-31 12:38:07 +02:00
parent 0161771ef4
commit c7cfd76cbd
24 changed files with 804 additions and 644 deletions

View File

@@ -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.
*/ */

View File

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

View File

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

View 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>
</>
);
}

View File

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

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

View 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");
}
}

View File

@@ -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 })} if (result.error) throw result.error;
> if (!result.data?.node) throw notFound();
{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");
}
}

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

View File

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

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

@@ -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(

View File

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

View File

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

View File

@@ -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!) {

View File

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

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

View File

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