From 1501e8bbd52611ba68b85f72b7100bbb19cb8948 Mon Sep 17 00:00:00 2001 From: Joonatan Kuosa Date: Wed, 24 Apr 2024 16:52:15 +0300 Subject: [PATCH] add: permission checks to application-round pages --- apps/admin-ui/src/hooks/usePermission.ts | 20 ++++++ apps/admin-ui/src/modules/permissionHelper.ts | 1 + .../[id]/allocation/index.tsx | 27 ++++++-- .../application-rounds/[id]/index.tsx | 62 ++++++++++++------- .../application-rounds/queries.tsx | 25 +++----- 5 files changed, 93 insertions(+), 42 deletions(-) diff --git a/apps/admin-ui/src/hooks/usePermission.ts b/apps/admin-ui/src/hooks/usePermission.ts index 0a1cbc43e8..1ea91027e8 100644 --- a/apps/admin-ui/src/hooks/usePermission.ts +++ b/apps/admin-ui/src/hooks/usePermission.ts @@ -3,6 +3,7 @@ import type { UnitNode, ReservationNode, UserNode, + ApplicationRoundNode, } from "common/types/gql-types"; import { hasPermission as baseHasPermission, @@ -83,6 +84,24 @@ const usePermission = () => { return baseHasAnyPermission(user); }; + // TODO restrict the Permission type to only those that are applicable to application rounds + const hasApplicationRoundPermission = ( + applicationRound: ApplicationRoundNode, + permission: Permission + ) => { + if (!user) return false; + const units = filterNonNullable( + applicationRound.reservationUnits.flatMap((ru) => ru.unit) + ); + for (const unit of units) { + if (hasUnitPermission(user, permission, unit)) { + return true; + } + } + return false; + }; + + // TODO this is becoming convoluted with the addition of a new function for each object type return { user, hasPermission: ( @@ -94,6 +113,7 @@ const usePermission = () => { hasAnyPermission, hasUnitPermission: (permission: Permission, unit: UnitNode) => hasUnitPermission(user, permission, unit), + hasApplicationRoundPermission, }; }; diff --git a/apps/admin-ui/src/modules/permissionHelper.ts b/apps/admin-ui/src/modules/permissionHelper.ts index 9802598c40..851c821a91 100644 --- a/apps/admin-ui/src/modules/permissionHelper.ts +++ b/apps/admin-ui/src/modules/permissionHelper.ts @@ -20,6 +20,7 @@ export enum Permission { CAN_MANAGE_RESOURCES = GeneralPermissionChoices.CanManageResources, CAN_MANAGE_UNITS = GeneralPermissionChoices.CanManageUnits, CAN_VALIDATE_APPLICATIONS = GeneralPermissionChoices.CanValidateApplications, + CAN_MANAGE_APPLICATIONS = GeneralPermissionChoices.CanHandleApplications, CAN_MANAGE_BANNER_NOTIFICATIONS = GeneralPermissionChoices.CanManageNotifications, } /* eslint-enable @typescript-eslint/prefer-literal-enum-member */ diff --git a/apps/admin-ui/src/spa/recurring-reservations/application-rounds/[id]/allocation/index.tsx b/apps/admin-ui/src/spa/recurring-reservations/application-rounds/[id]/allocation/index.tsx index 86655a333b..473b2475d8 100644 --- a/apps/admin-ui/src/spa/recurring-reservations/application-rounds/[id]/allocation/index.tsx +++ b/apps/admin-ui/src/spa/recurring-reservations/application-rounds/[id]/allocation/index.tsx @@ -556,6 +556,7 @@ function AllocationWrapper({ const { t } = useTranslation(); const { hasUnitPermission } = usePermission(); + const { hasApplicationRoundPermission } = usePermission(); // TODO don't use spinners, skeletons are better // also this blocks the sub component query (the initial with zero filters) which slows down the page load @@ -570,8 +571,24 @@ function AllocationWrapper({ return

{t("errors.errorFetchingData")}

; } - const appRound = data?.applicationRound; - const reservationUnits = filterNonNullable(appRound?.reservationUnits); + const { applicationRound } = data ?? {}; + + // should never be null but our codegen causes type problems + const canManage = + applicationRound != null + ? hasApplicationRoundPermission( + applicationRound, + Permission.CAN_MANAGE_APPLICATIONS + ) + : false; + + if (!canManage) { + return
{t("errors.noPermission")}
; + } + + const reservationUnits = filterNonNullable( + applicationRound?.reservationUnits + ); const unitData = reservationUnits.map((ru) => ru?.unit); // TODO name sort fails with numbers because 11 < 2 @@ -581,7 +598,7 @@ function AllocationWrapper({ ) .sort((a, b) => a?.nameFi?.localeCompare(b?.nameFi ?? "") ?? 0); - const roundName = appRound?.nameFi ?? "-"; + const roundName = applicationRound?.nameFi ?? "-"; const resUnits = uniqBy(filterNonNullable(reservationUnits), "pk").sort( (a, b) => a?.nameFi?.localeCompare(b?.nameFi ?? "") ?? 0 @@ -591,13 +608,13 @@ function AllocationWrapper({ <> diff --git a/apps/admin-ui/src/spa/recurring-reservations/application-rounds/[id]/index.tsx b/apps/admin-ui/src/spa/recurring-reservations/application-rounds/[id]/index.tsx index ea761fd97b..db4ded644d 100644 --- a/apps/admin-ui/src/spa/recurring-reservations/application-rounds/[id]/index.tsx +++ b/apps/admin-ui/src/spa/recurring-reservations/application-rounds/[id]/index.tsx @@ -2,34 +2,38 @@ import React from "react"; import { useQuery } from "@apollo/client"; import { useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { type Query } from "common/types/gql-types"; +import { + type Query, + type QueryApplicationRoundArgs, +} from "common/types/gql-types"; import { useNotification } from "@/context/NotificationContext"; import BreadcrumbWrapper from "@/component/BreadcrumbWrapper"; import Loader from "@/component/Loader"; import { Review } from "./review/Review"; import { APPLICATION_ROUND_QUERY } from "../queries"; +import usePermission from "@/hooks/usePermission"; +import { Permission } from "@/modules/permissionHelper"; +import { base64encode } from "common/src/helpers"; -function ApplicationRound({ - applicationRoundId, -}: { - applicationRoundId: number; -}): JSX.Element | null { +function ApplicationRound({ pk }: { pk: number }): JSX.Element { const { notifyError } = useNotification(); const { t } = useTranslation(); - const { data, loading: isLoading } = useQuery( - APPLICATION_ROUND_QUERY, - { - skip: !applicationRoundId, - variables: { - pk: [applicationRoundId], - }, - onError: () => { - notifyError(t("errors.errorFetchingData")); - }, - } - ); - const applicationRound = data?.applicationRounds?.edges?.[0]?.node; + const id = base64encode(`ApplicationRoundNode:${pk}`); + const { data, loading: isLoading } = useQuery< + Query, + QueryApplicationRoundArgs + >(APPLICATION_ROUND_QUERY, { + skip: !pk, + variables: { id }, + onError: () => { + notifyError(t("errors.errorFetchingData")); + }, + }); + + const { applicationRound } = data ?? {}; + + const { hasApplicationRoundPermission } = usePermission(); if (isLoading) { return ; @@ -39,6 +43,18 @@ function ApplicationRound({ return
{t("errors.applicationRoundNotFound")}
; } + const canView = hasApplicationRoundPermission( + applicationRound, + Permission.CAN_VALIDATE_APPLICATIONS + ); + const canManage = hasApplicationRoundPermission( + applicationRound, + Permission.CAN_MANAGE_APPLICATIONS + ); + if (!canView && !canManage) { + return
{t("errors.noPermission")}
; + } + const route = [ { alias: t("breadcrumb.recurring-reservations"), @@ -70,10 +86,12 @@ function ApplicationRoundRouted(): JSX.Element | null { const { t } = useTranslation(); const { applicationRoundId } = useParams(); - if (!applicationRoundId || Number.isNaN(Number(applicationRoundId))) { - return
{t("errors.router.invalidApplicationRoundNumber")}
; + const pk = Number(applicationRoundId); + if (pk > 0) { + return ; } - return ; + + return
{t("errors.router.invalidApplicationRoundNumber")}
; } export default ApplicationRoundRouted; diff --git a/apps/admin-ui/src/spa/recurring-reservations/application-rounds/queries.tsx b/apps/admin-ui/src/spa/recurring-reservations/application-rounds/queries.tsx index 467f95292c..200f9dc2e9 100644 --- a/apps/admin-ui/src/spa/recurring-reservations/application-rounds/queries.tsx +++ b/apps/admin-ui/src/spa/recurring-reservations/application-rounds/queries.tsx @@ -32,23 +32,18 @@ export const APPLICATION_ROUNDS_QUERY = gql` } `; -// TODO replace with relay query export const APPLICATION_ROUND_QUERY = gql` ${APPLICATION_ROUND_FRAGMENT} - query ApplicationRound($pk: [Int]!) { - applicationRounds(pk: $pk) { - edges { - node { - ...ApplicationRoundFragment - applicationsCount - reservationUnits { - pk - nameFi - unit { - pk - nameFi - } - } + query ApplicationRound($id: ID!) { + applicationRound(id: $id) { + ...ApplicationRoundFragment + applicationsCount + reservationUnits { + pk + nameFi + unit { + pk + nameFi } } }