Skip to content

Commit

Permalink
OFMCC-3504 - Manage Users View security (#199)
Browse files Browse the repository at this point in the history
* Added permissions for Manage Users View.
Refactored fundingAgreement queries to improve API and simplify code.

* PR feedback.

* minor change so funding agreement view loads FAs with no EA attached yet

---------

Co-authored-by: weskubo-cgi <[email protected]>
Co-authored-by: Jen Beckett <[email protected]>
  • Loading branch information
3 people authored May 17, 2024
1 parent 1d14b92 commit 6388dfb
Show file tree
Hide file tree
Showing 12 changed files with 140 additions and 176 deletions.
37 changes: 16 additions & 21 deletions backend/src/components/fundingAgreements.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,31 @@
'use strict'
const { getOperation, patchOperationWithObjectId, getOperationWithObjectId } = require('./utils')
const { MappableObjectForFront, MappableObjectForBack } = require('../util/mapping/MappableObject')
const { buildFilterQuery, formatToISODateFormat } = require('../util/common')
const { buildDateFilterQuery, buildFilterQuery } = require('../util/common')
const { FundingAgreementMappings } = require('../util/mapping/Mappings')
const HttpStatus = require('http-status-codes')
const log = require('./logger')
const { isEmpty } = require('lodash')

function buildFilterQueryDates(queryParams) {
if (queryParams?.startDateThreshold) {
const startDateThreshold = queryParams.startDateThreshold
delete queryParams.startDateThreshold
return `ofm_start_date ge ${startDateThreshold} and `
}
if (queryParams?.startDateFrom && queryParams?.startDateTo) {
const startDateFrom = formatToISODateFormat(queryParams.startDateFrom)
const startDateTo = formatToISODateFormat(queryParams.startDateTo)
delete queryParams.startDateFrom
delete queryParams.startDateTo
return `ofm_start_date ge ${startDateFrom} and ofm_start_date le ${startDateTo} and `
}
return ''
}

async function getFundingAgreements(req, res) {
try {
const fundingAgreements = []
const operation = `ofm_fundings?$select=ofm_fundingid,ofm_funding_number,ofm_declaration,ofm_start_date,ofm_end_date,_ofm_application_value,_ofm_facility_value,statuscode,statecode&$filter=(${buildFilterQueryDates(
req?.query,
)}${buildFilterQuery(req?.query, FundingAgreementMappings)})`
let operation = 'ofm_fundings?$select=ofm_fundingid,ofm_funding_number,ofm_declaration,ofm_start_date,ofm_end_date,_ofm_application_value,_ofm_facility_value,statuscode,statecode'
if (req.query?.includeEA) {
operation += '&$expand=ofm_application($select=_ofm_expense_authority_value;$expand=ofm_expense_authority($select=ofm_first_name,ofm_last_name))'
}
const filter = `${buildDateFilterQuery(req?.query)}${buildFilterQuery(req?.query, FundingAgreementMappings)}`
operation += `&$filter=(${filter})`
const response = await getOperation(operation)
response?.value?.forEach((funding) => fundingAgreements.push(new MappableObjectForFront(funding, FundingAgreementMappings).toJSON()))
response?.value?.forEach((funding) => {
const fa = new MappableObjectForFront(funding, FundingAgreementMappings).toJSON()
if (req.query?.includeEA) {
const ea = funding.ofm_application?.ofm_expense_authority
fa.expenseAuthority = ea ? `${ea.ofm_first_name} ${ea.ofm_last_name}` : ''
}

fundingAgreements.push(fa)
})
if (isEmpty(fundingAgreements)) {
return res.status(HttpStatus.NO_CONTENT).json()
}
Expand Down
4 changes: 2 additions & 2 deletions backend/src/routes/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,15 @@ router.get('/:contactId/facilities', passport.authenticate('jwt', { session: fal
/**
* Create a new user/contact
*/
router.post('/create', passport.authenticate('jwt', { session: false }), isValidBackendToken, validatePermission(PERMISSIONS.MANAGE_USERS), [checkSchema(createUserSchema)], (req, res) => {
router.post('/create', passport.authenticate('jwt', { session: false }), isValidBackendToken, validatePermission(PERMISSIONS.MANAGE_USERS_EDIT), [checkSchema(createUserSchema)], (req, res) => {
validationResult(req).throw()
return createUser(req, res)
})

/**
* Update a user/contact
*/
router.post('/update', passport.authenticate('jwt', { session: false }), isValidBackendToken, validatePermission(PERMISSIONS.MANAGE_USERS), [checkSchema(updateUserSchema)], (req, res) => {
router.post('/update', passport.authenticate('jwt', { session: false }), isValidBackendToken, validatePermission(PERMISSIONS.MANAGE_USERS_EDIT), [checkSchema(updateUserSchema)], (req, res) => {
validationResult(req).throw()
return updateUser(req, res)
})
Expand Down
23 changes: 20 additions & 3 deletions backend/src/util/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
const { MappableObjectForBack } = require('../util/mapping/MappableObject')
const { isEmpty } = require('lodash')
const moment = require('moment')
const ISO_DATE_FORMAT = 'YYYY-MM-DD'

function buildFilterQuery(query, mapping) {
if (isEmpty(query)) return
Expand All @@ -13,16 +14,32 @@ function buildFilterQuery(query, mapping) {
return filterQuery
}

function buildDateFilterQuery(query) {
if (isEmpty(query)) return
let filterQuery = ''
if (query?.startDateFrom) {
const startDateFrom = formatToISODateFormat(query.startDateFrom)
delete query.startDateFrom
filterQuery += `ofm_start_date ge ${startDateFrom} and `
}
if (query?.startDateTo) {
const startDateTo = formatToISODateFormat(query.startDateTo)
delete query.startDateTo
filterQuery += `ofm_start_date le ${startDateTo} and `
}
return filterQuery
}

// Dynamics 365 expects OData queries on dates to be in ISO format
function formatToISODateFormat(dateString) {
if (moment(dateString, 'YYYY-MM-DD', true).isValid()) {
if (moment(dateString, ISO_DATE_FORMAT, true).isValid()) {
return dateString
}
const date = moment(dateString)
return date.isValid() ? date.format('YYYY-MM-DD') : 'Invalid date'
return date.isValid() ? date.format(ISO_DATE_FORMAT) : 'Invalid date'
}

module.exports = {
buildDateFilterQuery,
buildFilterQuery,
formatToISODateFormat,
}
2 changes: 2 additions & 0 deletions backend/src/util/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ const PERMISSIONS = Object.freeze({
UPDATE_ORG_FACILITY: 'Update Org/ facility information',
SUBMIT_CHANGE_REQUEST: 'Submit Change Request from AM',
MANAGE_USERS: 'Manage Users',
MANAGE_USERS_EDIT: 'Manage Users Edit',
MANAGE_USERS_VIEW: 'Manage Users View',

// Notifications and Messages
MANAGE_NOTIFICATIONS: 'Manage Notification and Messages',
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ const router = createRouter({
name: 'manage-users',
component: ManageUsersView,
meta: {
permission: PERMISSIONS.MANAGE_USERS,
permission: PERMISSIONS.MANAGE_USERS_VIEW,
},
},
],
Expand Down
21 changes: 6 additions & 15 deletions frontend/src/services/fundingAgreementService.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,27 +61,18 @@ export default {
}
},

async getFAByFacilityIdAndStartDateThreshold(facilityId, startDateThreshold) {
async getFAsByFacilityIdAndStartDate(facilityId, startDateFrom, startDateTo) {
try {
if (!facilityId && !startDateThreshold) return
const url = `${ApiRoutes.FUNDING_AGREEMENTS}?facilityId=${facilityId}&stateCode=0&startDateThreshold=${startDateThreshold}`
if (!facilityId && !startDateFrom) return
let url = `${ApiRoutes.FUNDING_AGREEMENTS}?facilityId=${facilityId}&stateCode=0&includeEA=true&startDateFrom=${startDateFrom}`
if (startDateTo) {
url += `&startDateTo=${startDateTo}`
}
const response = await ApiService.apiAxios.get(url)
return response?.data
} catch (error) {
console.log(`Failed to get the list of active funding agreements by facility id and start date threshold - ${error}`)
throw error
}
},

async getFAByFacilityIdAndStartFromEndDates(facilityId, startDateFrom, startDateTo) {
try {
if (!facilityId && !(startDateFrom || startDateTo)) return
const url = `${ApiRoutes.FUNDING_AGREEMENTS}?facilityId=${facilityId}&stateCode=0&startDateFrom=${startDateFrom}&startDateTo=${startDateTo}`
const response = await ApiService.apiAxios.get(url)
return response?.data
} catch (error) {
console.log(`Failed to get the list of active funding agreements by facility id and start dates (from/to) - ${error}`)
throw error
}
},
}
11 changes: 0 additions & 11 deletions frontend/src/services/organizationService.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,4 @@ export default {
throw error
}
},

async getUserPermissionsFacilities(organizationId) {
try {
if (!organizationId) return
const response = await ApiService.apiAxios.get(ApiRoutes.USER_PERMISSIONS_FACILITIES + '/' + organizationId)
return response.data
} catch (error) {
console.log('Failed to get the list of users by organization id: ' + organizationId, error)
throw error
}
},
}
20 changes: 20 additions & 0 deletions frontend/src/services/userService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import ApiService from '@/common/apiService'
import { ApiRoutes } from '@/utils/constants'

export default {
/**
* Gets all the users/contacts for the specified organizationId.
* @param {*} organizationId
* @returns The list of contacts
*/
async getOrganizationUsers(organizationId) {
try {
if (!organizationId) return
const response = await ApiService.apiAxios.get(`${ApiRoutes.USER_PERMISSIONS_FACILITIES}/${organizationId}`)
return response.data
} catch (error) {
console.log(`Failed to get the list of users by organization id: ${organizationId}`, error)
throw error
}
},
}
3 changes: 2 additions & 1 deletion frontend/src/utils/constants/permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export const PERMISSIONS = Object.freeze({
VIEW_ORG_FACILITY: 'View Org/Facility Information',
UPDATE_ORG_FACILITY: 'Update Org/ facility information',
SUBMIT_CHANGE_REQUEST: 'Submit Change Request from AM',
MANAGE_USERS: 'Manage Users',
MANAGE_USERS_EDIT: 'Manage Users Edit',
MANAGE_USERS_VIEW: 'Manage Users View',

// Notifications and Messages
MANAGE_NOTIFICATIONS: 'Manage Notification and Messages',
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/views/account-mgmt/AccountMgmtView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</template>
<router-link :to="{ name: 'manage-organization' }">Manage Organization/Facilities</router-link>
</v-list-item>
<v-list-item v-if="hasPermission(PERMISSIONS.MANAGE_USERS)">
<v-list-item v-if="hasPermission(PERMISSIONS.MANAGE_USERS_VIEW)">
<template v-slot:prepend>
<v-icon>mdi-account-group</v-icon>
</template>
Expand Down
25 changes: 13 additions & 12 deletions frontend/src/views/account-mgmt/ManageUsersView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<FacilityFilter :loading="loading" @facility-filter-changed="facilityFilterChanged" />
</v-col>
<v-col class="d-flex justify-end align-end pb-0">
<AppButton variant="text" @click="toggleDialog({})" :disabled="loading">
<AppButton variant="text" @click="toggleDialog({})" :disabled="loading" v-if="hasPermission(PERMISSIONS.MANAGE_USERS_EDIT)">
<v-icon left>mdi-plus</v-icon>
Add new user
</AppButton>
Expand All @@ -29,7 +29,7 @@
</template>

<template v-slot:item.actions="{ item }">
<AppButton @click.stop="toggleDialog(item)" variant="text">edit</AppButton>
<AppButton @click.stop="toggleDialog(item)" variant="text" v-if="hasPermission(PERMISSIONS.MANAGE_USERS_EDIT)">edit</AppButton>
</template>
<!-- Slots to translate specific column values into display values -->

Expand Down Expand Up @@ -87,20 +87,22 @@

<script>
import { mapState } from 'pinia'
import AppButton from '@/components/ui/AppButton.vue'
import DeactivateUserDialog from '@/components/account-mgmt/DeactivateUserDialog.vue'
import ManageUserDialog from '@/components/account-mgmt/ManageUserDialog.vue'
import FacilityFilter from '@/components/facilities/FacilityFilter.vue'
import AppBackButton from '@/components/ui/AppBackButton.vue'
import { useAuthStore } from '@/stores/auth'
import AppButton from '@/components/ui/AppButton.vue'
import alertMixin from '@/mixins/alertMixin'
import permissionsMixin from '@/mixins/permissionsMixin'
import UserService from '@/services/userService'
import { useAuthStore } from '@/stores/auth'
import { CRM_STATE_CODES, ROLES } from '@/utils/constants'
import ManageUserDialog from '@/components/account-mgmt/ManageUserDialog.vue'
import DeactivateUserDialog from '@/components/account-mgmt/DeactivateUserDialog.vue'
import FacilityFilter from '@/components/facilities/FacilityFilter.vue'
import OrganizationService from '@/services/organizationService'
export default {
name: 'ManageUsersView',
components: { AppButton, AppBackButton, ManageUserDialog, DeactivateUserDialog, FacilityFilter },
mixins: [alertMixin],
mixins: [alertMixin, permissionsMixin],
data() {
return {
loading: false,
Expand Down Expand Up @@ -159,7 +161,7 @@ export default {
},
showDeactivateUserButton(user) {
return !this.isDeactivatedUser(user) && !this.isSameUser(user) && user?.stateCode === CRM_STATE_CODES.ACTIVE
return !this.isDeactivatedUser(user) && !this.isSameUser(user) && user?.stateCode === CRM_STATE_CODES.ACTIVE && this.hasPermission(this.PERMISSIONS.MANAGE_USERS_EDIT)
},
/**
Expand All @@ -168,8 +170,7 @@ export default {
async getUsersAndFacilities() {
try {
this.loading = true
const res = await OrganizationService.getUserPermissionsFacilities(this.userInfo.organizationId)
this.usersAndFacilities = res.data
this.usersAndFacilities = await UserService.getOrganizationUsers(this.userInfo.organizationId)
} catch (error) {
this.setFailureAlert('Failed to get the list of users by organization id: ' + this.userInfo.organizationId, error)
} finally {
Expand Down
Loading

0 comments on commit 6388dfb

Please sign in to comment.