diff --git a/app/src/middleware/authorization.ts b/app/src/middleware/authorization.ts index 1a2becc1..03ed9160 100644 --- a/app/src/middleware/authorization.ts +++ b/app/src/middleware/authorization.ts @@ -13,7 +13,7 @@ import { yarsService } from '../services'; import { Initiative, GroupName } from '../utils/enums/application'; -import { getCurrentSubject } from '../utils/utils'; +import { getCurrentSubject, getCurrentUsername } from '../utils/utils'; import type { NextFunction, Request, Response } from 'express'; import { CurrentAuthorization } from '../types'; @@ -150,3 +150,30 @@ export const hasAccess = (param: string) => { next(); }; }; + +export const hasAccessPermit = (param: string) => { + return async (req: Request, res: Response, next: NextFunction) => { + try { + if (req.currentAuthorization?.attributes.includes('scope:self')) { + const id = req.params[param]; + const userId = await userService.getCurrentUserId(getCurrentSubject(req.currentContext), NIL); + + let data; + const func = paramMap.get(param); + if (func) data = await func(id); + + if (!data || data?.createdBy !== userId) { + const submission = (await submissionService.searchSubmissions({ activityId: [data.activityId] }))[0]; + if (!submission || submission?.submittedBy !== getCurrentUsername(req.currentContext)) + throw new Error('No access'); + } + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (err: any) { + return next(new Problem(403, { detail: err.message, instance: req.originalUrl })); + } + + // Continue middleware + next(); + }; +}; diff --git a/app/src/routes/v1/permit.ts b/app/src/routes/v1/permit.ts index a6d2e8ba..f3d77658 100644 --- a/app/src/routes/v1/permit.ts +++ b/app/src/routes/v1/permit.ts @@ -2,7 +2,7 @@ import express from 'express'; import permitNote from './permitNote'; import { permitController } from '../../controllers'; -import { hasAccess, hasAuthorization } from '../../middleware/authorization'; +import { hasAccess, hasAccessPermit, hasAuthorization } from '../../middleware/authorization'; import { requireSomeAuth } from '../../middleware/requireSomeAuth'; import { requireSomeGroup } from '../../middleware/requireSomeGroup'; import { Action, Resource } from '../../utils/enums/application'; @@ -71,7 +71,7 @@ router.get( router.get( '/:permitId', hasAuthorization(Resource.PERMIT, Action.READ), - hasAccess('permitId'), + hasAccessPermit('permitId'), permitValidator.getPermit, (req: Request<{ permitId: string }>, res: Response, next: NextFunction): void => { permitController.getPermit(req, res, next); diff --git a/frontend/src/locales/en-CA.json b/frontend/src/locales/en-CA.json index 5e477698..178f86e5 100644 --- a/frontend/src/locales/en-CA.json +++ b/frontend/src/locales/en-CA.json @@ -96,7 +96,7 @@ "logout": "Log out" }, "permitStatusView": { - "additionalUpdates": "Additional updates.", + "additionalUpdates": "Additional updates", "applicationProgress": "Application progress", "askNav": "Ask my Navigator", "contactNav": "Contact your Navigator for this project for further updates on this application.", diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index b23c8e5d..cb106dc5 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -179,15 +179,6 @@ const routes: Array = [ access: [NavigationPermission.HOUSING_STATUS_TRACKER] }, children: [ - { - path: '', - component: () => import('@/views/housing/project/ProjectListView.vue'), - beforeEnter: accessHandler, - meta: { - access: [NavigationPermission.HOUSING_STATUS_TRACKER] - }, - name: RouteName.HOUSING_PROJECTS_LIST - }, { path: ':submissionId', component: () => import('@/views/housing/project/ProjectView.vue'), diff --git a/frontend/src/store/authzStore.ts b/frontend/src/store/authzStore.ts index d2e92686..e7a20e42 100644 --- a/frontend/src/store/authzStore.ts +++ b/frontend/src/store/authzStore.ts @@ -62,7 +62,8 @@ const NavigationAuthorizationMap = [ NavigationPermission.HOUSING_DROPDOWN, NavigationPermission.HOUSING_ENQUIRY_INTAKE, NavigationPermission.HOUSING_SUBMISSION_INTAKE, - NavigationPermission.HOUSING_SUBMISSIONS_SUB + NavigationPermission.HOUSING_SUBMISSIONS_SUB, + NavigationPermission.HOUSING_STATUS_TRACKER ] }, { diff --git a/frontend/src/views/housing/HousingView.vue b/frontend/src/views/housing/HousingView.vue index a8cc03c4..d2dd97cc 100644 --- a/frontend/src/views/housing/HousingView.vue +++ b/frontend/src/views/housing/HousingView.vue @@ -29,13 +29,6 @@ const { t } = useI18n(); const route = useRoute(); const router = useRouter(); -function onProjectClick(project: Submission) { - router.push({ - name: RouteName.HOUSING_SUBMISSION_INTAKE, - query: { activityId: project.activityId, submissionId: project.submissionId } - }); -} - function onSubmissionDraftDelete(draftId: string) { drafts.value = drafts.value.filter((x) => x.draftId !== draftId); } @@ -166,15 +159,14 @@ onMounted(async () => { :index="index" class="rounded-sm shadow-md hover:shadow-lg px-6 py-4 custom-card hover-hand" :class="{ 'mb-2': index != displayedProjects.length - 1 }" - @click="onProjectClick(project)" >

{{ project.projectName }}

diff --git a/frontend/src/views/housing/project/ProjectListView.vue b/frontend/src/views/housing/project/ProjectListView.vue deleted file mode 100644 index 513fb264..00000000 --- a/frontend/src/views/housing/project/ProjectListView.vue +++ /dev/null @@ -1,91 +0,0 @@ - - - - - diff --git a/frontend/src/views/housing/project/ProjectView.vue b/frontend/src/views/housing/project/ProjectView.vue index 8eca7770..8a40bc11 100644 --- a/frontend/src/views/housing/project/ProjectView.vue +++ b/frontend/src/views/housing/project/ProjectView.vue @@ -20,11 +20,11 @@ import { RouteName } from '@/utils/enums/application'; import { PermitAuthorizationStatus, PermitNeeded, PermitStatus, SubmissionType } from '@/utils/enums/housing'; import { formatDate } from '@/utils/formatters'; -import { enquiryService, permitService, submissionService, userService } from '@/services'; +import { contactService, enquiryService, permitService, submissionService } from '@/services'; import { useSubmissionStore, useTypeStore } from '@/store'; import type { ComputedRef, Ref } from 'vue'; -import type { Permit, PermitType, User } from '@/types'; +import type { Contact, Permit, PermitType } from '@/types'; import type { MenuItem } from 'primevue/menuitem'; import EnquiryListProponent from '@/components/housing/enquiry/EnquiryListProponent.vue'; @@ -57,11 +57,11 @@ const typeStore = useTypeStore(); const { getPermitTypes } = storeToRefs(typeStore); // State -const assignee: Ref = ref(undefined); +const assignee: Ref = ref(undefined); const breadcrumbItems: ComputedRef> = computed(() => [ { label: getSubmission?.value?.projectName ?? '', class: 'font-bold' } ]); -const createdBy: Ref = ref(undefined); +const createdBy: Ref = ref(undefined); const loading: Ref = ref(true); const permitsNeeded = computed(() => { @@ -134,15 +134,14 @@ function permitFilter(config: PermitFilterConfig) { return returnArray.filter((pt) => !!pt) as Array; } -function navigateToSubmissionView() { +function navigateToSubmissionIntakeView() { router.push({ - name: RouteName.HOUSING_SUBMISSION, + name: RouteName.HOUSING_SUBMISSION_INTAKE, query: { activityId: getSubmission.value?.activityId, submissionId: getSubmission.value?.submissionId } }); } - onMounted(async () => { - let enquiriesValue, permitTypesValue, submissionValue; + let enquiriesValue, permitTypesValue, submissionValue: any; try { [submissionValue, permitTypesValue] = ( @@ -165,13 +164,13 @@ onMounted(async () => { submissionStore.setRelatedEnquiries(enquiriesValue); typeStore.setPermitTypes(permitTypesValue); - if (submissionValue?.assignedUserId) { - assignee.value = (await userService.searchUsers({ userId: [submissionValue.assignedUserId] })).data[0]; - } + // Fetch contacts for createdBy and assignedUserId + // Push only thruthy values into the array + const userIds = [submissionValue?.assignedUserId, submissionValue?.createdBy].filter(Boolean); + const contacts = (await contactService.searchContacts({ userId: userIds })).data; + assignee.value = contacts.find((contact: Contact) => contact.userId === submissionValue?.assignedUserId); + createdBy.value = contacts.find((contact: Contact) => contact.userId === submissionValue?.createdBy); - if (submissionValue?.createdBy) { - createdBy.value = (await userService.searchUsers({ userId: [submissionValue.createdBy] })).data[0]; - } loading.value = false; }); @@ -192,9 +191,9 @@ onMounted(async () => {

{{ getSubmission.projectName }} { { label: permit?.value?.name, class: 'font-bold' } ]; if (submission.value?.assignedUserId) { - assignedNavigator.value = (await userService.searchUsers({ userId: [submission.value.assignedUserId] })).data[0]; + assignedNavigator.value = ( + await contactService.searchContacts({ userId: [submission.value.assignedUserId] }) + ).data[0]; } if (permit.value?.updatedBy) { - const updatedByUser = (await userService.searchUsers({ userId: [permitResponse.data.updatedBy] })).data[0]; + const updatedByUser = (await contactService.searchContacts({ userId: [permitResponse.data.updatedBy] })).data[0]; updatedBy.value = updatedByUser.firstName + ' ' + updatedByUser.lastName; } } catch { diff --git a/frontend/tests/unit/views/housing/project/ProjectListView.spec.ts b/frontend/tests/unit/views/housing/project/ProjectListView.spec.ts deleted file mode 100644 index 6c5c5bf6..00000000 --- a/frontend/tests/unit/views/housing/project/ProjectListView.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import ProjectListView from '@/views/housing/project/ProjectListView.vue'; -import { submissionService } from '@/services'; -import { createTestingPinia } from '@pinia/testing'; -import PrimeVue from 'primevue/config'; -import ConfirmationService from 'primevue/confirmationservice'; -import ToastService from 'primevue/toastservice'; -import { shallowMount } from '@vue/test-utils'; -import type { AxiosResponse } from 'axios'; - -vi.mock('vue-router', () => ({ - useRoute: vi.fn(() => ({ - query: {} - })), - useRouter: vi.fn(() => ({ - push: vi.fn() - })) -})); - -const useSubmissionService = vi.spyOn(submissionService, 'searchSubmissions'); - -useSubmissionService.mockResolvedValue({ - data: [{ activityId: 'activity456' }] -} as AxiosResponse); - -const wrapperSettings = () => ({ - global: { - plugins: [ - () => - createTestingPinia({ - initialState: { - auth: { - user: {} - } - } - }), - PrimeVue, - ConfirmationService, - ToastService - ], - stubs: ['font-awesome-icon', 'router-link'] - } -}); - -describe('ProjectListView.vue', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('renders the component with the provided props', () => { - const wrapper = shallowMount(ProjectListView, wrapperSettings()); - expect(wrapper).toBeTruthy(); - }); -}); diff --git a/frontend/tests/unit/views/housing/project/ProjectView.spec.ts b/frontend/tests/unit/views/housing/project/ProjectView.spec.ts index 90adbe1b..fa214668 100644 --- a/frontend/tests/unit/views/housing/project/ProjectView.spec.ts +++ b/frontend/tests/unit/views/housing/project/ProjectView.spec.ts @@ -1,5 +1,5 @@ import ProjectView from '@/views/housing/project/ProjectView.vue'; -import { submissionService } from '@/services'; +import { contactService, submissionService } from '@/services'; import { createTestingPinia } from '@pinia/testing'; import PrimeVue from 'primevue/config'; import ConfirmationService from 'primevue/confirmationservice'; @@ -42,14 +42,25 @@ afterEach(() => { sessionStorage.clear(); }); +const useContactService = vi.spyOn(contactService, 'searchContacts'); const useSubmissionService = vi.spyOn(submissionService, 'searchSubmissions'); const testSubmissionId = 'submission123'; +const exampleContact = { + contactId: 'contact123', + name: 'John Doe', + email: 'john.doe@example.com', + phone: '123-456-7890' +}; useSubmissionService.mockResolvedValue({ data: [{ activityId: 'activity456' }] } as AxiosResponse); +useContactService.mockResolvedValue({ + data: [exampleContact] +} as AxiosResponse); + const wrapperSettings = (testSubmissionIdProp = testSubmissionId) => ({ props: { submissionId: testSubmissionIdProp