Skip to content

Commit

Permalink
ATS Search UI & API
Browse files Browse the repository at this point in the history
  • Loading branch information
sanjaytkbabu committed Oct 10, 2024
1 parent 79b0776 commit cb7327a
Show file tree
Hide file tree
Showing 36 changed files with 714 additions and 44 deletions.
2 changes: 2 additions & 0 deletions .github/environments/values.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ config:
FRONTEND_OPENSTREETMAP_APIPATH: https://tile.openstreetmap.org
FRONTEND_ORGBOOK_APIPATH: https://orgbook.gov.bc.ca/api/v4
SERVER_APIPATH: /api/v1
SERVER_ATS_APIPATH: https://i1api.nrs.gov.bc.ca/ats-api/v1
SERVER_ATS_TOKENURL: https://i1api.nrs.gov.bc.ca/oauth2/v1/oauth/token?grant_type=client_credentials&disableDeveloperFilter=true&scope=ATS_API.*
SERVER_BODYLIMIT: 30mb
SERVER_CHEFS_APIPATH: https://submit.digital.gov.bc.ca/app/api/v1
SERVER_CHES_APIPATH: https://ches-dev.api.gov.bc.ca/api/v1
Expand Down
2 changes: 2 additions & 0 deletions .github/environments/values.prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ config:
FRONTEND_OPENSTREETMAP_APIPATH: https://tile.openstreetmap.org
FRONTEND_ORGBOOK_APIPATH: https://orgbook.gov.bc.ca/api/v4
SERVER_APIPATH: /api/v1
SERVER_ATS_APIPATH: https://api.nrs.gov.bc.ca/ats-api/v1
SERVER_ATS_TOKENURL: https://api.nrs.gov.bc.ca/oauth2/v1/oauth/token?grant_type=client_credentials&disableDeveloperFilter=true&scope=ATS_API.*
SERVER_BODYLIMIT: 30mb
SERVER_CHEFS_APIPATH: https://submit.digital.gov.bc.ca/app/api/v1
SERVER_CHES_APIPATH: https://ches.api.gov.bc.ca/api/v1
Expand Down
2 changes: 2 additions & 0 deletions .github/environments/values.test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ config:
FRONTEND_OPENSTREETMAP_APIPATH: https://tile.openstreetmap.org
FRONTEND_ORGBOOK_APIPATH: https://orgbook.gov.bc.ca/api/v4
SERVER_APIPATH: /api/v1
SERVER_ATS_APIPATH: https://t1api.nrs.gov.bc.ca/ats-api/v1
SERVER_ATS_TOKENURL: https://t1api.nrs.gov.bc.ca/oauth2/v1/oauth/token?grant_type=client_credentials&disableDeveloperFilter=true&scope=ATS_API.*
SERVER_BODYLIMIT: 30mb
SERVER_CHEFS_APIPATH: https://submit.digital.gov.bc.ca/app/api/v1
SERVER_CHES_APIPATH: https://ches-test.api.gov.bc.ca/api/v1
Expand Down
6 changes: 6 additions & 0 deletions app/config/custom-environment-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@
},
"server": {
"apiPath": "SERVER_APIPATH",
"ats": {
"apiPath": "SERVER_ATS_APIPATH",
"clientId": "SERVER_ATS_CLIENTID",
"clientSecret": "SERVER_ATS_CLIENTSECRET",
"tokenUrl": "SERVER_ATS_TOKENURL"
},
"bodyLimit": "SERVER_BODYLIMIT",
"ches": {
"apiPath": "SERVER_CHES_APIPATH",
Expand Down
22 changes: 22 additions & 0 deletions app/src/controllers/ats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { atsService } from '../services';

import type { NextFunction, Request, Response } from 'express';
import type { ATSUserSearchParameters } from '../types';

const controller = {
searchATSUsers: async (
req: Request<never, never, never, ATSUserSearchParameters>,
res: Response,
next: NextFunction
) => {
try {
const response = await atsService.searchATSUsers(req.query);

res.status(response.status).json(response.data);
} catch (e: unknown) {
next(e);
}
}
};

export default controller;
1 change: 1 addition & 0 deletions app/src/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { default as accessRequestController } from './accessRequest';
export { default as activityController } from './activity';
export { default as atsController } from './ats';
export { default as documentController } from './document';
export { default as enquiryController } from './enquiry';
export { default as noteController } from './note';
Expand Down
249 changes: 249 additions & 0 deletions app/src/db/migrations/20241002000000_011-ats-integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
/* eslint-disable max-len */
import type { Knex } from 'knex';

import { Action, GroupName, Initiative, Resource } from '../../utils/enums/application';

const resources = [
{
name: Resource.ATS
}
];

const actions = [
{
name: Action.CREATE
},
{
name: Action.READ
},
{
name: Action.UPDATE
},
{
name: Action.DELETE
}
];

export async function up(knex: Knex): Promise<void> {
return (
Promise.resolve()
// Add ats ats_client_number to enquiry table
.then(() =>
knex.schema.alterTable('enquiry', (table) => {
table.text('ats_client_number');
})
)
// Drop added_to_ats from submission table as it is not needed anymore with the new link ats ui
.then(() =>
knex.schema.alterTable('submission', (table) => {
table.dropColumn('added_to_ats');
})
)

.then(() => {
return knex('yars.resource').insert(resources);
})

.then(() => {
/*
* Add policies
*/

const items = [];
for (const resource of resources) {
for (const action of actions) {
items.push({
resource_id: knex('yars.resource').where({ name: resource.name }).select('resource_id'),
action_id: knex('yars.action').where({ name: action.name }).select('action_id')
});
}
}

return knex('yars.policy').insert(items);
})

.then(async () => {
/*
* Add roles
*/

const items: Array<{ name: string; description: string }> = [];

const addRolesForResource = (resourceName: string) => {
items.push(
{
name: `${resourceName.toUpperCase()}_CREATOR`,
description: `Can create ${resourceName.toLowerCase()}s`
},
{
name: `${resourceName.toUpperCase()}_VIEWER`,
description: `Can view ${resourceName.toLowerCase()}s`
},
{
name: `${resourceName.toUpperCase()}_EDITOR`,
description: `Can edit ${resourceName.toLowerCase()}s`
}
);
};

for (const resource of resources) {
addRolesForResource(resource.name);
}

return knex('yars.role').insert(items);
})

.then(async () => {
/*
* Add role to policy mappings
*/

const policies = await knex
.select('p.policy_id', 'r.name as resource_name', 'a.name as action_name')
.from({ p: 'yars.policy' })
.innerJoin({ r: 'yars.resource' }, 'p.resource_id', '=', 'r.resource_id')
.innerJoin({ a: 'yars.action' }, 'p.action_id', '=', 'a.action_id');

const items: Array<{ role_id: number; policy_id: number }> = [];

const addRolePolicies = async (resourceName: string) => {
const creatorId = await knex('yars.role')
.where({ name: `${resourceName.toUpperCase()}_CREATOR` })
.select('role_id');
const viewerId = await knex('yars.role')
.where({ name: `${resourceName.toUpperCase()}_VIEWER` })
.select('role_id');
const editorId = await knex('yars.role')
.where({ name: `${resourceName.toUpperCase()}_EDITOR` })
.select('role_id');

const resourcePolicies = policies.filter((x) => x.resource_name === resourceName);
items.push(
{
role_id: creatorId[0].role_id,
policy_id: resourcePolicies.find((x) => x.action_name == Action.CREATE).policy_id
},
{
role_id: viewerId[0].role_id,
policy_id: resourcePolicies.find((x) => x.action_name == Action.READ).policy_id
},
{
role_id: editorId[0].role_id,
policy_id: resourcePolicies.find((x) => x.action_name == Action.UPDATE).policy_id
},

{
role_id: editorId[0].role_id,
policy_id: resourcePolicies.find((x) => x.action_name == Action.DELETE).policy_id
}
);
};

await addRolePolicies(Resource.ATS);

return knex('yars.role_policy').insert(items);
})

.then(async () => {
/*
* Add group to role mappings
*/

const housing_id = knex('initiative')
.where({
code: Initiative.HOUSING
})
.select('initiative_id');

const navigator_group_id = await knex('yars.group')
.where({ initiative_id: housing_id, name: GroupName.NAVIGATOR })
.select('group_id');

const navigator_read_group_id = await knex('yars.group')
.where({ initiative_id: housing_id, name: GroupName.NAVIGATOR_READ_ONLY })
.select('group_id');

const superviser_group_id = await knex('yars.group')
.where({ initiative_id: housing_id, name: GroupName.SUPERVISOR })
.select('group_id');

const admin_group_id = await knex('yars.group')
.where({ initiative_id: housing_id, name: GroupName.ADMIN })
.select('group_id');

const items: Array<{ group_id: number; role_id: number }> = [];

const addResourceRoles = async (group_id: number, resourceName: Resource, actionNames: Array<Action>) => {
if (actionNames.includes(Action.CREATE)) {
items.push({
group_id: group_id,
role_id: (
await knex('yars.role')
.where({ name: `${resourceName}_CREATOR` })
.select('role_id')
)[0].role_id
});
}

if (actionNames.includes(Action.READ)) {
items.push({
group_id: group_id,
role_id: (
await knex('yars.role')
.where({ name: `${resourceName}_VIEWER` })
.select('role_id')
)[0].role_id
});
}

if (actionNames.includes(Action.UPDATE) || actionNames.includes(Action.DELETE)) {
items.push({
group_id: group_id,
role_id: (
await knex('yars.role')
.where({ name: `${resourceName}_EDITOR` })
.select('role_id')
)[0].role_id
});
}
};

// Note: Only UPDATE or DELETE is required to be given EDITOR role, don't include both
// prettier-ignore
{
// Add all navigator role mappings
await addResourceRoles(navigator_group_id[0].group_id, Resource.ATS, [Action.CREATE, Action.READ]);

// Add all navigator read only role mappings
await addResourceRoles(navigator_read_group_id[0].group_id, Resource.ATS, [Action.READ]);


// Add all supervisor role mappings
await addResourceRoles(superviser_group_id[0].group_id, Resource.ATS, [Action.CREATE, Action.READ]);

// Add all admin role mappings
await addResourceRoles(admin_group_id[0].group_id, Resource.ATS, [Action.READ]);

}
return knex('yars.group_role').insert(items);
})
);
}

export async function down(knex: Knex): Promise<void> {
return (
Promise.resolve()
// Drop client_id from enquiry table
.then(() =>
knex.schema.alterTable('enquiry', (table) => {
table.dropColumn('ats_client_number');
})
)
// Add added_to_ats to submission table
.then(() =>
knex.schema.alterTable('submission', (table) => {
table.boolean('added_to_ats').notNullable().defaultTo(false);
})
)
);
}
2 changes: 2 additions & 0 deletions app/src/db/models/enquiry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default {
enquiry_id: input.enquiryId,
activity_id: input.activityId,
assigned_user_id: input.assignedUserId,
ats_client_number: input.atsClientNumber,
enquiry_type: input.enquiryType,
submitted_at: new Date(input.submittedAt ?? Date.now()),
submitted_by: input.submittedBy,
Expand Down Expand Up @@ -47,6 +48,7 @@ export default {
enquiryId: input.enquiry_id,
activityId: input.activity_id,
assignedUserId: input.assigned_user_id,
atsClientNumber: input.ats_client_number,
enquiryType: input.enquiry_type,
submittedAt: input.submitted_at?.toISOString() as string,
submittedBy: input.submitted_by,
Expand Down
2 changes: 0 additions & 2 deletions app/src/db/models/submission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ export default {
related_permits: input.relatedPermits,
ast_notes: input.astNotes,
ast_updated: input.astUpdated,
added_to_ats: input.addedToATS,
ats_client_number: input.atsClientNumber,
ltsa_completed: input.ltsaCompleted,
bc_online_completed: input.bcOnlineCompleted,
Expand Down Expand Up @@ -103,7 +102,6 @@ export default {
relatedPermits: input.related_permits,
astNotes: input.ast_notes,
astUpdated: input.ast_updated,
addedToATS: input.added_to_ats,
atsClientNumber: input.ats_client_number,
ltsaCompleted: input.ltsa_completed,
bcOnlineCompleted: input.bc_online_completed,
Expand Down
2 changes: 1 addition & 1 deletion app/src/db/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,6 @@ model submission {
related_permits String?
ast_notes String?
ast_updated Boolean @default(false)
added_to_ats Boolean @default(false)
ats_client_number String?
ltsa_completed Boolean @default(false)
bc_online_completed Boolean @default(false)
Expand Down Expand Up @@ -267,6 +266,7 @@ model enquiry {
created_at DateTime? @default(now()) @db.Timestamptz(6)
updated_by String?
updated_at DateTime? @db.Timestamptz(6)
ats_client_number String?
activity activity @relation(fields: [activity_id], references: [activity_id], onDelete: Cascade, map: "enquiry_activity_id_foreign")
user user? @relation(fields: [assigned_user_id], references: [user_id], onDelete: Cascade, map: "enquiry_assigned_user_id_foreign")
Expand Down
24 changes: 24 additions & 0 deletions app/src/routes/v1/ats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import express from 'express';

import { atsController } from '../../controllers';
import { hasAuthorization } from '../../middleware/authorization';
import { requireSomeAuth } from '../../middleware/requireSomeAuth';
import { requireSomeGroup } from '../../middleware/requireSomeGroup';
import { Action, Resource } from '../../utils/enums/application';

import type { NextFunction, Request, Response } from 'express';
import type { ATSUserSearchParameters } from '../../types';

const router = express.Router();
router.use(requireSomeAuth);
router.use(requireSomeGroup);

router.get(
'/clients',
hasAuthorization(Resource.ATS, Action.READ),
(req: Request<never, never, never, ATSUserSearchParameters>, res: Response, next: NextFunction): void => {
atsController.searchATSUsers(req, res, next);
}
);

export default router;
Loading

0 comments on commit cb7327a

Please sign in to comment.