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 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 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 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 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,
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@@ -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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
@@ -1697,14 +1697,6 @@ export type VerifyEmailQueryQuery = { __typename?: 'Query', userEmail?: (
|
||||
& { ' $fragmentRefs'?: { 'UserEmail_VerifyEmailFragment': UserEmail_VerifyEmailFragment } }
|
||||
) | 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<{
|
||||
userId: Scalars['ID']['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 PasswordRecoveryQueryQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
export type PasswordChangeQueryQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type PasswordRecoveryQueryQuery = { __typename?: 'Query', siteConfig: (
|
||||
{ __typename?: 'SiteConfig', id: string }
|
||||
export type PasswordChangeQueryQuery = { __typename?: 'Query', viewer: { __typename: 'Anonymous', id: string } | { __typename: 'User', id: string }, siteConfig: (
|
||||
{ __typename?: 'SiteConfig' }
|
||||
& { ' $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 PasswordRecoveryQueryQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type PasswordRecoveryQueryQuery = { __typename?: 'Query', siteConfig: (
|
||||
{ __typename?: 'SiteConfig', id: string }
|
||||
& { ' $fragmentRefs'?: { 'PasswordCreationDoubleInput_SiteConfigFragment': PasswordCreationDoubleInput_SiteConfigFragment } }
|
||||
) };
|
||||
|
||||
export type AllowCrossSigningResetMutationVariables = Exact<{
|
||||
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 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 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 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 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>;
|
@@ -8,6 +8,8 @@
|
||||
|
||||
// This file is auto-generated by TanStack Router
|
||||
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
// Import Routes
|
||||
|
||||
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 PasswordChangeIndexImport } from './routes/password.change.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 AccountSessionsBrowsersImport } from './routes/_account.sessions.browsers'
|
||||
import { Route as AccountSessionsIdImport } from './routes/_account.sessions.$id'
|
||||
|
||||
// Create Virtual Routes
|
||||
|
||||
const PasswordChangeSuccessLazyImport = createFileRoute(
|
||||
'/password/change/success',
|
||||
)()
|
||||
|
||||
// Create/Update Routes
|
||||
|
||||
const ResetCrossSigningRoute = ResetCrossSigningImport.update({
|
||||
path: '/reset-cross-signing',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
} as any).lazy(() =>
|
||||
import('./routes/reset-cross-signing.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const AccountRoute = AccountImport.update({
|
||||
id: '/_account',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
} as any).lazy(() => import('./routes/_account.lazy').then((d) => d.Route))
|
||||
|
||||
const AccountIndexRoute = AccountIndexImport.update({
|
||||
path: '/',
|
||||
getParentRoute: () => AccountRoute,
|
||||
} as any)
|
||||
} as any).lazy(() =>
|
||||
import('./routes/_account.index.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const DevicesSplatRoute = DevicesSplatImport.update({
|
||||
path: '/devices/$',
|
||||
@@ -49,7 +60,7 @@ const DevicesSplatRoute = DevicesSplatImport.update({
|
||||
const ClientsIdRoute = ClientsIdImport.update({
|
||||
path: '/clients/$id',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
} as any).lazy(() => import('./routes/clients.$id.lazy').then((d) => d.Route))
|
||||
|
||||
const PasswordRecoveryIndexRoute = PasswordRecoveryIndexImport.update({
|
||||
path: '/password/recovery/',
|
||||
@@ -68,27 +79,37 @@ const PasswordChangeIndexRoute = PasswordChangeIndexImport.update({
|
||||
const AccountSessionsIndexRoute = AccountSessionsIndexImport.update({
|
||||
path: '/sessions/',
|
||||
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',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
} as any).lazy(() =>
|
||||
import('./routes/password.change.success.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const EmailsIdVerifyRoute = EmailsIdVerifyImport.update({
|
||||
path: '/emails/$id/verify',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
} as any).lazy(() =>
|
||||
import('./routes/emails.$id.verify.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const AccountSessionsBrowsersRoute = AccountSessionsBrowsersImport.update({
|
||||
path: '/sessions/browsers',
|
||||
getParentRoute: () => AccountRoute,
|
||||
} as any)
|
||||
} as any).lazy(() =>
|
||||
import('./routes/_account.sessions.browsers.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const AccountSessionsIdRoute = AccountSessionsIdImport.update({
|
||||
path: '/sessions/$id',
|
||||
getParentRoute: () => AccountRoute,
|
||||
} as any)
|
||||
} as any).lazy(() =>
|
||||
import('./routes/_account.sessions.$id.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
// Populate the FileRoutesByPath interface
|
||||
|
||||
@@ -154,7 +175,7 @@ declare module '@tanstack/react-router' {
|
||||
id: '/password/change/success'
|
||||
path: '/password/change/success'
|
||||
fullPath: '/password/change/success'
|
||||
preLoaderRoute: typeof PasswordChangeSuccessImport
|
||||
preLoaderRoute: typeof PasswordChangeSuccessLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/_account/sessions/': {
|
||||
@@ -194,7 +215,7 @@ export const routeTree = rootRoute.addChildren({
|
||||
ClientsIdRoute,
|
||||
DevicesSplatRoute,
|
||||
EmailsIdVerifyRoute,
|
||||
PasswordChangeSuccessRoute,
|
||||
PasswordChangeSuccessLazyRoute,
|
||||
PasswordChangeIndexRoute,
|
||||
PasswordRecoveryIndexRoute,
|
||||
})
|
||||
@@ -251,7 +272,7 @@ export const routeTree = rootRoute.addChildren({
|
||||
"filePath": "emails.$id.verify.tsx"
|
||||
},
|
||||
"/password/change/success": {
|
||||
"filePath": "password.change.success.tsx"
|
||||
"filePath": "password.change.success.lazy.tsx"
|
||||
},
|
||||
"/_account/sessions/": {
|
||||
"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
|
||||
// limitations under the License.
|
||||
|
||||
import {
|
||||
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 { createFileRoute, notFound, redirect } from "@tanstack/react-router";
|
||||
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";
|
||||
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
export const QUERY = graphql(/* GraphQL */ `
|
||||
query UserProfileQuery {
|
||||
viewer {
|
||||
__typename
|
||||
@@ -137,68 +120,4 @@ export const Route = createFileRoute("/_account/")({
|
||||
if (result.error) throw result.error;
|
||||
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.
|
||||
|
||||
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";
|
||||
|
||||
export const Route = createFileRoute("/_account/sessions/$id")({
|
||||
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 */ `
|
||||
export const QUERY = graphql(/* GraphQL */ `
|
||||
query SessionDetailQuery($id: ID!) {
|
||||
viewerSession {
|
||||
... on Node {
|
||||
@@ -56,44 +34,14 @@ const QUERY = graphql(/* GraphQL */ `
|
||||
}
|
||||
`);
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
export const Route = createFileRoute("/_account/sessions/$id")({
|
||||
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();
|
||||
},
|
||||
});
|
||||
|
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.
|
||||
|
||||
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 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 {
|
||||
BackwardPagination,
|
||||
Pagination,
|
||||
type Pagination,
|
||||
type BackwardPagination,
|
||||
paginationSchema,
|
||||
usePages,
|
||||
} from "../pagination";
|
||||
|
||||
const PAGE_SIZE = 6;
|
||||
const DEFAULT_PAGE: BackwardPagination = { last: PAGE_SIZE };
|
||||
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
export const QUERY = graphql(/* GraphQL */ `
|
||||
query BrowserSessionList(
|
||||
$first: Int
|
||||
$after: String
|
||||
@@ -119,95 +110,5 @@ export const Route = createFileRoute("/_account/sessions/browsers")({
|
||||
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.
|
||||
|
||||
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 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 {
|
||||
BackwardPagination,
|
||||
Pagination,
|
||||
type BackwardPagination,
|
||||
type Pagination,
|
||||
paginationSchema,
|
||||
usePages,
|
||||
} from "../pagination";
|
||||
|
||||
const PAGE_SIZE = 6;
|
||||
const DEFAULT_PAGE: BackwardPagination = { last: PAGE_SIZE };
|
||||
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
export const QUERY = graphql(/* GraphQL */ `
|
||||
query SessionsOverviewQuery {
|
||||
viewer {
|
||||
__typename
|
||||
@@ -49,7 +38,7 @@ const QUERY = graphql(/* GraphQL */ `
|
||||
}
|
||||
`);
|
||||
|
||||
const LIST_QUERY = graphql(/* GraphQL */ `
|
||||
export const LIST_QUERY = graphql(/* GraphQL */ `
|
||||
query AppSessionsListQuery(
|
||||
$before: 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({
|
||||
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 (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
|
||||
// limitations under the License.
|
||||
|
||||
import { Outlet, createFileRoute, notFound } from "@tanstack/react-router";
|
||||
import { Heading } from "@vector-im/compound-web";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery } from "urql";
|
||||
import { createFileRoute, notFound } from "@tanstack/react-router";
|
||||
|
||||
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";
|
||||
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
export const QUERY = graphql(/* GraphQL */ `
|
||||
query CurrentUserGreeting {
|
||||
viewerSession {
|
||||
__typename
|
||||
@@ -60,43 +50,4 @@ export const Route = createFileRoute("/_account")({
|
||||
if (result.data?.viewerSession.__typename !== "BrowserSession")
|
||||
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.
|
||||
|
||||
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";
|
||||
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
export const QUERY = graphql(/* GraphQL */ `
|
||||
query OAuth2ClientQuery($id: ID!) {
|
||||
oauth2Client(id: $id) {
|
||||
...OAuth2Client_detail
|
||||
@@ -37,22 +34,4 @@ export const Route = createFileRoute("/clients/$id")({
|
||||
if (result.error) throw result.error;
|
||||
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.
|
||||
|
||||
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";
|
||||
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
export const QUERY = graphql(/* GraphQL */ `
|
||||
query VerifyEmailQuery($id: ID!) {
|
||||
userEmail(id: $id) {
|
||||
...UserEmail_verifyEmail
|
||||
@@ -39,21 +36,4 @@ export const Route = createFileRoute("/emails/$id/verify")({
|
||||
if (result.error) throw result.error;
|
||||
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 { translateSetPasswordError } from "../i18n/password_changes";
|
||||
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
query PasswordChangeQuery {
|
||||
viewer {
|
||||
__typename
|
||||
... on Node {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
siteConfig {
|
||||
...PasswordCreationDoubleInput_siteConfig
|
||||
}
|
||||
}
|
||||
`);
|
||||
import { QUERY } from "./password.change.index";
|
||||
|
||||
const CHANGE_PASSWORD_MUTATION = graphql(/* GraphQL */ `
|
||||
mutation ChangePassword(
|
||||
|
@@ -16,7 +16,7 @@ import { createFileRoute, notFound } from "@tanstack/react-router";
|
||||
|
||||
import { graphql } from "../gql";
|
||||
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
export const QUERY = graphql(/* GraphQL */ `
|
||||
query PasswordChangeQuery {
|
||||
viewer {
|
||||
__typename
|
||||
|
@@ -12,47 +12,21 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// 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 { useTranslation } from "react-i18next";
|
||||
import { useQuery } from "urql";
|
||||
|
||||
import BlockList from "../components/BlockList";
|
||||
import { ButtonLink } from "../components/ButtonLink";
|
||||
import Layout from "../components/Layout";
|
||||
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,
|
||||
});
|
||||
|
||||
function ChangePasswordSuccess(): React.ReactNode {
|
||||
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 (
|
||||
<Layout>
|
@@ -32,14 +32,7 @@ import { graphql } from "../gql";
|
||||
import { SetPasswordStatus } from "../gql/graphql";
|
||||
import { translateSetPasswordError } from "../i18n/password_changes";
|
||||
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
query PasswordRecoveryQuery {
|
||||
siteConfig {
|
||||
id
|
||||
...PasswordCreationDoubleInput_siteConfig
|
||||
}
|
||||
}
|
||||
`);
|
||||
import { QUERY } from "./password.recovery.index";
|
||||
|
||||
const RECOVER_PASSWORD_MUTATION = graphql(/* GraphQL */ `
|
||||
mutation RecoverPassword($ticket: String!, $newPassword: String!) {
|
||||
|
@@ -16,7 +16,7 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { graphql } from "../gql";
|
||||
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
export const QUERY = graphql(/* GraphQL */ `
|
||||
query PasswordRecoveryQuery {
|
||||
siteConfig {
|
||||
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
|
||||
// limitations under the License.
|
||||
|
||||
import { createFileRoute, 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 { notFound, createFileRoute } from "@tanstack/react-router";
|
||||
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";
|
||||
|
||||
const searchSchema = z.object({
|
||||
deepLink: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const CURRENT_VIEWER_QUERY = graphql(/* GraphQL */ `
|
||||
export const CURRENT_VIEWER_QUERY = graphql(/* GraphQL */ `
|
||||
query CurrentViewerQuery {
|
||||
viewer {
|
||||
__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")({
|
||||
async loader({ context, abortController: { signal } }) {
|
||||
const viewer = await context.client.query(
|
||||
@@ -64,72 +44,4 @@ export const Route = createFileRoute("/reset-cross-signing")({
|
||||
},
|
||||
|
||||
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