diff --git a/frontend/src/gql/gql.ts b/frontend/src/gql/gql.ts index ad51ac71..2d605bfc 100644 --- a/frontend/src/gql/gql.ts +++ b/frontend/src/gql/gql.ts @@ -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. */ diff --git a/frontend/src/gql/graphql.ts b/frontend/src/gql/graphql.ts index 9d2bb85d..d14a4ba5 100644 --- a/frontend/src/gql/graphql.ts +++ b/frontend/src/gql/graphql.ts @@ -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; 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; 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; -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; 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; -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; +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; 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; +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; 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; \ No newline at end of file diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 4ccc4604..93daca0e 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -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", diff --git a/frontend/src/routes/_account.index.lazy.tsx b/frontend/src/routes/_account.index.lazy.tsx new file mode 100644 index 00000000..02f24578 --- /dev/null +++ b/frontend/src/routes/_account.index.lazy.tsx @@ -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 => { + await navigate({ to: "/emails/$id/verify", params: { id } }); + }; + + return ( + <> + + {/* This wrapper is only needed for the anchor link */} +
+ {user.primaryEmail ? ( + + ) : ( + + )} + + }> + + + + {siteConfig.emailChangeAllowed && ( + + )} +
+ + + + {siteConfig.passwordLoginEnabled && ( + + )} + + + + + {t("frontend.reset_cross_signing.heading")} + +
+ + ); +} diff --git a/frontend/src/routes/_account.index.tsx b/frontend/src/routes/_account.index.tsx index f418acc8..68dd8e50 100644 --- a/frontend/src/routes/_account.index.tsx +++ b/frontend/src/routes/_account.index.tsx @@ -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 => { - await navigate({ to: "/emails/$id/verify", params: { id } }); - }; - - return ( - <> - - {/* This wrapper is only needed for the anchor link */} -
- {user.primaryEmail ? ( - - ) : ( - - )} - - }> - - - - {siteConfig.emailChangeAllowed && ( - - )} -
- - - - {siteConfig.passwordLoginEnabled && ( - - )} - - - - - {t("frontend.reset_cross_signing.heading")} - -
- - ); -} diff --git a/frontend/src/routes/_account.lazy.tsx b/frontend/src/routes/_account.lazy.tsx new file mode 100644 index 00000000..be7eea89 --- /dev/null +++ b/frontend/src/routes/_account.lazy.tsx @@ -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 ( + +
+
+ + {t("frontend.account.title")} + + + +
+ + + + + + + {t("frontend.nav.settings")} + {t("frontend.nav.devices")} + +
+ + +
+ ); +} diff --git a/frontend/src/routes/_account.sessions.$id.lazy.tsx b/frontend/src/routes/_account.sessions.$id.lazy.tsx new file mode 100644 index 00000000..65aecda3 --- /dev/null +++ b/frontend/src/routes/_account.sessions.$id.lazy.tsx @@ -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 ( + + {t("frontend.session_detail.alert.text")} + + {t("frontend.session_detail.alert.button")} + + + ); +} + +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 ; + case "Oauth2Session": + return ; + case "BrowserSession": + return ( + + ); + default: + throw new Error("Unknown session type"); + } +} diff --git a/frontend/src/routes/_account.sessions.$id.tsx b/frontend/src/routes/_account.sessions.$id.tsx index 4af468c5..ade4ec17 100644 --- a/frontend/src/routes/_account.sessions.$id.tsx +++ b/frontend/src/routes/_account.sessions.$id.tsx @@ -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 ( - - {t("frontend.session_detail.alert.text")} - - {t("frontend.session_detail.alert.button")} - - - ); -} - -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 ; - case "Oauth2Session": - return ; - case "BrowserSession": - return ( - - ); - 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(); + }, +}); diff --git a/frontend/src/routes/_account.sessions.browsers.lazy.tsx b/frontend/src/routes/_account.sessions.browsers.lazy.tsx new file mode 100644 index 00000000..a552db4d --- /dev/null +++ b/frontend/src/routes/_account.sessions.browsers.lazy.tsx @@ -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 ( + +
{t("frontend.browser_sessions_overview.heading")}
+ +
+ + {t("frontend.last_active.inactive_90_days")} + +
+ + {edges.map((n) => ( + + ))} + + {currentSession.user.browserSessions.totalCount === 0 && ( + + {inactive + ? t( + "frontend.browser_sessions_overview.no_active_sessions.inactive_90_days", + ) + : t( + "frontend.browser_sessions_overview.no_active_sessions.default", + )} + + )} + + {/* Only show the pagination buttons if there are pages to go to */} + {(forwardPage || backwardPage) && ( +
+ + {t("common.previous")} + + + {/* Spacer */} +
+ + + {t("common.next")} + +
+ )} + + ); +} diff --git a/frontend/src/routes/_account.sessions.browsers.tsx b/frontend/src/routes/_account.sessions.browsers.tsx index 27a3132d..bc870dd1 100644 --- a/frontend/src/routes/_account.sessions.browsers.tsx +++ b/frontend/src/routes/_account.sessions.browsers.tsx @@ -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: () =>
Hello /_account/sessions/browsers!
, }); - -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 ( - -
{t("frontend.browser_sessions_overview.heading")}
- -
- - {t("frontend.last_active.inactive_90_days")} - -
- - {edges.map((n) => ( - - ))} - - {currentSession.user.browserSessions.totalCount === 0 && ( - - {inactive - ? t( - "frontend.browser_sessions_overview.no_active_sessions.inactive_90_days", - ) - : t( - "frontend.browser_sessions_overview.no_active_sessions.default", - )} - - )} - - {/* Only show the pagination buttons if there are pages to go to */} - {(forwardPage || backwardPage) && ( -
- - {t("common.previous")} - - - {/* Spacer */} -
- - - {t("common.next")} - -
- )} - - ); -} diff --git a/frontend/src/routes/_account.sessions.index.lazy.tsx b/frontend/src/routes/_account.sessions.index.lazy.tsx new file mode 100644 index 00000000..95a52b6e --- /dev/null +++ b/frontend/src/routes/_account.sessions.index.lazy.tsx @@ -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 ( + +

{t("frontend.user_sessions_overview.heading")}

+ + +
+ + {t("frontend.last_active.inactive_90_days")} + +
+ {edges.map((session) => { + const type = session.node.__typename; + switch (type) { + case "Oauth2Session": + return ( + + ); + case "CompatSession": + return ( + + ); + default: + unknownSessionType(type); + } + })} + + {appSessions.totalCount === 0 && ( + + {inactive + ? t( + "frontend.user_sessions_overview.no_active_sessions.inactive_90_days", + ) + : t("frontend.user_sessions_overview.no_active_sessions.default")} + + )} + + {/* Only show the pagination buttons if there are pages to go to */} + {(forwardPage || backwardPage) && ( +
+ + {t("common.previous")} + + + {/* Spacer */} +
+ + + {t("common.next")} + +
+ )} + + ); +} diff --git a/frontend/src/routes/_account.sessions.index.tsx b/frontend/src/routes/_account.sessions.index.tsx index 6a85e8c7..6defa444 100644 --- a/frontend/src/routes/_account.sessions.index.tsx +++ b/frontend/src/routes/_account.sessions.index.tsx @@ -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 ( - -

{t("frontend.user_sessions_overview.heading")}

- - -
- - {t("frontend.last_active.inactive_90_days")} - -
- {edges.map((session) => { - const type = session.node.__typename; - switch (type) { - case "Oauth2Session": - return ( - - ); - case "CompatSession": - return ( - - ); - default: - unknownSessionType(type); - } - })} - - {appSessions.totalCount === 0 && ( - - {inactive - ? t( - "frontend.user_sessions_overview.no_active_sessions.inactive_90_days", - ) - : t("frontend.user_sessions_overview.no_active_sessions.default")} - - )} - - {/* Only show the pagination buttons if there are pages to go to */} - {(forwardPage || backwardPage) && ( -
- - {t("common.previous")} - - - {/* Spacer */} -
- - - {t("common.next")} - -
- )} - - ); -} diff --git a/frontend/src/routes/_account.tsx b/frontend/src/routes/_account.tsx index a48aef15..bb0719aa 100644 --- a/frontend/src/routes/_account.tsx +++ b/frontend/src/routes/_account.tsx @@ -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 ( - -
-
- - {t("frontend.account.title")} - - - -
- - - - - - - {t("frontend.nav.settings")} - {t("frontend.nav.devices")} - -
- - -
- ); -} diff --git a/frontend/src/routes/clients.$id.lazy.tsx b/frontend/src/routes/clients.$id.lazy.tsx new file mode 100644 index 00000000..a410942c --- /dev/null +++ b/frontend/src/routes/clients.$id.lazy.tsx @@ -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 ( + + + + ); +} diff --git a/frontend/src/routes/clients.$id.tsx b/frontend/src/routes/clients.$id.tsx index 5299c479..c99f5aec 100644 --- a/frontend/src/routes/clients.$id.tsx +++ b/frontend/src/routes/clients.$id.tsx @@ -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 ( - - - - ); -} diff --git a/frontend/src/routes/emails.$id.verify.lazy.tsx b/frontend/src/routes/emails.$id.verify.lazy.tsx new file mode 100644 index 00000000..37930075 --- /dev/null +++ b/frontend/src/routes/emails.$id.verify.lazy.tsx @@ -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 ( + + + + ); +} diff --git a/frontend/src/routes/emails.$id.verify.tsx b/frontend/src/routes/emails.$id.verify.tsx index ebabce6f..aee88b7b 100644 --- a/frontend/src/routes/emails.$id.verify.tsx +++ b/frontend/src/routes/emails.$id.verify.tsx @@ -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 ( - - - - ); -} diff --git a/frontend/src/routes/password.change.index.lazy.tsx b/frontend/src/routes/password.change.index.lazy.tsx index 90d8f93e..8d475d84 100644 --- a/frontend/src/routes/password.change.index.lazy.tsx +++ b/frontend/src/routes/password.change.index.lazy.tsx @@ -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( diff --git a/frontend/src/routes/password.change.index.tsx b/frontend/src/routes/password.change.index.tsx index 303efe89..34fb4a5f 100644 --- a/frontend/src/routes/password.change.index.tsx +++ b/frontend/src/routes/password.change.index.tsx @@ -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 diff --git a/frontend/src/routes/password.change.success.tsx b/frontend/src/routes/password.change.success.lazy.tsx similarity index 63% rename from frontend/src/routes/password.change.success.tsx rename to frontend/src/routes/password.change.success.lazy.tsx index 79935dcc..05325c83 100644 --- a/frontend/src/routes/password.change.success.tsx +++ b/frontend/src/routes/password.change.success.lazy.tsx @@ -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 ( diff --git a/frontend/src/routes/password.recovery.index.lazy.tsx b/frontend/src/routes/password.recovery.index.lazy.tsx index b4020466..78a055db 100644 --- a/frontend/src/routes/password.recovery.index.lazy.tsx +++ b/frontend/src/routes/password.recovery.index.lazy.tsx @@ -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!) { diff --git a/frontend/src/routes/password.recovery.index.tsx b/frontend/src/routes/password.recovery.index.tsx index 9ee5afd2..bc7bc2ee 100644 --- a/frontend/src/routes/password.recovery.index.tsx +++ b/frontend/src/routes/password.recovery.index.tsx @@ -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 diff --git a/frontend/src/routes/reset-cross-signing.lazy.tsx b/frontend/src/routes/reset-cross-signing.lazy.tsx new file mode 100644 index 00000000..eb04b123 --- /dev/null +++ b/frontend/src/routes/reset-cross-signing.lazy.tsx @@ -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 ( + + + + + {!result.data && !result.error && ( + <> + + {t("frontend.reset_cross_signing.description")} + + + + )} + {result.data && ( + + {t("frontend.reset_cross_signing.success.description")} + + )} + {result.error && ( + + {t("frontend.reset_cross_signing.failure.description")} + + )} + + {!deepLink && ( + + {t("action.back")} + + )} + + + ); +} diff --git a/frontend/src/routes/reset-cross-signing.tsx b/frontend/src/routes/reset-cross-signing.tsx index e5c6a04d..cd07b0dc 100644 --- a/frontend/src/routes/reset-cross-signing.tsx +++ b/frontend/src/routes/reset-cross-signing.tsx @@ -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 ( - - - - - {!result.data && !result.error && ( - <> - - {t("frontend.reset_cross_signing.description")} - - - - )} - {result.data && ( - - {t("frontend.reset_cross_signing.success.description")} - - )} - {result.error && ( - - {t("frontend.reset_cross_signing.failure.description")} - - )} - - {!deepLink && ( - - {t("action.back")} - - )} - - - ); -}