From 78673aab4ef3578ede42ad802af75b7443b2fd53 Mon Sep 17 00:00:00 2001 From: Barrett Falk Date: Mon, 23 Sep 2024 17:32:57 -0700 Subject: [PATCH 01/19] feat: CE-1046 create a read only level of access for nat com (#650) Co-authored-by: dmitri-korin-bcps <108112696+dmitri-korin-bcps@users.noreply.github.com> Co-authored-by: gregorylavery <100631366+gregorylavery@users.noreply.github.com> Co-authored-by: Scarlett <35635257+Scarlett-Truong@users.noreply.github.com> Co-authored-by: Mike <100624415+marqueone-ps@users.noreply.github.com> Co-authored-by: Mike Sears Co-authored-by: jeznorth Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: afwilcox Co-authored-by: Scarlett --- backend/src/auth/jwtrole.guard.ts | 25 +++++-- frontend/src/app/common/api.ts | 15 +++++ .../containers/admin/user-management.tsx | 65 ++++++++++--------- frontend/src/app/constants/ceeb-roles.ts | 4 +- frontend/src/app/store/reducers/complaints.ts | 1 - 5 files changed, 73 insertions(+), 37 deletions(-) diff --git a/backend/src/auth/jwtrole.guard.ts b/backend/src/auth/jwtrole.guard.ts index 0ba769095..48d39d570 100644 --- a/backend/src/auth/jwtrole.guard.ts +++ b/backend/src/auth/jwtrole.guard.ts @@ -1,7 +1,14 @@ -import { ExecutionContext, Injectable, CanActivate, UnauthorizedException, Logger } from "@nestjs/common"; +import { + ExecutionContext, + Injectable, + CanActivate, + UnauthorizedException, + Logger, + ForbiddenException, +} from "@nestjs/common"; import { Reflector } from "@nestjs/core"; import { AuthGuard } from "@nestjs/passport"; -import { Role } from "src/enum/role.enum"; +import { Role } from "../enum/role.enum"; import { ROLES_KEY } from "./decorators/roles.decorator"; import { IS_PUBLIC_KEY } from "./decorators/public.decorator"; @@ -46,6 +53,17 @@ export class JwtRoleGuard extends AuthGuard("jwt") implements CanActivate { } else { this.logger.debug("User authorization verified"); } + const userRoles: string[] = user.client_roles; + // Check if the user has the readonly role + const hasReadOnlyRole = userRoles.includes(Role.READ_ONLY); + + // If the user has readonly role, allow only GET requests + if (hasReadOnlyRole) { + if (request.method !== "GET") { + this.logger.debug(`User with readonly role attempted ${request.method} method`); + throw new ForbiddenException("Access denied: Read-only users cannot perform this action"); + } + } // if there aren't any required roles, don't allow the user to access any api. Unless the API is marked as public, at least one role is required. if (!requiredRoles) { @@ -57,9 +75,6 @@ export class JwtRoleGuard extends AuthGuard("jwt") implements CanActivate { this.logger.debug(`Endpoint ${request.originalUrl} is properly guarded.`); } - // roles that the user has - const userRoles: string[] = user.client_roles; - this.logger.debug(`User Roles: ${userRoles}`); // does the user have a required role? diff --git a/frontend/src/app/common/api.ts b/frontend/src/app/common/api.ts index 321c0fd66..b3be11580 100644 --- a/frontend/src/app/common/api.ts +++ b/frontend/src/app/common/api.ts @@ -5,6 +5,7 @@ import { AUTH_TOKEN } from "../service/user-service"; import { ApiRequestParameters } from "../types/app/api-request-parameters"; import { toggleLoading, toggleNotification } from "../store/reducers/app"; import { store } from "../../app/store/store"; +import { ToggleError } from "./toast"; const STATUS_CODES = { Ok: 200, @@ -47,6 +48,20 @@ axios.interceptors.response.use( }, ); +axios.interceptors.response.use( + (response) => { + // Successful response, just return the data + return response; + }, + (error: AxiosError) => { + const { response } = error; + + if (response && response.status === STATUS_CODES.Forbiden) { + ToggleError("User is not authorized to perform this action"); + } + }, +); + const { KEYCLOAK_URL } = config; export const generateApiParameters = ( diff --git a/frontend/src/app/components/containers/admin/user-management.tsx b/frontend/src/app/components/containers/admin/user-management.tsx index b5f955e02..8473db6a0 100644 --- a/frontend/src/app/components/containers/admin/user-management.tsx +++ b/frontend/src/app/components/containers/admin/user-management.tsx @@ -198,15 +198,17 @@ export const UserManagement: FC = () => { const officerId = officer?.value ? officer.value : ""; const officeId = office?.value ? office.value : ""; dispatch(assignOfficerToOffice(officerId, officeId)); - res = await updateTeamRole(selectedUserIdir, officerGuid, selectedAgency?.value, null, [ - { name: Roles.COS_OFFICER }, - ]); + const mapRoles = selectedRoles?.map((role) => { + return { name: role.value }; + }); + res = await updateTeamRole(selectedUserIdir, officerGuid, selectedAgency?.value, null, mapRoles); break; } } if (res && res.team && res.roles) { ToggleSuccess("Success"); } else { + debugger; ToggleError("Unable to update"); } } @@ -324,39 +326,42 @@ export const UserManagement: FC = () => { />
+ + )} + {selectedAgency?.value === "COS" && ( + <>
- Select Role - handleOfficeChange(evt)} + classNames={{ + menu: () => "top-layer-select", + }} + options={officeAssignments} + placeholder="Select" + enableValidation={true} + value={office} + errorMessage={officeError} />
+
)} - {selectedAgency?.value === "COS" && ( -
- Select Office - handleOfficeChange(evt)} - classNames={{ - menu: () => "top-layer-select", - }} - options={officeAssignments} - placeholder="Select" - enableValidation={true} - value={office} - errorMessage={officeError} - /> -
- )} +
+ Select Role + +

Rationale
-
{getValue("rationale")?.label}
+
{rationale}
Assigned to
diff --git a/frontend/src/app/constants/code-table-types.ts b/frontend/src/app/constants/code-table-types.ts index 77e5d9d46..10e4d42b7 100644 --- a/frontend/src/app/constants/code-table-types.ts +++ b/frontend/src/app/constants/code-table-types.ts @@ -27,7 +27,6 @@ export const CODE_TABLE_TYPES = { GIR_TYPE: "gir-type", DISCHARGE: "discharge", NON_COMPLIANCE: "non-compliance", - RATIONALE: "rationale", SECTOR: "sector", SCHEDULE: "schedule", DECISION_TYPE: "decision-type", diff --git a/frontend/src/app/store/reducers/code-table-selectors.ts b/frontend/src/app/store/reducers/code-table-selectors.ts index 466a10693..513935be9 100644 --- a/frontend/src/app/store/reducers/code-table-selectors.ts +++ b/frontend/src/app/store/reducers/code-table-selectors.ts @@ -25,18 +25,6 @@ export const selectNonComplianceDropdown = (state: RootState): Array
); diff --git a/frontend/src/app/components/containers/complaints/complaint-filter.tsx b/frontend/src/app/components/containers/complaints/complaint-filter.tsx index 99373bbed..07af6acb2 100644 --- a/frontend/src/app/components/containers/complaints/complaint-filter.tsx +++ b/frontend/src/app/components/containers/complaints/complaint-filter.tsx @@ -11,6 +11,7 @@ import { selectCascadedCommunity, selectComplaintStatusWithPendingCodeDropdown, selectGirTypeCodeDropdown, + selectComplaintReceivedMethodDropdown, } from "../../../store/reducers/code-table"; import { selectOfficersDropdown } from "../../../store/reducers/officer"; import COMPLAINT_TYPES from "../../../types/app/complaint-types"; @@ -40,6 +41,7 @@ export const ComplaintFilter: FC = ({ type }) => { startDate, endDate, girType, + complaintMethod, }, dispatch, } = useContext(ComplaintFilterContext); @@ -59,6 +61,8 @@ export const ComplaintFilter: FC = ({ type }) => { const zones = useAppSelector(selectCascadedZone(region?.value, zone?.value, community?.value)); const communities = useAppSelector(selectCascadedCommunity(region?.value, zone?.value, community?.value)); + const complaintMethods = useAppSelector(selectComplaintReceivedMethodDropdown); + const activeFilters = useAppSelector(listActiveFilters()); const setFilter = useCallback( @@ -112,98 +116,102 @@ export const ComplaintFilter: FC = ({ type }) => { const renderComplaintFilters = (): JSX.Element => { return (
- {COMPLAINT_TYPES.HWCR === type && activeFilters.showNatureComplaintFilter && activeFilters.showSpeciesFilter && ( // wildlife only filter - <> -
- + {COMPLAINT_TYPES.HWCR === type && + activeFilters.showNatureComplaintFilter && + activeFilters.showSpeciesFilter && ( // wildlife only filter + <> +
+ +
+ { + setFilter("natureOfComplaint", option); + }} + classNames={{ + menu: () => "top-layer-select", + }} + options={natureOfComplaintTypes} + placeholder="Select" + enableValidation={false} + value={natureOfComplaint} + isClearable={true} + /> +
+
+ +
+ +
+ { + setFilter("species", option); + }} + classNames={{ + menu: () => "top-layer-select", + }} + options={speciesTypes} + placeholder="Select" + enableValidation={false} + value={species} + isClearable={true} + /> +
+
+ + )} + + {COMPLAINT_TYPES.ERS === type && + activeFilters.showViolationFilter && ( // wildlife only filter +
+ {/* */} +
{ - setFilter("natureOfComplaint", option); + setFilter("violationType", option); }} classNames={{ menu: () => "top-layer-select", }} - options={natureOfComplaintTypes} + options={violationTypes} placeholder="Select" enableValidation={false} - value={natureOfComplaint} + value={violationType} isClearable={true} />
+ )} -
- + {COMPLAINT_TYPES.GIR === type && + activeFilters.showGirTypeFilter && ( // GIR only filter +
+
{ - setFilter("species", option); + setFilter("girType", option); }} classNames={{ menu: () => "top-layer-select", }} - options={speciesTypes} + options={girTypes} placeholder="Select" enableValidation={false} - value={species} + value={girType} isClearable={true} />
- - )} - - {COMPLAINT_TYPES.ERS === type && activeFilters.showViolationFilter && ( // wildlife only filter -
- {/* */} - -
- { - setFilter("violationType", option); - }} - classNames={{ - menu: () => "top-layer-select", - }} - options={violationTypes} - placeholder="Select" - enableValidation={false} - value={violationType} - isClearable={true} - /> -
-
- )} - - {COMPLAINT_TYPES.GIR === type && activeFilters.showGirTypeFilter && ( // GIR only filter -
- -
- { - setFilter("girType", option); - }} - classNames={{ - menu: () => "top-layer-select", - }} - options={girTypes} - placeholder="Select" - enableValidation={false} - value={girType} - isClearable={true} - /> -
-
- )} + )} {activeFilters.showDateFilter && (
@@ -216,8 +224,9 @@ export const ComplaintFilter: FC = ({ type }) => {
)} + + {COMPLAINT_TYPES.ERS === type && activeFilters.showMethodFilter && ( +
+ +
+ { + setFilter("complaintMethod", option); + }} + classNames={{ + menu: () => "top-layer-select", + }} + options={complaintMethods} + placeholder="Select" + enableValidation={false} + value={complaintMethod} + isClearable={true} + /> +
+
+ )}
); }; diff --git a/frontend/src/app/components/containers/complaints/complaint-list.tsx b/frontend/src/app/components/containers/complaints/complaint-list.tsx index 2c002c23a..c668337eb 100644 --- a/frontend/src/app/components/containers/complaints/complaint-list.tsx +++ b/frontend/src/app/components/containers/complaints/complaint-list.tsx @@ -51,6 +51,7 @@ export const generateComplaintRequestPayload = ( natureOfComplaint, violationType, girType, + complaintMethod, } = filters; const common = { @@ -78,6 +79,7 @@ export const generateComplaintRequestPayload = ( return { ...common, violationFilter: violationType, + complaintMethodFilter: complaintMethod, } as ComplaintRequestPayload; case COMPLAINT_TYPES.HWCR: default: diff --git a/frontend/src/app/components/containers/complaints/complaint-map.tsx b/frontend/src/app/components/containers/complaints/complaint-map.tsx index 5de0ce89e..4ad79b41d 100644 --- a/frontend/src/app/components/containers/complaints/complaint-map.tsx +++ b/frontend/src/app/components/containers/complaints/complaint-map.tsx @@ -24,8 +24,19 @@ export const generateMapComplaintRequestPayload = ( sortKey: string, sortDirection: string, ): ComplaintRequestPayload => { - const { region, zone, community, officer, startDate, endDate, status, species, natureOfComplaint, violationType } = - filters; + const { + region, + zone, + community, + officer, + startDate, + endDate, + status, + species, + natureOfComplaint, + violationType, + complaintMethod, + } = filters; const common = { sortColumn: sortKey, @@ -44,6 +55,7 @@ export const generateMapComplaintRequestPayload = ( return { ...common, violationFilter: violationType, + complaintMethodFilter: complaintMethod, } as ComplaintRequestPayload; case COMPLAINT_TYPES.HWCR: default: diff --git a/frontend/src/app/providers/complaint-filter-provider.tsx b/frontend/src/app/providers/complaint-filter-provider.tsx index 23f927e27..61bfd985e 100644 --- a/frontend/src/app/providers/complaint-filter-provider.tsx +++ b/frontend/src/app/providers/complaint-filter-provider.tsx @@ -25,6 +25,7 @@ let initialState: ComplaintFilters = { natureOfComplaint: null, violationType: null, filters: [], + complaintMethod: null, }; const ComplaintFilterContext = createContext({ diff --git a/frontend/src/app/store/reducers/complaints.ts b/frontend/src/app/store/reducers/complaints.ts index ad3b09e10..68357c65f 100644 --- a/frontend/src/app/store/reducers/complaints.ts +++ b/frontend/src/app/store/reducers/complaints.ts @@ -276,6 +276,7 @@ export const getComplaints = violationFilter, girTypeFilter, complaintStatusFilter, + complaintMethodFilter, page, pageSize, query, @@ -298,6 +299,7 @@ export const getComplaints = violationCode: violationFilter?.value, girTypeCode: girTypeFilter?.value, status: complaintStatusFilter?.value, + complaintMethod: complaintMethodFilter?.value, page: page, pageSize: pageSize, query: query, @@ -328,6 +330,7 @@ export const getMappedComplaints = endDateFilter, violationFilter, complaintStatusFilter, + complaintMethodFilter, page, pageSize, query, @@ -349,6 +352,7 @@ export const getMappedComplaints = incidentReportedEnd: endDateFilter, violationCode: violationFilter?.value, status: complaintStatusFilter?.value, + complaintMethod: complaintMethodFilter?.value, page: page, pageSize: pageSize, query: query, diff --git a/frontend/src/app/types/complaints/complaint-filters.ts b/frontend/src/app/types/complaints/complaint-filters.ts index 1a49a46f4..40e84c81d 100644 --- a/frontend/src/app/types/complaints/complaint-filters.ts +++ b/frontend/src/app/types/complaints/complaint-filters.ts @@ -14,6 +14,7 @@ export interface ComplaintFilters { startDateFilter?: Date; endDateFilter?: Date; complaintStatusFilter?: Option; + complaintMethodFilter?: Option; page?: number; pageSize?: number; query?: string; diff --git a/frontend/src/app/types/complaints/complaint-filters/complaint-filters.ts b/frontend/src/app/types/complaints/complaint-filters/complaint-filters.ts index 3821ec9d4..6e8975c90 100644 --- a/frontend/src/app/types/complaints/complaint-filters/complaint-filters.ts +++ b/frontend/src/app/types/complaints/complaint-filters/complaint-filters.ts @@ -18,5 +18,7 @@ export type ComplaintFilters = { girType?: DropdownOption | null; + complaintMethod: DropdownOption | null; + filters: Array; }; diff --git a/migrations/migrations/R__Create-Test-Data.sql b/migrations/migrations/R__Create-Test-Data.sql index 229179a23..d2dd0a9c2 100644 --- a/migrations/migrations/R__Create-Test-Data.sql +++ b/migrations/migrations/R__Create-Test-Data.sql @@ -9483,6 +9483,10 @@ SELECT now() ON CONFLICT DO NOTHING; +UPDATE feature_agency_xref SET active_ind = false WHERE feature_code = 'METH_FLTR' AND agency_code = 'COS'; +UPDATE feature_agency_xref SET active_ind = false WHERE feature_code = 'METH_FLTR' AND agency_code = 'PARKS'; +UPDATE feature_agency_xref SET active_ind = true WHERE feature_code = 'METH_FLTR' AND agency_code = 'EPO'; + -------------------------- -- New Changes above this line ------------------------- @@ -9490,4 +9494,4 @@ UPDATE configuration SET configuration_value = configuration_value::int + 1 WHERE - configuration_code = 'CDTABLEVER'; \ No newline at end of file + configuration_code = 'CDTABLEVER'; From dbc31a97489d73bbb9500ddd364fc9ede2d8b14d Mon Sep 17 00:00:00 2001 From: gregorylavery <100631366+gregorylavery@users.noreply.github.com> Date: Tue, 1 Oct 2024 11:09:42 -0700 Subject: [PATCH 15/19] fix: Ce 1112 (#679) --- .../complaints/complaint-filter.tsx | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/components/containers/complaints/complaint-filter.tsx b/frontend/src/app/components/containers/complaints/complaint-filter.tsx index 07af6acb2..cbc1cc817 100644 --- a/frontend/src/app/components/containers/complaints/complaint-filter.tsx +++ b/frontend/src/app/components/containers/complaints/complaint-filter.tsx @@ -13,7 +13,7 @@ import { selectGirTypeCodeDropdown, selectComplaintReceivedMethodDropdown, } from "../../../store/reducers/code-table"; -import { selectOfficersDropdown } from "../../../store/reducers/officer"; +import { selectOfficersByAgencyDropdown } from "../../../store/reducers/officer"; import COMPLAINT_TYPES from "../../../types/app/complaint-types"; import DatePicker from "react-datepicker"; import { CompSelect } from "../../common/comp-select"; @@ -46,11 +46,11 @@ export const ComplaintFilter: FC = ({ type }) => { dispatch, } = useContext(ComplaintFilterContext); - let officers = useAppSelector(selectOfficersDropdown(false)); - if (officers && officers[0]?.value !== "Unassigned") { - officers.unshift({ value: "Unassigned", label: "Unassigned" }); - } const agency = getUserAgency(); + let officersByAgency = useAppSelector(selectOfficersByAgencyDropdown(agency)); + if (officersByAgency && officersByAgency[0]?.value !== "Unassigned") { + officersByAgency.unshift({ value: "Unassigned", label: "Unassigned" }); + } const natureOfComplaintTypes = useAppSelector(selectHwcrNatureOfComplaintCodeDropdown); const speciesTypes = useAppSelector(selectSpeciesCodeDropdown); const statusTypes = useAppSelector(selectComplaintStatusWithPendingCodeDropdown); @@ -224,9 +224,8 @@ export const ComplaintFilter: FC = ({ type }) => {