diff --git a/apps/ui/middleware.ts b/apps/ui/middleware.ts index 473f1aafd..79e37f703 100644 --- a/apps/ui/middleware.ts +++ b/apps/ui/middleware.ts @@ -9,11 +9,11 @@ import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; import { env } from "@/env.mjs"; import { getSignInUrl, buildGraphQLUrl } from "common/src/urlBuilder"; -import { type LocalizationLanguages } from "common/src/helpers"; +import { base64encode, type LocalizationLanguages } from "common/src/helpers"; const apiBaseUrl = env.TILAVARAUS_API_URL ?? ""; -type gqlQuery = { +type QqlQuery = { query: string; // TODO don't type to unknown (undefined and Date break JSON.stringify) variables: Record; @@ -24,7 +24,7 @@ type gqlQuery = { /// @param query - Query object with query and variables /// @returns Promise /// custom function so we don't have to import apollo client in middleware -async function gqlQueryFetch(req: NextRequest, query: gqlQuery) { +async function gqlQueryFetch(req: NextRequest, query: QqlQuery) { const { cookies, headers } = req; // TODO this is copy to the createApolloClient function but different header types // NextRequest vs. RequestInit @@ -68,10 +68,40 @@ async function gqlQueryFetch(req: NextRequest, query: gqlQuery) { }); } +type User = { + pk: number; + hasAccess: boolean; +}; + +const RESERVATION_QUERY = ` + reservation(id: $reservationId) { + id + user { + id + pk + } + }`; + +const APPLICATION_QUERY = ` + application(id: $applicationId) { + id + user { + id + pk + } + }`; + /// Get the current user from the backend /// @param req - NextRequest +/// @param opts - optional parameters for fetching additional data /// @returns Promise - user id or null if not logged in -async function getCurrentUser(req: NextRequest): Promise { +async function getCurrentUser( + req: NextRequest, + opts?: { + applicationPk?: number | null; + reservationPk?: number | null; + } +): Promise { const { cookies } = req; const hasSession = cookies.has("sessionid"); if (!hasSession) { @@ -85,14 +115,37 @@ async function getCurrentUser(req: NextRequest): Promise { return null; } - const query: gqlQuery = { + const applicationId = + opts?.applicationPk != null + ? base64encode(`ApplicationNode:${opts.applicationPk}`) + : null; + const reservationId = + opts?.reservationPk != null + ? base64encode(`ReservationNode:${opts.reservationPk}`) + : null; + + // NOTE: need to build queries dynamically because of the optional parameters + const params = + reservationId != null || applicationId != null + ? `( +${reservationId ? "$reservationId: ID!" : ""} +${applicationId ? "$applicationId: ID!" : ""} +)` + : ""; + + const query: QqlQuery = { query: ` - query GetCurrentUser { + query GetCurrentUser ${params} { currentUser { pk } + ${reservationId ? RESERVATION_QUERY : ""} + ${applicationId ? APPLICATION_QUERY : ""} }`, - variables: {}, + variables: { + ...(reservationId != null ? { reservationId } : {}), + ...(applicationId != null ? { applicationId } : {}), + }, }; const res = await gqlQueryFetch(req, query); @@ -109,22 +162,69 @@ async function getCurrentUser(req: NextRequest): Promise { console.warn("no data in response"); return null; } - if ( - typeof data.data === "object" && - data.data != null && - "currentUser" in data.data - ) { - const { currentUser } = data.data; + + return parseUserGQLquery(data.data, reservationId, applicationId); +} + +function parseUserGQLquery( + data: unknown, + reservationId: string | null, + applicationId: string | null +): User | null { + let userPk = null; + let hasAccess = reservationId == null && applicationId == null; + if (typeof data !== "object" || data == null) { + return null; + } + + if ("currentUser" in data) { + const { currentUser } = data; if ( typeof currentUser === "object" && currentUser != null && "pk" in currentUser ) { - if (typeof currentUser.pk === "number") { - return currentUser.pk; + userPk = typeof currentUser.pk === "number" ? currentUser.pk : null; + } + } + + if ("reservation" in data) { + const { reservation } = data; + if ( + reservation != null && + typeof reservation === "object" && + "user" in reservation && + reservation.user != null && + typeof reservation.user === "object" && + "pk" in reservation.user + ) { + const { pk } = reservation.user; + if (pk != null && typeof pk === "number") { + hasAccess = pk === userPk; } } } + + if ("application" in data) { + const { application } = data; + if ( + application != null && + typeof application === "object" && + "user" in application && + application.user != null && + typeof application.user === "object" && + "pk" in application.user + ) { + const { pk } = application.user; + if (pk != null && typeof pk === "number") { + hasAccess = pk === userPk; + } + } + } + + if (userPk != null) { + return { pk: userPk, hasAccess }; + } return null; } @@ -149,7 +249,7 @@ function getLocalizationFromUrl(url: URL): LocalizationLanguages { /// NOTE The responsibility to update the cookie is on the caller (who creates the next request). async function maybeSaveUserLanguage( req: NextRequest, - user: number | null + user: User | null ): Promise { const { cookies } = req; const url = new URL(req.url); @@ -167,7 +267,7 @@ async function maybeSaveUserLanguage( return; } - const query: gqlQuery = { + const query: QqlQuery = { query: ` mutation SaveUserLanguage($preferredLanguage: PreferredLanguage!) { updateCurrentUser( @@ -198,7 +298,7 @@ async function maybeSaveUserLanguage( /// @returns Promise - the redirect url or null if no redirect is needed function getRedirectProtectedRoute( req: NextRequest, - user: number | null + user: User | null ): string | null { const { headers } = req; @@ -267,11 +367,18 @@ function redirectCsrfToken(req: NextRequest): URL | undefined { // refactor the matcher or fix the underlining matcher issue in nextjs // matcher syntax: /hard-path/:path* -> /hard-path/anything // our syntax: hard-path -const authenticatedRoutes = [ +const reservationRoutes = [ "reservation", //:path*', "reservations", //:path*', +]; +const applicationRoutes = [ "applications", //:path*', "application", //:path*', +]; +const authenticatedRoutes = [ + // just in case if the route falls through + ...reservationRoutes, + ...applicationRoutes, "success", ]; // url matcher that is very specific to our case @@ -297,7 +404,42 @@ export async function middleware(req: NextRequest) { return NextResponse.next(); } - const user = await getCurrentUser(req); + const url = new URL(req.url); + let reservationPk: number | null = null; + let applicationPk: number | null = null; + + if (reservationRoutes.some((route) => doesUrlMatch(req.url, route))) { + const id = url.pathname.match(/\/reservations?\/(\d+)/)?.[1]; + const pk = Number(id); + // can be either an url issues (user error) or a bug in our matcher + if (pk > 0) { + reservationPk = pk; + } else { + // eslint-disable-next-line no-console + console.error("Invalid reservation id"); + } + } + if (applicationRoutes.some((route) => doesUrlMatch(req.url, route))) { + const id = url.pathname.match(/\/applications?\/(\d+)/)?.[1]; + const pk = Number(id); + // can be either an url issues (user error) or a bug in our matcher + if (pk > 0) { + applicationPk = pk; + } else { + // eslint-disable-next-line no-console + console.error("Invalid application id"); + } + } + + const options = { + applicationPk, + reservationPk, + }; + + const user = await getCurrentUser(req, options); + if (user != null && !user.hasAccess) { + return NextResponse.error(); + } if (authenticatedRoutes.some((route) => doesUrlMatch(req.url, route))) { const redirect = getRedirectProtectedRoute(req, user); diff --git a/apps/ui/pages/applications/index.tsx b/apps/ui/pages/applications/index.tsx index 645823d00..05995c867 100644 --- a/apps/ui/pages/applications/index.tsx +++ b/apps/ui/pages/applications/index.tsx @@ -38,6 +38,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { const commonProps = getCommonServerSideProps(); const client = createApolloClient(commonProps.apiBaseUrl, ctx); + // NOTE have to be done with double query because applications returns everything the user has access to (not what he owns) const { data: userData } = await client.query({ query: CurrentUserDocument, }); diff --git a/apps/ui/pages/reservations/[id]/cancel.tsx b/apps/ui/pages/reservations/[id]/cancel.tsx index 81d5ba3d8..b868d30a7 100644 --- a/apps/ui/pages/reservations/[id]/cancel.tsx +++ b/apps/ui/pages/reservations/[id]/cancel.tsx @@ -2,7 +2,6 @@ import React from "react"; import type { GetServerSidePropsContext } from "next"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { - CurrentUserQuery, ReservationCancelReasonsDocument, type ReservationCancelReasonsQuery, type ReservationCancelReasonsQueryVariables, @@ -15,7 +14,6 @@ import { getCommonServerSideProps } from "@/modules/serverUtils"; import { createApolloClient } from "@/modules/apolloClient"; import { base64encode, filterNonNullable } from "common/src/helpers"; import { isReservationCancellable } from "@/modules/reservation"; -import { CURRENT_USER } from "@/modules/queries/user"; import { getReservationPath } from "@/modules/urls"; type PropsNarrowed = Exclude; @@ -47,12 +45,6 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { }); const { reservation } = reservationData || {}; - const { data: userData } = await client.query({ - query: CURRENT_USER, - fetchPolicy: "no-cache", - }); - const user = userData?.currentUser; - const { data: cancelReasonsData } = await client.query< ReservationCancelReasonsQuery, ReservationCancelReasonsQueryVariables @@ -67,16 +59,6 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { ) ); - if (reservation?.user?.pk !== user?.pk) { - return { - notFound: true, - props: { - notFound: true, - ...commonProps, - ...(await serverSideTranslations(locale ?? "fi")), - }, - }; - } const canCancel = reservation != null && isReservationCancellable(reservation); if (canCancel) { diff --git a/apps/ui/pages/reservations/[id]/confirmation.tsx b/apps/ui/pages/reservations/[id]/confirmation.tsx index a5ddf63b6..696b7e305 100644 --- a/apps/ui/pages/reservations/[id]/confirmation.tsx +++ b/apps/ui/pages/reservations/[id]/confirmation.tsx @@ -1,6 +1,5 @@ import React from "react"; import { - CurrentUserQuery, ReservationDocument, type ReservationQuery, type ReservationQueryVariables, @@ -18,7 +17,6 @@ import { getCommonServerSideProps } from "@/modules/serverUtils"; import { base64encode } from "common/src/helpers"; import { CenterSpinner } from "@/components/common/common"; import Error from "next/error"; -import { CURRENT_USER } from "@/modules/queries/user"; import { createApolloClient } from "@/modules/apolloClient"; // TODO styles are copies from [...params].tsx @@ -118,13 +116,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { }); const { reservation } = reservationData || {}; - const { data: userData } = await client.query({ - query: CURRENT_USER, - fetchPolicy: "no-cache", - }); - const user = userData?.currentUser; - - if (user != null && user.pk === reservation?.user?.pk) { + if (reservation) { return { props: { ...getCommonServerSideProps(), diff --git a/apps/ui/pages/reservations/[id]/edit.tsx b/apps/ui/pages/reservations/[id]/edit.tsx index 97c5830c4..8091b1fdc 100644 --- a/apps/ui/pages/reservations/[id]/edit.tsx +++ b/apps/ui/pages/reservations/[id]/edit.tsx @@ -12,7 +12,6 @@ import { ReservationDocument, type ReservationQuery, type ReservationQueryVariables, - type CurrentUserQuery, type Mutation, type MutationAdjustReservationTimeArgs, useAdjustReservationTimeMutation, @@ -21,7 +20,6 @@ import { base64encode, filterNonNullable } from "common/src/helpers"; import { toApiDate } from "common/src/common/util"; import { addYears } from "date-fns"; import { RELATED_RESERVATION_STATES } from "common/src/const"; -import { CURRENT_USER } from "@/modules/queries/user"; import { type FetchResult } from "@apollo/client"; import { useRouter } from "next/router"; import { StepState, Stepper } from "hds-react"; @@ -289,12 +287,6 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { }); const { reservationUnit } = reservationUnitData; - const { data: userData } = await client.query({ - query: CURRENT_USER, - fetchPolicy: "no-cache", - }); - const user = userData?.currentUser; - const options = await queryOptions(client, locale ?? ""); const timespans = filterNonNullable(reservationUnit?.reservableTimeSpans); @@ -302,11 +294,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { reservationUnitData?.affectingReservations ); - if ( - reservation != null && - reservationUnit != null && - reservation.user?.pk === user?.pk - ) { + if (reservation != null && reservationUnit != null) { return { props: { ...commonProps, diff --git a/apps/ui/pages/reservations/[id]/index.tsx b/apps/ui/pages/reservations/[id]/index.tsx index 47cdd4eb8..135e664d1 100644 --- a/apps/ui/pages/reservations/[id]/index.tsx +++ b/apps/ui/pages/reservations/[id]/index.tsx @@ -21,8 +21,6 @@ import { ReservationDocument, type ReservationQuery, type ReservationQueryVariables, - CurrentUserDocument, - type CurrentUserQuery, OrderStatus, } from "@gql/gql-types"; import Link from "next/link"; @@ -647,7 +645,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { // NOTE errors will fallback to 404 const id = base64encode(`ReservationNode:${pk}`); - const { data, error } = await apolloClient.query< + const { data } = await apolloClient.query< ReservationQuery, ReservationQueryVariables >({ @@ -656,24 +654,8 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { variables: { id }, }); - const { data: userData } = await apolloClient.query({ - query: CurrentUserDocument, - fetchPolicy: "no-cache", - }); - const user = userData?.currentUser; - - if (error) { - // eslint-disable-next-line no-console - console.error("Error while fetching reservation", error); - } - const { reservation } = data ?? {}; - // Return 404 for unauthorized access - if ( - reservation != null && - user != null && - reservation.user?.pk === user.pk - ) { + if (reservation != null) { return { props: { ...commonProps,