diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c3d235d14..fba0d4cdf 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,6 +13,7 @@ "@radix-ui/react-collapsible": "1.0.3", "@radix-ui/react-dialog": "^1.0.5", "@tanstack/react-router": "^1.58.15", + "@tanstack/router-zod-adapter": "^1.58.15", "@urql/core": "^5.0.6", "@urql/devtools": "^2.0.3", "@urql/exchange-graphcache": "^7.1.3", @@ -8067,6 +8068,23 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/router-zod-adapter": { + "version": "1.58.15", + "resolved": "https://registry.npmjs.org/@tanstack/router-zod-adapter/-/router-zod-adapter-1.58.15.tgz", + "integrity": "sha512-e8sgEWbipqBmUQPB7hII6Plneatt53+O/nWJb7OeRErjNZj1+wvsJZTm+kNKFnzXdqpfUUEHUi6fC6j1v7obYQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-router": ">=1.43.2", + "zod": ">=3" + } + }, "node_modules/@tanstack/store": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.5.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index 398bbba1a..e83839c56 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,6 +22,7 @@ "@radix-ui/react-collapsible": "1.0.3", "@radix-ui/react-dialog": "^1.0.5", "@tanstack/react-router": "^1.58.15", + "@tanstack/router-zod-adapter": "^1.58.15", "@urql/core": "^5.0.6", "@urql/devtools": "^2.0.3", "@urql/exchange-graphcache": "^7.1.3", diff --git a/frontend/src/components/UserProfile/UserEmailList.tsx b/frontend/src/components/UserProfile/UserEmailList.tsx index ce0419e26..7b358ed2a 100644 --- a/frontend/src/components/UserProfile/UserEmailList.tsx +++ b/frontend/src/components/UserProfile/UserEmailList.tsx @@ -7,10 +7,10 @@ import { useTransition } from "react"; import { useQuery } from "urql"; -import { FragmentType, graphql, useFragment } from "../../gql"; +import { type FragmentType, graphql, useFragment } from "../../gql"; import { FIRST_PAGE, - Pagination, + type Pagination, usePages, usePagination, } from "../../pagination"; diff --git a/frontend/src/components/UserSessionsOverview/BrowserSessionsOverview.tsx b/frontend/src/components/UserSessionsOverview/BrowserSessionsOverview.tsx index 16c3e8e54..3336de0d8 100644 --- a/frontend/src/components/UserSessionsOverview/BrowserSessionsOverview.tsx +++ b/frontend/src/components/UserSessionsOverview/BrowserSessionsOverview.tsx @@ -38,7 +38,7 @@ const BrowserSessionsOverview: React.FC<{ })} - + {t("frontend.browser_sessions_overview.view_all_button")} diff --git a/frontend/src/components/UserSessionsOverview/__snapshots__/BrowserSessionsOverview.test.tsx.snap b/frontend/src/components/UserSessionsOverview/__snapshots__/BrowserSessionsOverview.test.tsx.snap index f3f93036c..a62d63e40 100644 --- a/frontend/src/components/UserSessionsOverview/__snapshots__/BrowserSessionsOverview.test.tsx.snap +++ b/frontend/src/components/UserSessionsOverview/__snapshots__/BrowserSessionsOverview.test.tsx.snap @@ -22,7 +22,7 @@ exports[`BrowserSessionsOverview > renders with no browser sessions 1`] = ` View all @@ -53,7 +53,7 @@ exports[`BrowserSessionsOverview > renders with sessions 1`] = ` View all diff --git a/frontend/src/pagination.ts b/frontend/src/pagination.ts index 924a77497..e08f71ec7 100644 --- a/frontend/src/pagination.ts +++ b/frontend/src/pagination.ts @@ -7,29 +7,43 @@ import { useState } from "react"; import * as z from "zod"; -import { PageInfo } from "./gql/graphql"; +import type { PageInfo } from "./gql/graphql"; export const FIRST_PAGE = Symbol("FIRST_PAGE"); export const LAST_PAGE = Symbol("LAST_PAGE"); +export const anyPaginationSchema = z.object({ + first: z.number().optional(), + after: z.string().optional(), + last: z.number().optional(), + before: z.string().optional(), +}); + export const forwardPaginationSchema = z.object({ first: z.number(), after: z.string().optional(), }); -export const backwardPaginationSchema = z.object({ +const backwardPaginationSchema = z.object({ last: z.number(), before: z.string().optional(), }); -export const paginationSchema = z.union([ +const paginationSchema = z.union([ forwardPaginationSchema, backwardPaginationSchema, ]); -export type ForwardPagination = z.infer; -export type BackwardPagination = z.infer; +type ForwardPagination = z.infer; +type BackwardPagination = z.infer; export type Pagination = z.infer; +export type AnyPagination = z.infer; + +// Check if the pagination is a valid pagination +export const isValidPagination = ( + pagination: AnyPagination, +): pagination is Pagination => + typeof pagination.first === "number" || typeof pagination.last === "number"; // Check if the pagination is forward pagination. export const isForwardPagination = ( @@ -47,26 +61,40 @@ export const isBackwardPagination = ( type Action = typeof FIRST_PAGE | typeof LAST_PAGE | Pagination; +// Normalize pagination parameters to a valid pagination object +export const normalizePagination = ( + pagination: AnyPagination, + pageSize = 6, + type: "forward" | "backward" = "forward", +): Pagination => { + if (isValidPagination(pagination)) { + return pagination; + } + + if (type === "forward") { + return { first: pageSize } satisfies ForwardPagination; + } + + return { last: pageSize } satisfies BackwardPagination; +}; + // Hook to handle pagination state. export const usePagination = ( pageSize = 6, ): [Pagination, (action: Action) => void] => { const [pagination, setPagination] = useState({ first: pageSize, - after: undefined, }); const handlePagination = (action: Action): void => { if (action === FIRST_PAGE) { setPagination({ first: pageSize, - after: undefined, - }); + } satisfies ForwardPagination); } else if (action === LAST_PAGE) { setPagination({ last: pageSize, - before: undefined, - }); + } satisfies BackwardPagination); } else { setPagination(action); } @@ -78,7 +106,7 @@ export const usePagination = ( // Compute the next backward and forward pagination parameters based on the current pagination and the page info. export const usePages = ( currentPagination: Pagination, - pageInfo: PageInfo | null, + pageInfo: PageInfo, pageSize = 6, ): [BackwardPagination | null, ForwardPagination | null] => { const hasProbablyPreviousPage = @@ -90,17 +118,17 @@ export const usePages = ( let previousPagination: BackwardPagination | null = null; let nextPagination: ForwardPagination | null = null; - if (pageInfo?.hasPreviousPage || hasProbablyPreviousPage) { + if (pageInfo.hasPreviousPage || hasProbablyPreviousPage) { previousPagination = { last: pageSize, - before: pageInfo?.startCursor ?? undefined, + before: pageInfo.startCursor ?? undefined, }; } - if (pageInfo?.hasNextPage || hasProbablyNextPage) { + if (pageInfo.hasNextPage || hasProbablyNextPage) { nextPagination = { first: pageSize, - after: pageInfo?.endCursor ?? undefined, + after: pageInfo.endCursor ?? undefined, }; } diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 93daca0e4..d96801a8f 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -204,21 +204,138 @@ declare module '@tanstack/react-router' { // Create and export the route tree -export const routeTree = rootRoute.addChildren({ - AccountRoute: AccountRoute.addChildren({ - AccountIndexRoute, - AccountSessionsIdRoute, - AccountSessionsBrowsersRoute, - AccountSessionsIndexRoute, - }), - ResetCrossSigningRoute, - ClientsIdRoute, - DevicesSplatRoute, - EmailsIdVerifyRoute, - PasswordChangeSuccessLazyRoute, - PasswordChangeIndexRoute, - PasswordRecoveryIndexRoute, -}) +interface AccountRouteChildren { + AccountIndexRoute: typeof AccountIndexRoute + AccountSessionsIdRoute: typeof AccountSessionsIdRoute + AccountSessionsBrowsersRoute: typeof AccountSessionsBrowsersRoute + AccountSessionsIndexRoute: typeof AccountSessionsIndexRoute +} + +const AccountRouteChildren: AccountRouteChildren = { + AccountIndexRoute: AccountIndexRoute, + AccountSessionsIdRoute: AccountSessionsIdRoute, + AccountSessionsBrowsersRoute: AccountSessionsBrowsersRoute, + AccountSessionsIndexRoute: AccountSessionsIndexRoute, +} + +const AccountRouteWithChildren = + AccountRoute._addFileChildren(AccountRouteChildren) + +export interface FileRoutesByFullPath { + '': typeof AccountRouteWithChildren + '/reset-cross-signing': typeof ResetCrossSigningRoute + '/clients/$id': typeof ClientsIdRoute + '/devices/$': typeof DevicesSplatRoute + '/': typeof AccountIndexRoute + '/sessions/$id': typeof AccountSessionsIdRoute + '/sessions/browsers': typeof AccountSessionsBrowsersRoute + '/emails/$id/verify': typeof EmailsIdVerifyRoute + '/password/change/success': typeof PasswordChangeSuccessLazyRoute + '/sessions': typeof AccountSessionsIndexRoute + '/password/change': typeof PasswordChangeIndexRoute + '/password/recovery': typeof PasswordRecoveryIndexRoute +} + +export interface FileRoutesByTo { + '/reset-cross-signing': typeof ResetCrossSigningRoute + '/clients/$id': typeof ClientsIdRoute + '/devices/$': typeof DevicesSplatRoute + '/': typeof AccountIndexRoute + '/sessions/$id': typeof AccountSessionsIdRoute + '/sessions/browsers': typeof AccountSessionsBrowsersRoute + '/emails/$id/verify': typeof EmailsIdVerifyRoute + '/password/change/success': typeof PasswordChangeSuccessLazyRoute + '/sessions': typeof AccountSessionsIndexRoute + '/password/change': typeof PasswordChangeIndexRoute + '/password/recovery': typeof PasswordRecoveryIndexRoute +} + +export interface FileRoutesById { + __root__: typeof rootRoute + '/_account': typeof AccountRouteWithChildren + '/reset-cross-signing': typeof ResetCrossSigningRoute + '/clients/$id': typeof ClientsIdRoute + '/devices/$': typeof DevicesSplatRoute + '/_account/': typeof AccountIndexRoute + '/_account/sessions/$id': typeof AccountSessionsIdRoute + '/_account/sessions/browsers': typeof AccountSessionsBrowsersRoute + '/emails/$id/verify': typeof EmailsIdVerifyRoute + '/password/change/success': typeof PasswordChangeSuccessLazyRoute + '/_account/sessions/': typeof AccountSessionsIndexRoute + '/password/change/': typeof PasswordChangeIndexRoute + '/password/recovery/': typeof PasswordRecoveryIndexRoute +} + +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '' + | '/reset-cross-signing' + | '/clients/$id' + | '/devices/$' + | '/' + | '/sessions/$id' + | '/sessions/browsers' + | '/emails/$id/verify' + | '/password/change/success' + | '/sessions' + | '/password/change' + | '/password/recovery' + fileRoutesByTo: FileRoutesByTo + to: + | '/reset-cross-signing' + | '/clients/$id' + | '/devices/$' + | '/' + | '/sessions/$id' + | '/sessions/browsers' + | '/emails/$id/verify' + | '/password/change/success' + | '/sessions' + | '/password/change' + | '/password/recovery' + id: + | '__root__' + | '/_account' + | '/reset-cross-signing' + | '/clients/$id' + | '/devices/$' + | '/_account/' + | '/_account/sessions/$id' + | '/_account/sessions/browsers' + | '/emails/$id/verify' + | '/password/change/success' + | '/_account/sessions/' + | '/password/change/' + | '/password/recovery/' + fileRoutesById: FileRoutesById +} + +export interface RootRouteChildren { + AccountRoute: typeof AccountRouteWithChildren + ResetCrossSigningRoute: typeof ResetCrossSigningRoute + ClientsIdRoute: typeof ClientsIdRoute + DevicesSplatRoute: typeof DevicesSplatRoute + EmailsIdVerifyRoute: typeof EmailsIdVerifyRoute + PasswordChangeSuccessLazyRoute: typeof PasswordChangeSuccessLazyRoute + PasswordChangeIndexRoute: typeof PasswordChangeIndexRoute + PasswordRecoveryIndexRoute: typeof PasswordRecoveryIndexRoute +} + +const rootRouteChildren: RootRouteChildren = { + AccountRoute: AccountRouteWithChildren, + ResetCrossSigningRoute: ResetCrossSigningRoute, + ClientsIdRoute: ClientsIdRoute, + DevicesSplatRoute: DevicesSplatRoute, + EmailsIdVerifyRoute: EmailsIdVerifyRoute, + PasswordChangeSuccessLazyRoute: PasswordChangeSuccessLazyRoute, + PasswordChangeIndexRoute: PasswordChangeIndexRoute, + PasswordRecoveryIndexRoute: PasswordRecoveryIndexRoute, +} + +export const routeTree = rootRoute + ._addFileChildren(rootRouteChildren) + ._addFileTypes() /* prettier-ignore-end */ diff --git a/frontend/src/routes/_account.index.tsx b/frontend/src/routes/_account.index.tsx index e979994d2..fd4ce9b08 100644 --- a/frontend/src/routes/_account.index.tsx +++ b/frontend/src/routes/_account.index.tsx @@ -5,6 +5,7 @@ // Please see LICENSE in the repository root for full details. import { createFileRoute, notFound, redirect } from "@tanstack/react-router"; +import { zodSearchValidator } from "@tanstack/router-zod-adapter"; import * as z from "zod"; import { graphql } from "../gql"; @@ -36,9 +37,6 @@ export const QUERY = graphql(/* GraphQL */ ` } `); -// XXX: we probably shouldn't have to specify the search parameters on /sessions/ -const PAGE_SIZE = 6; - const actionSchema = z .discriminatedUnion("action", [ z.object({ @@ -65,7 +63,7 @@ const actionSchema = z .catch({ action: undefined }); export const Route = createFileRoute("/_account/")({ - validateSearch: actionSchema, + validateSearch: zodSearchValidator(actionSchema), beforeLoad({ search }) { switch (search.action) { @@ -75,7 +73,7 @@ export const Route = createFileRoute("/_account/")({ case "sessions_list": case "org.matrix.sessions_list": - throw redirect({ to: "/sessions", search: { last: PAGE_SIZE } }); + throw redirect({ to: "/sessions" }); case "session_view": case "org.matrix.session_view": @@ -84,7 +82,7 @@ export const Route = createFileRoute("/_account/")({ to: "/devices/$", params: { _splat: search.device_id }, }); - throw redirect({ to: "/sessions", search: { last: PAGE_SIZE } }); + throw redirect({ to: "/sessions" }); case "session_end": case "org.matrix.session_end": @@ -93,7 +91,7 @@ export const Route = createFileRoute("/_account/")({ to: "/devices/$", params: { _splat: search.device_id }, }); - throw redirect({ to: "/sessions", search: { last: PAGE_SIZE } }); + throw redirect({ to: "/sessions" }); case "org.matrix.cross_signing_reset": throw redirect({ diff --git a/frontend/src/routes/_account.sessions.$id.lazy.tsx b/frontend/src/routes/_account.sessions.$id.lazy.tsx index 2796c6cc4..36ed5ba49 100644 --- a/frontend/src/routes/_account.sessions.$id.lazy.tsx +++ b/frontend/src/routes/_account.sessions.$id.lazy.tsx @@ -31,9 +31,7 @@ function NotFound(): React.ReactElement { title={t("frontend.session_detail.alert.title", { deviceId: id })} > {t("frontend.session_detail.alert.text")} - - {t("frontend.session_detail.alert.button")} - + {t("frontend.session_detail.alert.button")} ); } diff --git a/frontend/src/routes/_account.sessions.browsers.lazy.tsx b/frontend/src/routes/_account.sessions.browsers.lazy.tsx index c9732afb5..7dece96b4 100644 --- a/frontend/src/routes/_account.sessions.browsers.lazy.tsx +++ b/frontend/src/routes/_account.sessions.browsers.lazy.tsx @@ -14,13 +14,12 @@ 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 { usePages } from "../pagination"; import { getNinetyDaysAgo } from "../utils/dates"; import { QUERY } from "./_account.sessions.browsers"; const PAGE_SIZE = 6; -const DEFAULT_PAGE: BackwardPagination = { last: PAGE_SIZE }; export const Route = createLazyFileRoute("/_account/sessions/browsers")({ component: BrowserSessions, @@ -28,7 +27,7 @@ export const Route = createLazyFileRoute("/_account/sessions/browsers")({ function BrowserSessions(): React.ReactElement { const { t } = useTranslation(); - const { inactive, ...pagination } = Route.useLoaderDeps(); + const { inactive, pagination } = Route.useLoaderDeps(); const variables = { lastActive: inactive ? { before: getNinetyDaysAgo() } : undefined, @@ -59,7 +58,7 @@ function BrowserSessions(): React.ReactElement { {t("frontend.last_active.inactive_90_days")} diff --git a/frontend/src/routes/_account.sessions.browsers.tsx b/frontend/src/routes/_account.sessions.browsers.tsx index 670af9d2f..c74dc237e 100644 --- a/frontend/src/routes/_account.sessions.browsers.tsx +++ b/frontend/src/routes/_account.sessions.browsers.tsx @@ -5,18 +5,14 @@ // Please see LICENSE in the repository root for full details. import { createFileRoute, notFound } from "@tanstack/react-router"; +import { zodSearchValidator } from "@tanstack/router-zod-adapter"; import * as z from "zod"; import { graphql } from "../gql"; -import { - type Pagination, - type BackwardPagination, - paginationSchema, -} from "../pagination"; +import { anyPaginationSchema, normalizePagination } from "../pagination"; import { getNinetyDaysAgo } from "../utils/dates"; const PAGE_SIZE = 6; -const DEFAULT_PAGE: BackwardPagination = { last: PAGE_SIZE }; export const QUERY = graphql(/* GraphQL */ ` query BrowserSessionList( @@ -65,22 +61,23 @@ export const QUERY = graphql(/* GraphQL */ ` } `); -const searchSchema = z.object({ - inactive: z.literal(true).optional().catch(undefined), -}); - -type Search = z.infer; +const searchSchema = z + .object({ + inactive: z.literal(true).optional(), + }) + .and(anyPaginationSchema); export const Route = createFileRoute("/_account/sessions/browsers")({ - // We paginate backwards, so we need to validate the `last` parameter by default - validateSearch: paginationSchema.catch(DEFAULT_PAGE).and(searchSchema), + validateSearch: zodSearchValidator(searchSchema), - loaderDeps: ({ search }): Pagination & Search => - paginationSchema.and(searchSchema).parse(search), + loaderDeps: ({ search: { inactive, ...pagination } }) => ({ + inactive, + pagination: normalizePagination(pagination, PAGE_SIZE, "backward"), + }), async loader({ context, - deps: { inactive, ...pagination }, + deps: { inactive, pagination }, abortController: { signal }, }) { const variables = { @@ -95,6 +92,4 @@ export const Route = createFileRoute("/_account/sessions/browsers")({ if (result.data?.viewerSession?.__typename !== "BrowserSession") throw notFound(); }, - - component: () =>
Hello /_account/sessions/browsers!
, }); diff --git a/frontend/src/routes/_account.sessions.index.lazy.tsx b/frontend/src/routes/_account.sessions.index.lazy.tsx index 7f72aa377..19e37a028 100644 --- a/frontend/src/routes/_account.sessions.index.lazy.tsx +++ b/frontend/src/routes/_account.sessions.index.lazy.tsx @@ -16,13 +16,12 @@ 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 { usePages } from "../pagination"; import { getNinetyDaysAgo } from "../utils/dates"; 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 => { @@ -35,7 +34,7 @@ export const Route = createLazyFileRoute("/_account/sessions/")({ function Sessions(): React.ReactElement { const { t } = useTranslation(); - const { inactive, ...pagination } = Route.useLoaderDeps(); + const { inactive, pagination } = Route.useLoaderDeps(); const [overview] = useQuery({ query: QUERY }); if (overview.error) throw overview.error; const user = @@ -73,7 +72,7 @@ function Sessions(): React.ReactElement { {t("frontend.last_active.inactive_90_days")} diff --git a/frontend/src/routes/_account.sessions.index.tsx b/frontend/src/routes/_account.sessions.index.tsx index 79bf8e820..b3f76f817 100644 --- a/frontend/src/routes/_account.sessions.index.tsx +++ b/frontend/src/routes/_account.sessions.index.tsx @@ -5,18 +5,14 @@ // Please see LICENSE in the repository root for full details. import { createFileRoute, notFound } from "@tanstack/react-router"; +import { zodSearchValidator } from "@tanstack/router-zod-adapter"; import * as z from "zod"; import { graphql } from "../gql"; -import { - type BackwardPagination, - type Pagination, - paginationSchema, -} from "../pagination"; +import { anyPaginationSchema, normalizePagination } from "../pagination"; import { getNinetyDaysAgo } from "../utils/dates"; const PAGE_SIZE = 6; -const DEFAULT_PAGE: BackwardPagination = { last: PAGE_SIZE }; export const QUERY = graphql(/* GraphQL */ ` query SessionsOverviewQuery { @@ -74,22 +70,23 @@ export const LIST_QUERY = graphql(/* GraphQL */ ` } `); -const searchSchema = z.object({ - inactive: z.literal(true).optional().catch(undefined), -}); - -type Search = z.infer; +const searchSchema = z + .object({ + inactive: z.literal(true).optional(), + }) + .and(anyPaginationSchema); export const Route = createFileRoute("/_account/sessions/")({ - // We paginate backwards, so we need to validate the `last` parameter by default - validateSearch: paginationSchema.catch(DEFAULT_PAGE).and(searchSchema), + validateSearch: zodSearchValidator(searchSchema), - loaderDeps: ({ search }): Pagination & Search => - paginationSchema.and(searchSchema).parse(search), + loaderDeps: ({ search: { inactive, ...pagination } }) => ({ + inactive, + pagination: normalizePagination(pagination, PAGE_SIZE, "backward"), + }), async loader({ context, - deps: { inactive, ...pagination }, + deps: { inactive, pagination }, abortController: { signal }, }) { const variables = { diff --git a/frontend/src/routes/devices.$.tsx b/frontend/src/routes/devices.$.tsx index f8ec3abd7..693a5639b 100644 --- a/frontend/src/routes/devices.$.tsx +++ b/frontend/src/routes/devices.$.tsx @@ -78,9 +78,7 @@ function NotFound(): React.ReactElement { title={t("frontend.session_detail.alert.title", { deviceId })} > {t("frontend.session_detail.alert.text")} - - {t("frontend.session_detail.alert.button")} - + {t("frontend.session_detail.alert.button")} ); diff --git a/frontend/src/routes/password.recovery.index.tsx b/frontend/src/routes/password.recovery.index.tsx index 9ea479075..dd72a3a69 100644 --- a/frontend/src/routes/password.recovery.index.tsx +++ b/frontend/src/routes/password.recovery.index.tsx @@ -5,6 +5,8 @@ // Please see LICENSE in the repository root for full details. import { createFileRoute } from "@tanstack/react-router"; +import { zodSearchValidator } from "@tanstack/router-zod-adapter"; +import * as z from "zod"; import { graphql } from "../gql"; @@ -17,11 +19,13 @@ export const QUERY = graphql(/* GraphQL */ ` } `); +const schema = z.object({ + ticket: z.string(), +}); + export const Route = createFileRoute("/password/recovery/")({ - validateSearch: (search) => - search as { - ticket: string; - }, + validateSearch: zodSearchValidator(schema), + async loader({ context, abortController: { signal } }) { const queryResult = await context.client.query( QUERY, diff --git a/frontend/src/routes/reset-cross-signing.tsx b/frontend/src/routes/reset-cross-signing.tsx index 78d624ee1..9bd192822 100644 --- a/frontend/src/routes/reset-cross-signing.tsx +++ b/frontend/src/routes/reset-cross-signing.tsx @@ -5,6 +5,7 @@ // Please see LICENSE in the repository root for full details. import { notFound, createFileRoute } from "@tanstack/react-router"; +import { zodSearchValidator } from "@tanstack/router-zod-adapter"; import * as z from "zod"; import { graphql } from "../gql"; @@ -35,5 +36,5 @@ export const Route = createFileRoute("/reset-cross-signing")({ if (viewer.data?.viewer.__typename !== "User") throw notFound(); }, - validateSearch: searchSchema, + validateSearch: zodSearchValidator(searchSchema), });