diff --git a/express-api/Dockerfile b/express-api/Dockerfile index 1904c2b2f..cc2827ddf 100644 --- a/express-api/Dockerfile +++ b/express-api/Dockerfile @@ -16,9 +16,6 @@ COPY . . # Install packages. Needed for build process. RUN npm i -# Generate swagger-output -RUN npm run swagger - # Compile to JavaScript build RUN npm run build diff --git a/express-api/package.json b/express-api/package.json index 7d0acb33e..be67e144b 100644 --- a/express-api/package.json +++ b/express-api/package.json @@ -13,7 +13,6 @@ "test": "cross-env DOTENV_CONFIG_PATH=../.env jest", "test:unit": "npm run test -- --testPathPattern=/tests/unit", "test:integration": "npm run test -- --testPathPattern=/tests/integration", - "swagger": "node ./src/swagger/swagger.mjs", "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli", "migration": "sh ./src/typeorm/utilities/helperScripts/migrationScript.sh" }, @@ -58,6 +57,7 @@ "@types/node-cron": "3.0.11", "@types/nunjucks": "3.2.6", "@types/supertest": "6.0.2", + "@types/swagger-jsdoc": "6.0.4", "@types/swagger-ui-express": "4.1.6", "@typescript-eslint/eslint-plugin": "8.1.0", "@typescript-eslint/parser": "8.1.0", @@ -70,6 +70,7 @@ "prettier": "3.3.0", "supertest": "7.0.0", "swagger-autogen": "2.23.7", + "swagger-jsdoc": "6.2.8", "ts-jest": "29.2.0", "tsc-alias": "1.8.8", "typescript": "5.5.2" diff --git a/express-api/src/controllers/administrativeAreas/administrativeAreasController.ts b/express-api/src/controllers/administrativeAreas/administrativeAreasController.ts index 5829d35e5..7b4773758 100644 --- a/express-api/src/controllers/administrativeAreas/administrativeAreasController.ts +++ b/express-api/src/controllers/administrativeAreas/administrativeAreasController.ts @@ -15,17 +15,11 @@ import userServices from '@/services/users/usersServices'; * @returns {Response} A 200 status with a list of administrative areas. */ export const getAdministrativeAreas = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Administrative Areas - Admin'] - * #swagger.description = 'Returns a paged list of administrative areas from the datasource.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ const ssoUser = req.user; const filter = AdministrativeAreaFilterSchema.safeParse(req.query); if (filter.success) { const adminAreas = await administrativeAreasServices.getAdministrativeAreas(filter.data); + // TODO: Do we still need this condition? Few fields are trimmed since moving to view. if (!ssoUser.hasRoles([Roles.ADMIN])) { const trimmed = AdministrativeAreaPublicResponseSchema.array().parse(adminAreas.data); return res.status(200).send({ @@ -35,7 +29,7 @@ export const getAdministrativeAreas = async (req: Request, res: Response) => { } return res.status(200).send(adminAreas); } else { - return res.status(400).send('Could not parse filter.'); + return res.status(400).send(filter.error); } }; @@ -46,13 +40,6 @@ export const getAdministrativeAreas = async (req: Request, res: Response) => { * @returns {Response} A 201 status and response with the added administrative area. */ export const addAdministrativeArea = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Administrative Areas - Admin'] - * #swagger.description = 'Add a new administrative area to the datasource.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ const user = await userServices.getUser((req.user as SSOUser).preferred_username); const addBody = { ...req.body, CreatedById: user.Id }; const response = await administrativeAreasServices.addAdministrativeArea(addBody); @@ -66,14 +53,6 @@ export const addAdministrativeArea = async (req: Request, res: Response) => { * @returns {Response} A 200 status and the administrative area data. */ export const getAdministrativeAreaById = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Administrative Areas - Admin'] - * #swagger.description = 'Returns an administrative area that matches the supplied ID.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ - const id = Number(req.params.id); const adminArea = await administrativeAreasServices.getAdministrativeAreaById(id); return res.status(200).send(adminArea); @@ -86,14 +65,6 @@ export const getAdministrativeAreaById = async (req: Request, res: Response) => * @returns {Response} A 200 status and the administrative area data. */ export const updateAdministrativeAreaById = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Administrative Areas - Admin'] - * #swagger.description = 'Updates an administrative area that matches the supplied ID.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ - const id = req.params.id; if (id != req.body.Id) { return res.status(400).send('Id mismatched or invalid.'); diff --git a/express-api/src/controllers/agencies/agenciesController.ts b/express-api/src/controllers/agencies/agenciesController.ts index d4fb216bf..8bbeb2acb 100644 --- a/express-api/src/controllers/agencies/agenciesController.ts +++ b/express-api/src/controllers/agencies/agenciesController.ts @@ -14,13 +14,6 @@ import { Agency } from '@/typeorm/Entities/Agency'; * @returns {Response} A 200 status with a list of agencies. */ export const getAgencies = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Agencies - Admin'] - * #swagger.description = 'Gets a paged list of agencies.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ const ssoUser = req.user; const filter = AgencyFilterSchema.safeParse(req.query); if (filter.success) { @@ -45,14 +38,6 @@ export const getAgencies = async (req: Request, res: Response) => { * @returns {Response} A 201 status and the data of the agency added. */ export const addAgency = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Agencies - Admin'] - * #swagger.description = 'Adds a new agency to the datasource.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ - const user = await userServices.getUser((req.user as SSOUser).preferred_username); const agency = await agencyService.addAgency({ ...req.body, CreatedById: user.Id }); @@ -66,14 +51,6 @@ export const addAgency = async (req: Request, res: Response) => { * @returns {Response} A 200 status and the agency data. */ export const getAgencyById = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Agencies - Admin'] - * #swagger.description = 'Returns an agency that matches the supplied ID.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ - const agency = await agencyService.getAgencyById(parseInt(req.params.id)); if (!agency) { return res.status(404).send('Agency does not exist.'); @@ -88,13 +65,6 @@ export const getAgencyById = async (req: Request, res: Response) => { * @returns {Response} A 200 status and the agency data. */ export const updateAgencyById = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Agencies - Admin'] - * #swagger.description = 'Updates an agency that matches the supplied ID.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ const idParse = z.string().safeParse(req.params.id); if (!idParse.success) { return res.status(400).send(idParse); @@ -119,13 +89,6 @@ export const updateAgencyById = async (req: Request, res: Response) => { * @returns {Response} A 204 status indicating successful deletion. */ export const deleteAgencyById = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Agencies - Admin'] - * #swagger.description = 'Deletes an agency that matches the supplied ID.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ const idParse = z.string().safeParse(req.params.id); if (!idParse.success) { return res.status(400).send(idParse); diff --git a/express-api/src/controllers/buildings/buildingsController.ts b/express-api/src/controllers/buildings/buildingsController.ts index d9db1a5e9..89a28302b 100644 --- a/express-api/src/controllers/buildings/buildingsController.ts +++ b/express-api/src/controllers/buildings/buildingsController.ts @@ -41,13 +41,6 @@ export const getBuildings = async (req: Request, res: Response) => { * @returns {Response} A 200 status with a response body containing building data. */ export const getBuilding = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['building'] - * #swagger.description = 'Get the building from the data source if the user is permitted.' - * #swagger.security = [{ - * "bearerAuth": [] - * }] - */ const buildingId = Number(req.params.buildingId); if (isNaN(buildingId)) { return res.status(400).send('Building Id is invalid.'); @@ -73,13 +66,6 @@ export const getBuilding = async (req: Request, res: Response) => { * @returns {Response} A 200 status with a response body containing building data. */ export const updateBuilding = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['building'] - * #swagger.description = 'Updates the building from the data source if the user is permitted.' - * #swagger.security = [{ - * "bearerAuth": [] - * }] - */ const buildingId = Number(req.params.buildingId); if (isNaN(buildingId) || buildingId !== req.body.Id) { return res.status(400).send('Building ID was invalid or mismatched with body.'); @@ -97,13 +83,6 @@ export const updateBuilding = async (req: Request, res: Response) => { * @returns {Response} A 200 status with a response body containing building data. */ export const deleteBuilding = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['building'] - * #swagger.description = 'Deletes the building from the data source if the user is permitted.' - * #swagger.security = [{ - * "bearerAuth": [] - * }] - */ const buildingId = Number(req.params.buildingId); if (isNaN(buildingId)) { return res.status(400).send('Building ID was invalid.'); @@ -123,13 +102,6 @@ export const deleteBuilding = async (req: Request, res: Response) => { * Note: the original implementation returns 200, but as a resource is created 201 is better. */ export const addBuilding = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['building'] - * #swagger.description = 'Creates a new building in the datasource.' - * #swagger.security = [{ - * "bearerAuth": [] - * }] - */ const user = await userServices.getUser((req.user as SSOUser).preferred_username); const createBody: Building = { ...req.body, CreatedById: user.Id }; createBody.Evaluations = createBody.Evaluations?.map((evaluation) => ({ diff --git a/express-api/src/controllers/healthController.ts b/express-api/src/controllers/healthController.ts index 0cef3d333..cd8df12ba 100644 --- a/express-api/src/controllers/healthController.ts +++ b/express-api/src/controllers/healthController.ts @@ -7,9 +7,5 @@ import { Request, Response } from 'express'; * @returns {Response} A 200 status indicating API is healthy and running */ export const healthCheck = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Health'] - * #swagger.description = 'Returns a 200 (OK) status if API is reached.' - */ return res.status(200).send('/health endpoint reached. API running.'); }; diff --git a/express-api/src/controllers/lookup/lookupController.ts b/express-api/src/controllers/lookup/lookupController.ts index 3f1253911..ed69c82fa 100644 --- a/express-api/src/controllers/lookup/lookupController.ts +++ b/express-api/src/controllers/lookup/lookupController.ts @@ -34,13 +34,6 @@ import getConfig from '@/constants/config'; * @returns {Response} A 200 status and a list of property classifications. */ export const lookupPropertyClassifications = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Lookup'] - * #swagger.description = 'Get all property classification entries.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ const classifications = await AppDataSource.getRepository(PropertyClassification).find(); const filtered = classifications.filter((c) => !c.IsDisabled); const parsed = ClassificationPublicResponseSchema.array().safeParse(filtered); @@ -52,13 +45,6 @@ export const lookupPropertyClassifications = async (req: Request, res: Response) }; export const lookupBuildingPredominateUse = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Lookup'] - * #swagger.description = 'Get all predomanite uses entries.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ const uses = await AppDataSource.getRepository(BuildingPredominateUse).find(); const filtered = uses.filter((u) => !u.IsDisabled); const parsed = PredominateUsePublicResponseSchema.array().safeParse(filtered); @@ -70,13 +56,6 @@ export const lookupBuildingPredominateUse = async (req: Request, res: Response) }; export const lookupBuildingConstructionType = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Lookup'] - * #swagger.description = 'Get all building construction type entries.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ const uses = await AppDataSource.getRepository(BuildingConstructionType).find(); const filtered = uses.filter((u) => !u.IsDisabled); const parsed = BuildingConstructionPublicResponseSchema.array().safeParse(filtered); diff --git a/express-api/src/controllers/ltsa/ltsaController.ts b/express-api/src/controllers/ltsa/ltsaController.ts index fddd67b9d..eb646eabe 100644 --- a/express-api/src/controllers/ltsa/ltsaController.ts +++ b/express-api/src/controllers/ltsa/ltsaController.ts @@ -8,13 +8,6 @@ import ltsaService from '@/services/ltsa/ltsaServices'; * @returns {Response} A 200 status with LTSA order information. */ export const getLTSA = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['LTSA'] - * #swagger.description = 'Returns property information from LTSA.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ const pid = req.query.pid as string; const getLandTitleInfo = await ltsaService.processLTSARequest(pid); return res.status(200).send(getLandTitleInfo); diff --git a/express-api/src/controllers/notifications/notificationsController.ts b/express-api/src/controllers/notifications/notificationsController.ts index d4da5147a..7b6f2f8ec 100644 --- a/express-api/src/controllers/notifications/notificationsController.ts +++ b/express-api/src/controllers/notifications/notificationsController.ts @@ -7,6 +7,7 @@ import { Request, Response } from 'express'; import { DisposalNotificationFilterSchema } from './notificationsSchema'; import { isAdmin, isAuditor } from '@/utilities/authorizationChecks'; import projectServices from '@/services/projects/projectsServices'; +import { Roles } from '@/constants/roles'; import logger from '@/utilities/winstonLogger'; /** @@ -31,7 +32,9 @@ export const getNotificationsByProjectId = async (req: Request, res: Response) = const project = await projectServices.getProjectById(filterResult.projectId); if (!usersAgencies.includes(project.AgencyId)) { - return res.status(403).send({ message: 'User is not authorized to access this endpoint.' }); + return res + .status(403) + .send({ message: 'User cannot access project outside their agencies.' }); } } @@ -53,15 +56,14 @@ export const getNotificationsByProjectId = async (req: Request, res: Response) = }; export const resendNotificationById = async (req: Request, res: Response) => { + const kcUser = req.user; + if (!kcUser.hasRoles([Roles.ADMIN])) + return res.status(403).send('User lacks permissions to resend notification.'); const id = Number(req.params.id); const notification = await notificationServices.getNotificationById(id); if (!notification) { return res.status(404).send('Notification not found.'); } - const kcUser = req.user; - if (!isAdmin(kcUser)) { - return res.status(403).send({ message: 'User is not authorized to access this endpoint.' }); - } const resultantNotification = await notificationServices.sendNotification(notification, kcUser); const user = await userServices.getUser(kcUser.preferred_username); const updatedNotification = await notificationServices.updateNotificationStatus( @@ -72,16 +74,19 @@ export const resendNotificationById = async (req: Request, res: Response) => { }; export const cancelNotificationById = async (req: Request, res: Response) => { + const kcUser = req.user; + if (!kcUser.hasRoles([Roles.ADMIN])) + return res.status(403).send('User lacks permissions to cancel notification.'); const id = Number(req.params.id); const notification = await notificationServices.getNotificationById(id); if (!notification) { return res.status(404).send('Notification not found.'); } - const kcUser = req.user; - if (!isAdmin(kcUser)) { - return res.status(403).send({ message: 'User is not authorized to access this endpoint.' }); - } - const resultantNotification = await notificationServices.cancelNotificationById(notification.Id); + const user = await userServices.getUser(kcUser.preferred_username); + const resultantNotification = await notificationServices.cancelNotificationById( + notification.Id, + user, + ); if (resultantNotification.Status !== NotificationStatus.Cancelled) { return res.status(400).send(resultantNotification); } diff --git a/express-api/src/controllers/notifications/notificationsSchema.ts b/express-api/src/controllers/notifications/notificationsSchema.ts index 14d9b482f..bbec0c4b3 100644 --- a/express-api/src/controllers/notifications/notificationsSchema.ts +++ b/express-api/src/controllers/notifications/notificationsSchema.ts @@ -35,8 +35,8 @@ export const NotificationResponseSchema = z.object({ }); export const DisposalNotificationFilterSchema = z.object({ - page: z.number().optional(), - quantity: z.number().optional(), + page: z.coerce.number().optional(), + quantity: z.coerce.number().optional(), sort: z.array(z.string()).optional(), projectNumber: z.string().optional(), projectId: z.coerce.number(), diff --git a/express-api/src/controllers/parcels/parcelsController.ts b/express-api/src/controllers/parcels/parcelsController.ts index cad83f3b0..265605e5b 100644 --- a/express-api/src/controllers/parcels/parcelsController.ts +++ b/express-api/src/controllers/parcels/parcelsController.ts @@ -14,13 +14,6 @@ import { checkUserAgencyPermission, isAdmin, isAuditor } from '@/utilities/autho * @returns {Response} A 200 status with a response body containing parcel data. */ export const getParcel = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['parcels'] - * #swagger.description = 'Get the parcel from the data source if the user is permitted.' - * #swagger.security = [{ - * "bearerAuth": [] - * }] - */ const parcelId = Number(req.params.parcelId); if (isNaN(parcelId)) { return res.status(400).send('Parcel ID was invalid.'); @@ -45,13 +38,6 @@ export const getParcel = async (req: Request, res: Response) => { * @returns {Response} A 200 status with a response body containing parcel data. */ export const updateParcel = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['parcels'] - * #swagger.description = 'Updates the parcel from the data source if the user is permitted.' - * #swagger.security = [{ - * "bearerAuth": [] - * }] - */ const parcelId = Number(req.params.parcelId); if (isNaN(parcelId) || parcelId !== req.body.Id) { return res.status(400).send('Parcel ID was invalid or mismatched with body.'); @@ -72,13 +58,6 @@ export const updateParcel = async (req: Request, res: Response) => { * @returns {Response} A 200 status with a response body containing parcel data. */ export const deleteParcel = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['parcels'] - * #swagger.description = 'Deletes the parcel from the data source if the user is permitted.' - * #swagger.security = [{ - * "bearerAuth": [] - * }] - */ const parcelId = Number(req.params.parcelId); if (isNaN(parcelId)) { return res.status(400).send('Parcel ID was invalid.'); @@ -125,13 +104,6 @@ export const getParcels = async (req: Request, res: Response) => { * Note: the original implementation returns 200, but as a resource is created 201 is better. */ export const addParcel = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['parcels'] - * #swagger.description = 'Creates a new parcel in the datasource.' - * #swagger.security = [{ - * "bearerAuth": [] - * }] - */ const user = await userServices.getUser((req.user as SSOUser).preferred_username); const parcel: Parcel = { ...req.body, CreatedById: user.Id }; parcel.Evaluations = parcel.Evaluations?.map((evaluation) => ({ diff --git a/express-api/src/controllers/projects/projectsController.ts b/express-api/src/controllers/projects/projectsController.ts index 18d550798..09711d6c2 100644 --- a/express-api/src/controllers/projects/projectsController.ts +++ b/express-api/src/controllers/projects/projectsController.ts @@ -16,13 +16,6 @@ import notificationServices from '@/services/notifications/notificationServices' * @returns {Response} A 200 status with the requested project. */ export const getDisposalProject = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Projects'] - * #swagger.description = 'Get disposal project by either the numeric id or projectNumber.' - * #swagger.security = [{ - * "bearerAuth" : [] - * }] - */ // admins are permitted to view any project const permittedRoles = [Roles.ADMIN]; const user = req.user as SSOUser; @@ -53,14 +46,6 @@ export const getDisposalProject = async (req: Request, res: Response) => { * @returns {Response} A 200 status with the updated project. */ export const updateDisposalProject = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Projects'] - * #swagger.description = 'Update the project for the specified id.' - * #swagger.security = [{ - * "bearerAuth" : [] - * }] - */ - // Only admins can edit projects if (!isAdmin(req.user)) { return res.status(403).send('Projects only editable by Administrator role.'); @@ -94,13 +79,6 @@ export const updateDisposalProject = async (req: Request, res: Response) => { * @returns {Response} A 200 status with the deleted project. */ export const deleteDisposalProject = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Projects'] - * #swagger.description = 'Delete the project for the specified id.' - * #swagger.security = [{ - * "bearerAuth" : [] - * }] - */ // Only admins can delete projects if (!isAdmin(req.user)) { return res.status(403).send('Projects can only be deleted by Administrator role.'); diff --git a/express-api/src/controllers/properties/propertiesController.ts b/express-api/src/controllers/properties/propertiesController.ts index 35a3f4719..e02fdaf28 100644 --- a/express-api/src/controllers/properties/propertiesController.ts +++ b/express-api/src/controllers/properties/propertiesController.ts @@ -25,13 +25,6 @@ import { Roles } from '@/constants/roles'; * @returns {Response} A 200 status with a list of properties. */ export const getPropertiesFuzzySearch = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Properties'] - * #swagger.description = 'Returns a list of fuzzy searched properties.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ const keyword = String(req.query.keyword); const take = req.query.take ? Number(req.query.take) : undefined; const kcUser = req.user; @@ -67,13 +60,6 @@ export const getLinkedProjects = async (req: Request, res: Response) => { * @returns {Response} A 200 status with a list of property geolocation information. */ export const getPropertiesForMap = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Properties'] - * #swagger.description = 'Returns a list of all property geolocation information.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ // parse for filter const filter = MapFilterSchema.safeParse(req.query); if (filter.success == false) { diff --git a/express-api/src/controllers/properties/propertiesSchema.ts b/express-api/src/controllers/properties/propertiesSchema.ts index 2a5aa5949..3edd14c95 100644 --- a/express-api/src/controllers/properties/propertiesSchema.ts +++ b/express-api/src/controllers/properties/propertiesSchema.ts @@ -39,7 +39,6 @@ export type MapFilter = z.infer; export const PropertyUnionFilterSchema = z.object({ pid: z.string().optional(), pin: z.string().optional(), - status: z.string().optional(), classification: z.string().optional(), agency: z.string().optional(), agencyIds: z.array(z.number().int().nonnegative()).optional(), diff --git a/express-api/src/controllers/reports/reportsController.ts b/express-api/src/controllers/reports/reportsController.ts index 8c6a9dcaf..df2c3444a 100644 --- a/express-api/src/controllers/reports/reportsController.ts +++ b/express-api/src/controllers/reports/reportsController.ts @@ -12,13 +12,6 @@ import getConfig from '@/constants/config'; * @returns Response (200) that includes the error information and the CHES response. */ export const submitErrorReport = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Reports'] - * #swagger.description = 'Accepts an error report from the frontend and sends an email to administrators.' - * #swagger.security = [{ - * "bearerAuth" : [] - * }] - */ const info: ErrorReport = req.body; logger.info(info); const errorParse = errorReportSchema.safeParse(info); diff --git a/express-api/src/controllers/roles/rolesController.ts b/express-api/src/controllers/roles/rolesController.ts index 91b51b62d..c4dc1c233 100644 --- a/express-api/src/controllers/roles/rolesController.ts +++ b/express-api/src/controllers/roles/rolesController.ts @@ -3,6 +3,14 @@ import rolesServices from '@/services/roles/rolesServices'; import { RolesFilter, RolesFilterSchema } from '@/controllers/roles/rolesSchema'; import { UUID } from 'crypto'; +/** + * NOTE + * The routes for /roles have been removed, so these controllers are not accessible from outside the API. + * Use the rolesServices within the API to perform role-related changes. + * + * Keeping these intact for now in case they are needed in the future. + */ + /** * @description Gets a paged list of roles. * @param {Request} req Incoming request @@ -10,16 +18,9 @@ import { UUID } from 'crypto'; * @returns {Response} A 200 status with a list of roles. */ export const getRoles = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Roles - Admin'] - * #swagger.description = 'Gets a paged list of roles.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ const filter = RolesFilterSchema.safeParse(req.query); if (filter.success) { - const roles = await rolesServices.getRoles(filter.data as RolesFilter); //await rolesServices.getRoles(filter.data as RolesFilter); + const roles = await rolesServices.getRoles(filter.data as RolesFilter); return res.status(200).send(roles); } else { return res.status(400).send('Could not parse filter.'); @@ -33,13 +34,6 @@ export const getRoles = async (req: Request, res: Response) => { * @returns {Response} A 201 status and the data of the role added. */ export const addRole = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Roles - Admin'] - * #swagger.description = 'Adds a new role to the datasource.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ const role = await rolesServices.addRole(req.body); return res.status(201).send(role); }; @@ -51,13 +45,6 @@ export const addRole = async (req: Request, res: Response) => { * @returns {Response} A 200 status and the role data. */ export const getRoleById = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Roles - Admin'] - * #swagger.description = 'Returns an role that matches the supplied ID.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ const id = req.params.id; const role = rolesServices.getRoleById(id as UUID); if (!role) { @@ -74,14 +61,6 @@ export const getRoleById = async (req: Request, res: Response) => { * @returns {Response} A 200 status and the role data. */ export const updateRoleById = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Roles - Admin'] - * #swagger.description = 'Updates an role that matches the supplied ID.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ - const id = req.params.id; if (id != req.body.Id) { return res.status(400).send('Request param id did not match request body id.'); @@ -98,13 +77,6 @@ export const updateRoleById = async (req: Request, res: Response) => { * @returns {Response} A 204 status indicating successful deletion. */ export const deleteRoleById = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Roles - Admin'] - * #swagger.description = 'Deletes an role that matches the supplied ID.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ const id = req.params.id; if (id != req.body.Id) { return res.status(400).send('Request param id did not match request body id.'); diff --git a/express-api/src/controllers/tools/toolsController.ts b/express-api/src/controllers/tools/toolsController.ts index e5a94b5ba..5bd96fe41 100644 --- a/express-api/src/controllers/tools/toolsController.ts +++ b/express-api/src/controllers/tools/toolsController.ts @@ -2,9 +2,17 @@ import { z } from 'zod'; import { Request, Response } from 'express'; import chesServices from '@/services/ches/chesServices'; import { ChesFilterSchema } from './toolsSchema'; -import { SSOUser } from '@bcgov/citz-imb-sso-express'; import geocoderService from '@/services/geocoder/geocoderService'; +/** + * NOTE + * Routes for the CHES controllers have been removed. + * Management of notifications and their CHES messages is + * handled through the notifications routes. + * + * Leaving these controllers here for now in case they are needed in the future. + */ + /** * @description Gets the status of a CHES message. * @param {Request} req Incoming request. @@ -12,13 +20,6 @@ import geocoderService from '@/services/geocoder/geocoderService'; * @returns {Response} A 200 status with a CHES object. */ export const getChesMessageStatusById = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Tools'] - * #swagger.description = 'Make a request to CHES to get the status of the specified 'messageId'.' - * #swagger.security = [{ - "bearerAuth" : [] - }] - */ const messageId = z.string().uuid().safeParse(req.params.messageId); if (messageId.success) { const status = await chesServices.getStatusByIdAsync(messageId.data); @@ -35,13 +36,6 @@ export const getChesMessageStatusById = async (req: Request, res: Response) => { * @returns {Response} A 200 status with a CHES object. */ export const getChesMessageStatuses = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Tools'] - * #swagger.description = 'Make a request to CHES to get the status of many messages by the specified query strings' - * #swagger.security = [{ - "bearerAuth" : [] - }] - */ const filter = ChesFilterSchema.safeParse(req.query); if (filter.success) { const status = await chesServices.getStatusesAsync(filter.data); @@ -58,13 +52,6 @@ export const getChesMessageStatuses = async (req: Request, res: Response) => { * @returns {Response} A 200 status with a CHES object. */ export const cancelChesMessageById = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Tools'] - * #swagger.description = 'Make a request to CHES to cancel the specified 'messageId'' - * #swagger.security = [{ - "bearerAuth" : [] - }] - */ const messageId = z.string().uuid().safeParse(req.params.messageId); if (messageId.success) { const status = await chesServices.cancelEmailByIdAsync(messageId.data); @@ -81,13 +68,6 @@ export const cancelChesMessageById = async (req: Request, res: Response) => { * @returns {Response} A 200 status with a CHES object. */ export const cancelChesMessages = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Tools'] - * #swagger.description = 'Make a request to CHES to cancel the specified query string filter' - * #swagger.security = [{ - "bearerAuth" : [] - }] - */ const filter = ChesFilterSchema.safeParse(req.query); if (filter.success) { const status = await chesServices.cancelEmailsAsync(filter.data); @@ -97,14 +77,6 @@ export const cancelChesMessages = async (req: Request, res: Response) => { } }; -// Will likely not make it into final release, excluding from api docs. -export const sendChesMessage = async (req: Request, res: Response) => { - const email = req.body; - const ssoUser = req.user as SSOUser; - const response = await chesServices.sendEmailAsync(email, ssoUser); - return res.status(201).send(response); -}; - /** * @description Search Geocoder for an address. * @param {Request} req Incoming request. @@ -112,13 +84,6 @@ export const sendChesMessage = async (req: Request, res: Response) => { * @returns {Response} A 200 status with an address object. */ export const searchGeocoderAddresses = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Tools'] - * #swagger.description = 'Make a request to Data BC Geocoder for addresses that match the specified `search`.' - * #swagger.security = [{ - "bearerAuth" : [] - }] - */ const address = String(req.query.address); const minScore = isNaN(Number(req.query.minScore)) ? undefined : String(req.query.minScore); const maxResults = isNaN(Number(req.query.maxResults)) ? undefined : String(req.query.maxResults); @@ -133,13 +98,6 @@ export const searchGeocoderAddresses = async (req: Request, res: Response) => { * @returns {Response} A 200 status with a siteId object. */ export const searchGeocoderSiteId = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Tools'] - * #swagger.description = 'Make a request to Data BC Geocoder for PIDs that belong to the specified 'siteId'.' - * #swagger.security = [{ - "bearerAuth" : [] - }] - */ const siteId = String(req.params.siteId); const result = await geocoderService.getPids(siteId); return res.status(200).send(result); diff --git a/express-api/src/controllers/users/usersController.ts b/express-api/src/controllers/users/usersController.ts index 6eb1bc993..b2794b65a 100644 --- a/express-api/src/controllers/users/usersController.ts +++ b/express-api/src/controllers/users/usersController.ts @@ -1,70 +1,12 @@ import userServices from '@/services/users/usersServices'; import { Request, Response } from 'express'; import { SSOUser } from '@bcgov/citz-imb-sso-express'; -import { decodeJWT } from '@/utilities/decodeJWT'; import { UserFiltering, UserFilteringSchema } from '@/controllers/users/usersSchema'; import { z } from 'zod'; import { isAdmin } from '@/utilities/authorizationChecks'; import notificationServices from '@/services/notifications/notificationServices'; import getConfig from '@/constants/config'; import logger from '@/utilities/winstonLogger'; -/** - * @description Function to filter users based on agencies - * @param {Request} req Incoming request. - * @param {Response} res Outgoing response. - * @param {SSOUser} ssoUser Incoming SSO user. - * @returns {User[]} An array of users. - */ -const filterUsersByAgencies = async (req: Request, res: Response, ssoUser: SSOUser) => { - const filter = UserFilteringSchema.safeParse(req.query); - if (!filter.success) { - return res.status(400).send('Failed to parse filter query.'); - } - const filterResult = filter.data; - - let users; - if (isAdmin(ssoUser)) { - users = await userServices.getUsers(filterResult as UserFiltering); - } else { - // Get agencies associated with the requesting user - const usersAgencies = await userServices.getAgencies(ssoUser.preferred_username); - filterResult.agencyId = usersAgencies; - users = await userServices.getUsers(filterResult as UserFiltering); - } - return users; -}; - -/** - * @description Redirects user to the keycloak user info endpoint. - * @param {Request} req Incoming request. - * @param {Response} res Outgoing response. - * @returns {Response} A 200 status with an object containing keycloak info. - */ - -export const getUserInfo = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Users'] - * #swagger.description = 'Redirect user to keycloak user info.' - * #swagger.security = [{ - "bearerAuth" : [] - }] - */ - - if (!req.token) return res.status(400).send('No access token'); - const [header, payload] = req.token.split('.'); - if (!header || !payload) return res.status(400).send('Bad token format.'); - - const info = { - header: decodeJWT(header), - payload: decodeJWT(payload), - }; - - if (info) { - return res.status(200).send(info.payload); - } else { - return res.status(400).send('No keycloak user authenticated.'); - } -}; /** * @description Submits a user access request. @@ -73,13 +15,6 @@ export const getUserInfo = async (req: Request, res: Response) => { * @returns {Response} A 201 status with the created access request. */ export const submitUserAccessRequest = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Users'] - * #swagger.description = 'Submit a user access request.' - * #swagger.security = [{ - "bearerAuth" : [] - }] - */ const result = await userServices.addKeycloakUserOnHold( req.user as SSOUser, Number(req.body.AgencyId), @@ -111,23 +46,24 @@ export const submitUserAccessRequest = async (req: Request, res: Response) => { * @param {Request} req Incoming request. * @param {Response} res Outgoing response. * @returns {Response} A 200 status with an array of integers. - * - * Note that the original docs imply it gives the full user response, but looking at the implementation - * I think it only gives the numeric agency Ids. */ export const getUserAgencies = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Users'] - * #swagger.description = 'Get user agency information.' - * #swagger.security = [{ - "bearerAuth" : [] - }] - */ const user = String(req.params?.username); const result = await userServices.getAgencies(user); return res.status(200).send(result); }; +/** + * Retrieves the user information for the currently authenticated user. + * Normalizes the Keycloak user using internal organization's method. + * Retrieves the user details based on the normalized username. + * If user details are found, returns a status of 200 with the user information. + * If no user details are found, returns a status of 204 indicating a valid request but no user for the Keycloak login. + * + * @param {Request} req - The request object containing user information. + * @param {Response} res - The response object to send back the user details or status. + * @returns {Response} A 200 status with user's info. + */ export const getSelf = async (req: Request, res: Response) => { const user = userServices.normalizeKeycloakUser(req.user as SSOUser); const result = await userServices.getUser(user.username); @@ -145,42 +81,23 @@ export const getSelf = async (req: Request, res: Response) => { * @returns {Response} A 200 status with a list of users. */ export const getUsers = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Users - Admin'] - * #swagger.description = 'Gets a paged list of users.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ + const ssoUser = req.user as unknown as SSOUser; const filter = UserFilteringSchema.safeParse(req.query); if (!filter.success) { return res.status(400).send('Failed to parse filter query.'); } - const ssoUser = req.user as unknown as SSOUser; - const users = await filterUsersByAgencies(req, res, ssoUser); - return res.status(200).send(users); -}; + const filterResult = filter.data; -/** - * @description Adds a new user to the datasource. - * @param {Request} req Incoming request - * @param {Response} res Outgoing response - * @returns {Response} A 201 status and the data of the user added. - */ -export const addUser = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Users - Admin'] - * #swagger.description = 'Adds a new user to the datasource.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ - try { - const user = await userServices.addUser(req.body); - return res.status(201).send(user); - } catch (e) { - return res.status(400).send(e.message); + let users; + if (isAdmin(ssoUser)) { + users = await userServices.getUsers(filterResult as UserFiltering); + } else { + // Get agencies associated with the requesting user + const usersAgencies = await userServices.getAgencies(ssoUser.preferred_username); + filterResult.agencyId = usersAgencies; + users = await userServices.getUsers(filterResult as UserFiltering); } + return res.status(200).send(users); }; /** @@ -190,13 +107,6 @@ export const addUser = async (req: Request, res: Response) => { * @returns {Response} A 200 status and the user data. */ export const getUserById = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Users'] - * #swagger.description = 'Returns an user that matches the supplied ID.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ const id = req.params.id; const uuid = z.string().uuid().safeParse(id); const ssoUser = req.user as unknown as SSOUser; @@ -229,13 +139,6 @@ export const getUserById = async (req: Request, res: Response) => { * @returns {Response} A 200 status and the user data. */ export const updateUserById = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Users - Admin'] - * #swagger.description = 'Updates an user that matches the supplied ID.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ const idParse = z.string().uuid().safeParse(req.params.id); if (!idParse.success) { return res.status(400).send(idParse); @@ -246,82 +149,3 @@ export const updateUserById = async (req: Request, res: Response) => { const user = await userServices.updateUser(req.body); return res.status(200).send(user); }; - -/** - * @description Deletes a single user that matches an ID. - * @param {Request} req Incoming request - * @param {Response} res Outgoing response - * @returns {Response} A 204 status indicating successful deletion. - */ -export const deleteUserById = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Users - Admin'] - * #swagger.description = 'Deletes an user that matches the supplied ID.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ - - const idParse = z.string().uuid().safeParse(req.params.id); - if (!idParse.success) { - return res.status(400).send(idParse); - } - if (idParse.data != req.body.Id) { - return res.status(400).send('The param ID does not match the request body.'); - } - const user = await userServices.deleteUser(req.body); - return res.status(200).send(user); -}; - -/** - * @description Gets all roles of a user based on their name. - * @param {Request} req Incoming request - * @param {Response} res Outgoing response - * @returns {Response} A 200 status with a list of the user's roles. - */ -export const getAllRoles = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Users - Admin'] - * #swagger.description = 'Gets a list of roles assigned to a user.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ - const roles = await userServices.getKeycloakRoles(); - return res.status(200).send(roles); -}; - -/** - * @description Gets all roles of a user based on their name. - * @param {Request} req Incoming request - * @param {Response} res Outgoing response - * @returns {Response} A 200 status with a list of the user's roles. - */ -export const getUserRolesByName = async (req: Request, res: Response) => { - /** - * #swagger.tags = ['Users - Admin'] - * #swagger.description = 'Gets a list of roles assigned to a user.' - * #swagger.security = [{ - "bearerAuth": [] - }] - */ - const username = req.params.username; - if (!username) { - return res.status(400).send('Username was empty.'); - } - const roles = await userServices.getKeycloakUserRoles(username); - return res.status(200).send(roles); -}; - -export const updateUserRolesByName = async (req: Request, res: Response) => { - const username = req.params.username; - const roles = z.string().array().safeParse(req.body); - if (!roles.success) { - return res.status(400).send('Request body was wrong format.'); - } - if (!username) { - return res.status(400).send('Username was empty.'); - } - const updatedRoles = await userServices.updateKeycloakUserRoles(username, roles.data); - return res.status(200).send(updatedRoles); -}; diff --git a/express-api/src/express.ts b/express-api/src/express.ts index 32f23d3aa..47d038ebb 100644 --- a/express-api/src/express.ts +++ b/express-api/src/express.ts @@ -10,10 +10,11 @@ import middleware from '@/middleware'; import constants from '@/constants'; import { SSO_OPTIONS } from '@/middleware/keycloak/keycloakOptions'; import swaggerUi from 'swagger-ui-express'; -import swaggerJSON from '@/swagger/swagger-output.json'; +import swaggerJSDoc from 'swagger-jsdoc'; import errorHandler from '@/middleware/errorHandler'; import { EndpointNotFound404 } from '@/constants/errors'; import nunjucks from 'nunjucks'; +import OPENAPI_OPTIONS from '@/swagger/swaggerConfig'; const app: Application = express(); @@ -50,7 +51,7 @@ app.use(cookieParser()); app.use(compression()); // Swagger service route -app.use('/api-docs/', swaggerUi.serve, swaggerUi.setup(swaggerJSON)); +app.use('/api-docs/', swaggerUi.serve, swaggerUi.setup(swaggerJSDoc(OPENAPI_OPTIONS))); // Get Custom Middleware const { headerHandler, morganMiddleware } = middleware; @@ -86,7 +87,6 @@ app.use(`/v2/administrativeAreas`, protectedRoute(), router.administrativeAreasR app.use(`/v2/agencies`, protectedRoute(), router.agenciesRouter); app.use('/v2/lookup', protectedRoute(), router.lookupRouter); app.use(`/v2/users`, protectedRoute(), router.usersRouter); -app.use(`/v2/roles`, protectedRoute(), router.rolesRouter); app.use(`/v2/properties`, protectedRoute(), router.propertiesRouter); app.use(`/v2/parcels`, protectedRoute(), router.parcelsRouter); app.use(`/v2/buildings`, protectedRoute(), router.buildingsRouter); diff --git a/express-api/src/routes/administrativeAreas.swagger.yaml b/express-api/src/routes/administrativeAreas.swagger.yaml new file mode 100644 index 000000000..67728b0a3 --- /dev/null +++ b/express-api/src/routes/administrativeAreas.swagger.yaml @@ -0,0 +1,217 @@ +### PATHS ### +paths: + /administrativeAreas: + get: + security: + - bearerAuth: [] + tags: + - Administrative Areas + summary: Gets a list of administrative areas based on supplied filter. + description: > + Used primarily for displaying administrative areas in a table. + Non-generic query values must be in this format: `{searchType},{searchValue}` + parameters: + - in: query + name: name + schema: + type: string + example: contains,Victoria + - in: query + name: regionalDistrictName + schema: + type: string + example: contains,Capital Regional District + - in: query + name: isDisabled + schema: + type: string + example: is,false + - in: query + name: createdOn + schema: + type: string + example: 'before,Wed Jul 31 2024 17:00:00 GMT-0700 (Pacific Daylight Time)' + - in: query + name: quickFilter + schema: + type: string + description: Applied as an OR WHERE search on relevant columns. + - in: query + name: quantity + schema: + type: string + description: Number of records to be returned. Equivalent to page size. + - in: query + name: page + schema: + type: string + description: Page number of requested results. Only necessary if quantity is specified. + - in: query + name: sortKey + schema: + type: string + description: The column name on which the records will be sorted. + - in: query + name: sortOrder + schema: + type: string + description: Either DESC or ASC. + - in: query + name: excelExport + schema: + type: boolean + example: false + description: If true, returns in format for Excel export. + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + totalCount: + type: integer + example: 1 + data: + type: array + items: + $ref: '#/components/schemas/AdministrativeAreaJoinView' + post: + security: + - bearerAuth: [] + tags: + - Administrative Areas + summary: Submits a new administrative area. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AdministrativeAreaPost' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/AdministrativeArea' + '409': + description: Conflict + content: + text/plain: + schema: + type: string + example: Administrative area already exists. + /administrativeAreas/{id}: + get: + security: + - bearerAuth: [] + tags: + - Administrative Areas + summary: Gets a single administrative area that matches the Id. + parameters: + - in: path + name: id + type: integer + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/AdministrativeArea' + put: + security: + - bearerAuth: [] + tags: + - Administrative Areas + summary: Updates a single administrative area that matches the Id. + parameters: + - in: path + name: id + type: integer + required: true + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AdministrativeArea' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/AdministrativeArea' + '404': + description: Not Found + content: + text/plain: + schema: + type: string + example: Administrative area does not exist. + +### SCHEMAS ### +components: + schemas: + AdministrativeAreaPost: + type: object + description: Object submitted in body of POST request. + properties: + Name: + required: true + type: string + example: Victoria + RegionalDistrictId: + required: true + type: integer + example: 19 + ProvinceId: + required: true + type: string + example: BC + IsDisabled: + type: boolean + example: false + AdministrativeArea: + allOf: + - $ref: '#/components/schemas/AdministrativeAreaPost' + - $ref: '#/components/schemas/CommonEntityProperties' + - type: object + properties: + Id: + type: integer + example: 300 + SortOrder: + type: integer + example: 0 + AdministrativeAreaJoinView: + type: object + description: Object returned from Administrative Area Join View. + properties: + Id: + type: integer + example: 300 + Name: + type: string + example: Victoria + IsDisabled: + type: boolean + example: false + SortOrder: + type: integer + example: 0 + RegionalDistrictId: + type: integer + example: 19 + RegionalDistrictName: + type: string + example: Capital Regional District + ProvinceId: + type: string + example: BC + CreatedOn: + type: date + example: '2023-01-18T01:58:40.246Z' + diff --git a/express-api/src/routes/administrativeAreasRouter.ts b/express-api/src/routes/administrativeAreasRouter.ts index 1d60443ec..cd46cfd20 100644 --- a/express-api/src/routes/administrativeAreasRouter.ts +++ b/express-api/src/routes/administrativeAreasRouter.ts @@ -15,7 +15,7 @@ const router = express.Router(); // Endpoints for Admin Administrative Areas router .route(`/`) - .get(catchErrors(getAdministrativeAreas)) + .get(activeUserCheck, catchErrors(getAdministrativeAreas)) .post(protectedRoute([Roles.ADMIN]), activeUserCheck, catchErrors(addAdministrativeArea)); router diff --git a/express-api/src/routes/agencies.swagger.yaml b/express-api/src/routes/agencies.swagger.yaml new file mode 100644 index 000000000..7ac879090 --- /dev/null +++ b/express-api/src/routes/agencies.swagger.yaml @@ -0,0 +1,298 @@ +### PATHS ### +paths: + /agencies: + get: + security: + - bearerAuth: [] + tags: + - Agencies + summary: Gets a list of agencies based on supplied filter. + description: > + Used primarily for displaying agencies in a table. + Non-generic query values must be in this format: `{searchType},{searchValue}` + parameters: + - in: query + name: name + schema: + type: string + example: contains,Real + - in: query + name: parentName + schema: + type: string + example: contains,Citizens + - in: query + name: isDisabled + schema: + type: string + example: is,false + - in: query + name: email + schema: + type: string + - in: query + name: sendEmail + schema: + type: string + example: is,true + - in: query + name: code + schema: + type: string + example: contains,RPD + - in: query + name: createdOn + schema: + type: string + example: 'before,Wed Aug 13 2024 17:00:00 GMT-0700 (Pacific Daylight Time)' + - in: query + name: updatedOn + schema: + type: string + - in: query + name: quickFilter + schema: + type: string + description: Applied as an OR WHERE search on relevant columns. + - in: query + name: quantity + schema: + type: string + description: Number of records to be returned. Equivalent to page size. + - in: query + name: page + schema: + type: string + description: Page number of requested results. Only necessary if quantity is specified. + - in: query + name: sortKey + schema: + type: string + description: The column name on which the records will be sorted. + - in: query + name: sortOrder + schema: + type: string + description: Either DESC or ASC. + - in: query + name: excelExport + schema: + type: boolean + example: false + description: If true, returns in format for Excel export. + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + totalCount: + type: integer + example: 1 + data: + type: array + items: + $ref: '#/components/schemas/AgencyJoinView' + post: + security: + - bearerAuth: [] + tags: + - Agencies + summary: Submits a new agency. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AgencyPost' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/Agency' + '409': + description: Conflict + content: + text/plain: + schema: + type: string + example: Agency with that name or code already exists. + /agencies/{id}: + get: + security: + - bearerAuth: [] + tags: + - Agencies + summary: Gets a single agency that matches the Id. + parameters: + - in: path + name: id + type: integer + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Agency' + put: + security: + - bearerAuth: [] + tags: + - Agencies + summary: Updates a single agency that matches the Id. + parameters: + - in: path + name: id + type: integer + required: true + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Agency' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Agency' + '400': + description: Bad Request + content: + text/plain: + schema: + type: string + example: The param ID does not match the request body. + '403': + description: Forbidden + content: + text/plain: + schema: + type: string + example: An agency cannot be its own parent. + '404': + description: Not Found + content: + text/plain: + schema: + type: string + example: Agency not found. + delete: + security: + - bearerAuth: [] + tags: + - Agencies + summary: Deletes a single agency that matches the Id. + parameters: + - in: path + name: id + type: integer + required: true + responses: + '204': + description: No Content + '400': + description: Bad Request + '404': + description: Not Found + content: + text/plain: + schema: + type: string + example: Agency not found. + +### SCHEMAS ### +components: + schemas: + AgencyPost: + type: object + description: Object submitted in body of POST request. + properties: + Name: + required: true + type: string + example: Real Property Division + Code: + required: true + type: string + example: RPD + Description: + type: string + example: Description goes here. + IsDisabled: + type: boolean + example: false + ParentId: + type: integer + example: 2 + SortOrder: + type: integer + example: 0 + SendEmail: + type: boolean + example: false + Email: + type: string + example: email@gov.bc.ca + CCEmail: + type: string + example: email@gov.bc.ca + Agency: + allOf: + - $ref: '#/components/schemas/AgencyPost' + - $ref: '#/components/schemas/CommonEntityProperties' + - type: object + properties: + Id: + type: integer + example: 300 + AddressTo: + type: string + example: Good Morning + AgencyJoinView: + type: object + description: Object returned from Agency Join View. + properties: + Id: + type: integer + example: 110 + Name: + type: string + example: Real Property Division + IsDisabled: + type: boolean + example: false + SortOrder: + type: integer + example: 0 + ParentId: + type: integer + example: 2 + ParentName: + type: string + example: "Citizens' Services" + Code: + type: string + example: RPD + Description: + type: string + example: Agency that manages PIMS. + Email: + type: string + example: email@gov.bc.ca + SendEmail: + type: boolean + example: false + CreatedOn: + type: date + example: '2023-01-18T01:58:40.246Z' + UpdatedOn: + type: date + example: '2023-01-18T01:58:40.246Z' + diff --git a/express-api/src/routes/agenciesRouter.ts b/express-api/src/routes/agenciesRouter.ts index 0fe9540b8..788d570de 100644 --- a/express-api/src/routes/agenciesRouter.ts +++ b/express-api/src/routes/agenciesRouter.ts @@ -16,12 +16,12 @@ const router = express.Router(); // Endpoints for Admin Agencies router .route(`/`) - .get(protectedRoute(), catchErrors(getAgencies)) + .get(activeUserCheck, catchErrors(getAgencies)) .post(protectedRoute([Roles.ADMIN]), activeUserCheck, catchErrors(addAgency)); router .route(`/:id`) - .get(protectedRoute(), activeUserCheck, catchErrors(getAgencyById)) + .get(activeUserCheck, catchErrors(getAgencyById)) .patch(protectedRoute([Roles.ADMIN]), activeUserCheck, catchErrors(updateAgencyById)) .delete(protectedRoute([Roles.ADMIN]), activeUserCheck, catchErrors(deleteAgencyById)); diff --git a/express-api/src/routes/buildings.swagger.yaml b/express-api/src/routes/buildings.swagger.yaml new file mode 100644 index 000000000..867ceec3d --- /dev/null +++ b/express-api/src/routes/buildings.swagger.yaml @@ -0,0 +1,349 @@ +### PATHS ### +paths: + /buildings: + get: + security: + - bearerAuth: [] + tags: + - Buildings + summary: Gets a list of buildings based on supplied filter. + parameters: + - in: query + name: pid + schema: + type: integer + description: Should not include leading 0s or hyphens. + - in: query + name: classificationId + schema: + type: integer + - in: query + name: agencyId + description: Can also be an array of integers + schema: + type: integer + - in: query + name: administrativeAreaId + schema: + type: integer + - in: query + name: propertyTypeId + schema: + type: integer + - in: query + name: buildingConstructionTypeId + schema: + type: integer + - in: query + name: buildingPredominateUseId + schema: + type: integer + - in: query + name: buildingOccupantTypeId + schema: + type: integer + - in: query + name: isSensitive + schema: + type: boolean + - in: query + name: page + schema: + type: integer + description: Page number of requested results. Only necessary if quantity is specified. + - in: query + name: quantity + schema: + type: integer + description: The column name on which the records will be sorted. + - in: query + name: sort + schema: + type: string + description: Either DESC or ASC. + - in: query + name: includeRelations + schema: + type: boolean + description: Flag to join and select foreign key records. + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Building' + '400': + description: Bad Request + content: + text/plain: + schema: + type: string + example: Could not parse filter. + post: + security: + - bearerAuth: [] + tags: + - Buildings + summary: Submits a new Building. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/BuildingPost' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/Building' + '409': + description: Conflict + content: + text/plain: + schema: + type: string + example: Building already exists. + /buildings/{id}: + get: + security: + - bearerAuth: [] + tags: + - Buildings + summary: Gets a single Building that matches the Id. + parameters: + - in: path + name: id + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Building' + '400': + description: Bad Request + content: + text/plain: + type: string + example: Building ID was invalid. + '403': + description: Forbidden + content: + text/plain: + type: string + example: You are not authorized to view this building. + '404': + description: Not Found + content: + text/plain: + type: string + example: Building matching this internal ID not found. + put: + security: + - bearerAuth: [] + tags: + - Buildings + summary: Updates a single Building that matches the Id. + parameters: + - in: path + name: id + type: integer + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Building' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Building' + '400': + description: Bad Request + content: + text/plain: + schema: + type: string + example: The param ID does not match the request body. + '403': + description: Forbidden + content: + text/plain: + schema: + type: string + example: Changing agency is not permitted. + '404': + description: Not Found + content: + text/plain: + schema: + type: string + example: Building does not exist. + delete: + security: + - bearerAuth: [] + tags: + - Buildings + summary: Deletes a single Building that matches the Id. + parameters: + - in: path + name: id + type: integer + responses: + '200': + description: OK + '400': + description: Bad Request + content: + text/plain: + schema: + type: string + example: Building ID was invalid. + '403': + description: Forbidden + content: + text/plain: + schema: + type: string + example: Building is involved in one or more projects with ID(s) ... + '404': + description: Not Found + content: + text/plain: + schema: + type: string + example: Building does not exist. + +### SCHEMAS ### +components: + schemas: + BuildingPost: + type: object + description: Object submitted in body of POST request. + properties: + Address1: + required: true + type: string + example: 742 Evergreen Terr. + AdministrativeAreaId: + required: true + type: integer + example: 2 + AgencyId: + required: true + type: integer + example: 2 + BuildingConstructionTypeId: + required: true + type: integer + example: 1 + BuildingFloorCount: + type: integer + example: 2 + BuildingPredominateUseId: + required: true + type: integer + example: 1 + BuildingTenancy: + type: string + example: '100' + description: > + Historical records force this to be saved as a string. + Frontend controls prevent additional non-numerical values. + BuildingTenancyUpdatedOn: + type: string + example: '2024-08-14T15:48:11.883Z' + ClassificationId: + required: true + type: integer + example: 0 + Description: + type: string + Evaluations: + type: array + items: + $ref: '#/components/schemas/EvaluationPost' + Fiscals: + type: array + items: + $ref: '#/components/schemas/FiscalPost' + IsSensitive: + type: boolean + example: false + IsVisibleToOtherAgencies: + type: boolean + example: false + Location: + required: true + type: object + properties: + x: + type: integer + example: -123.3134 + y: + type: integer + example: 48.2344 + Name: + required: true + type: string + example: Building Name + PID: + type: integer + example: 4242904 + PIN: + type: integer + example: 123456 + Postal: + type: string + example: V8X4S7 + PropertyTypeId: + required: true + description: Will always be 1 for buildings. + type: integer + example: 1 + RentableArea: + type: integer + example: 90 + description: Measured in metres squared + TotalArea: + required: true + type: integer + example: 100 + description: Measured in metres squared + Building: + allOf: + - $ref: '#/components/schemas/BuildingPost' + - $ref: '#/components/schemas/CommonEntityProperties' + - type: object + properties: + Id: + type: integer + example: 300 + SiteId: + type: string + example: null + description: > + Site ID from BC Geocoder. Not currently in use. + Evaluations: + type: array + items: + $ref: '#/components/schemas/BuildingEvaluation' + Fiscals: + type: array + items: + $ref: '#/components/schemas/BuildingFiscal' + BuildingUpdate: + allOf: + - $ref: '#/components/schemas/BuildingPost' + - type: object + properties: + Id: + type: integer + example: 300 diff --git a/express-api/src/routes/health.swagger.yaml b/express-api/src/routes/health.swagger.yaml new file mode 100644 index 000000000..be910080a --- /dev/null +++ b/express-api/src/routes/health.swagger.yaml @@ -0,0 +1,18 @@ +### PATHS ### +paths: + /health: + get: + tags: + - Health + summary: Checks if the API is running and healthy. + description: Checks if the API is running and healthy. + responses: + '200': + description: OK + content: + text/plain: + schema: + type: string + example: '/health endpoint reached. API running.' + '429': + $ref: '#/components/responses/429TooManyRequests' diff --git a/express-api/src/routes/index.ts b/express-api/src/routes/index.ts index 9c7b6c963..a9a020411 100644 --- a/express-api/src/routes/index.ts +++ b/express-api/src/routes/index.ts @@ -11,7 +11,6 @@ import reportsRouter from '@/routes/reportsRouter'; import toolsRouter from '@/routes/toolsRouter'; import agenciesRouter from '@/routes/agenciesRouter'; import administrativeAreasRouter from '@/routes/administrativeAreasRouter'; -import rolesRouter from '@/routes/rolesRouter'; const router = { administrativeAreasRouter, @@ -27,7 +26,6 @@ const router = { reportsRouter, toolsRouter, agenciesRouter, - rolesRouter, }; export default router; diff --git a/express-api/src/routes/lookup.swagger.yaml b/express-api/src/routes/lookup.swagger.yaml new file mode 100644 index 000000000..8b9d43738 --- /dev/null +++ b/express-api/src/routes/lookup.swagger.yaml @@ -0,0 +1,439 @@ +### PATHS ### +paths: + /lookup/regionalDistricts: + get: + security: + - bearerAuth: [] + tags: + - Lookup + summary: Gets a list of regional districts with minimal properties. + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RegionalDistrictPublic' + /lookup/property/classifications: + get: + security: + - bearerAuth: [] + tags: + - Lookup + summary: Gets a list of property classifications with minimal properties. + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PropertyClassificationPublic' + /lookup/property/predominateUses: + get: + security: + - bearerAuth: [] + tags: + - Lookup + summary: Gets a list of building predominate uses with minimal properties. + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PredominateUsePublic' + /lookup/property/constructionTypes: + get: + security: + - bearerAuth: [] + tags: + - Lookup + summary: Gets a list of building construction types with minimal properties. + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ConstructionTypePublic' + /lookup/project/tierLevels: + get: + security: + - bearerAuth: [] + tags: + - Lookup + summary: Gets a list of project tier levels with minimal properties. + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TierLevelPublic' + /lookup/project/status: + get: + security: + - bearerAuth: [] + tags: + - Lookup + summary: Gets a list of project statuses with minimal properties. + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ProjectStatusPublic' + /lookup/tasks: + get: + security: + - bearerAuth: [] + tags: + - Lookup + summary: Gets a list of project tasks with minimal properties. + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ProjectMetadataPublic' + /lookup/propertyTypes: + get: + security: + - bearerAuth: [] + tags: + - Lookup + summary: Gets a list of property types with minimal properties. + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PropertyTypePublic' + /lookup/noteTypes: + get: + security: + - bearerAuth: [] + tags: + - Lookup + summary: Gets a list of note types with minimal properties. + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ProjectMetadataPublic' + /lookup/timestampTypes: + get: + security: + - bearerAuth: [] + tags: + - Lookup + summary: Gets a list of timestamp types with minimal properties. + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ProjectMetadataPublic' + /lookup/monetaryTypes: + get: + security: + - bearerAuth: [] + tags: + - Lookup + summary: Gets a list of monetary types with minimal properties. + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ProjectMetadataPublic' + /lookup/all: + get: + security: + - bearerAuth: [] + tags: + - Lookup + summary: Gets an object containing all lookup values. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/LookupAll' + +### SCHEMAS ### +components: + schemas: + RegionalDistrictPublic: + type: object + properties: + Name: + type: string + example: Capital Regional District + Id: + type: integer + example: 1 + Abbreviation: + type: string + example: CRD + PropertyClassificationPublic: + type: object + properties: + Name: + type: string + example: Core Operational + Id: + type: integer + example: 1 + SortOrder: + type: integer + example: 0 + IsVisible: + type: boolean + example: true + PredominateUsePublic: + type: object + properties: + Name: + type: string + example: Clinic + Id: + type: integer + example: 1 + SortOrder: + type: integer + example: 0 + ConstructionTypePublic: + type: object + properties: + Name: + type: string + example: Wood + Id: + type: integer + example: 1 + SortOrder: + type: integer + example: 0 + TierLevelPublic: + type: object + properties: + Name: + type: string + example: Tier 1 + Id: + type: integer + example: 1 + SortOrder: + type: integer + example: 0 + ProjectStatusPublic: + type: object + properties: + Name: + type: string + example: Disposed + Id: + type: integer + example: 1 + SortOrder: + type: integer + example: 0 + IsDisabled: + type: boolean + example: false + PropertyTypePublic: + type: object + properties: + Name: + type: string + example: Building + Id: + type: integer + example: 1 + IsDisabled: + type: boolean + example: false + SortOrder: + type: integer + example: 0 + ProjectMetadataPublic: + type: object + properties: + Name: + type: string + Id: + type: integer + example: 1 + Description: + type: string + IsOptional: + type: boolean + example: true + StatusId: + type: integer + example: 1 + RiskPublic: + type: object + properties: + Name: + type: string + example: Green + Id: + type: integer + example: 1 + Code: + type: string + example: GREEN + Description: + type: string + RolePublic: + type: object + properties: + Name: + type: string + example: General User + Id: + type: string + example: '00000000-0000-0000-0000-000000000000' + Description: + type: string + WorkflowPublic: + type: object + properties: + Name: + type: string + example: Surplus Property List + Id: + type: integer + example: 1 + AdministrativeAreaPublic: + type: object + properties: + Name: + type: string + example: Victoria + Id: + type: integer + example: 1 + RegionalDistrictId: + type: integer + example: 1 + AgencyPublic: + type: object + properties: + Name: + type: string + example: Real Property Division + Id: + type: integer + example: 1 + Code: + type: string + example: RPD + ParentId: + type: integer + example: 2 + LookupAll: + type: object + properties: + AdministrativeAreas: + type: array + items: + $ref: '#/components/schemas/AdministrativeAreaPublic' + Agencies: + type: array + items: + $ref: '#/components/schemas/AgencyPublic' + Classifications: + type: array + items: + $ref: '#/components/schemas/PropertyClassificationPublic' + Config: + type: object + properties: + contactEmail: + type: string + example: email@gov.bc.ca + ConstructionTypes: + type: array + items: + $ref: '#/components/schemas/ConstructionTypePublic' + MonetaryTypes: + type: array + items: + $ref: '#/components/schemas/ProjectMetadataPublic' + NoteTypes: + type: array + items: + $ref: '#/components/schemas/ProjectMetadataPublic' + PredominateUses: + type: array + items: + $ref: '#/components/schemas/PredominateUsePublic' + ProjectStatuses: + type: array + items: + $ref: '#/components/schemas/ProjectStatusPublic' + ProjectTiers: + type: array + items: + $ref: '#/components/schemas/TierLevelPublic' + PropertyTypes: + type: array + items: + $ref: '#/components/schemas/PropertyTypePublic' + RegionalDistricts: + type: array + items: + $ref: '#/components/schemas/RegionalDistrictPublic' + Risks: + type: array + items: + $ref: '#/components/schemas/RiskPublic' + Roles: + type: array + items: + $ref: '#/components/schemas/RolePublic' + Tasks: + type: array + items: + $ref: '#/components/schemas/ProjectMetadataPublic' + TimestampTypes: + type: array + items: + $ref: '#/components/schemas/ProjectMetadataPublic' + Workflows: + type: array + items: + $ref: '#/components/schemas/WorkflowPublic' diff --git a/express-api/src/routes/ltsa.swagger.yaml b/express-api/src/routes/ltsa.swagger.yaml new file mode 100644 index 000000000..dc94c891c --- /dev/null +++ b/express-api/src/routes/ltsa.swagger.yaml @@ -0,0 +1,342 @@ +### PATHS ### +paths: + /ltsa/land/title: + get: + security: + - bearerAuth: [] + tags: + - LTSA + summary: Gets a order object from the Land Title and Survey Authority. + parameters: + - in: query + name: pid + schema: + type: string + example: '123456789' + description: Should not include leading 0s or hyphens. + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + order: + type: object + properties: + billingInfo: + type: object + properties: + billingModel: + type: string + example: PROV + feeExempted: + type: boolean + example: true + productCode: + type: string + example: Search + productFee: + type: number + example: 0 + productFeeTax: + type: number + example: 0 + productName: + type: string + example: Searches + serviceCharge: + type: number + example: 0 + serviceChargeTax: + type: number + example: 0 + subtotalFee: + type: number + example: 0 + totalFee: + type: number + example: 0 + totalTax: + type: number + example: 0 + fileReference: + type: string + orderId: + type: string + example: '142beeab-ca69-495c-91ac-021e6b64812c' + orderedProduct: + type: object + properties: + fieldedData: + type: object + properties: + titleStatus: + type: string + example: "REGISTERED" + titleIdentifier: + type: object + properties: + titleNumber: + type: string + example: "AA000000" + landTitleDistrict: + type: string + example: "VICTORIA" + tombstone: + type: object + properties: + applicationReceivedDate: + type: string + format: date-time + example: "2024-01-01T00:00:00Z" + enteredDate: + type: string + format: date-time + example: "2024-01-01T00:00:00Z" + titleRemarks: + type: string + marketValueAmount: + type: string + example: "100" + fromTitles: + type: array + items: + type: object + properties: + titleNumber: + type: string + example: "AAA000" + landTitleDistrict: + type: string + example: "VICTORIA" + natureOfTransfers: + type: array + items: + type: object + properties: + transferReason: + type: string + example: "FEE SIMPLE" + ownershipGroups: + type: array + items: + type: object + properties: + jointTenancyIndication: + type: boolean + example: false + interestFractionNumerator: + type: string + example: "1" + interestFractionDenominator: + type: string + example: "1" + ownershipRemarks: + type: string + example: "" + titleOwners: + type: array + items: + type: object + properties: + lastNameOrCorpName1: + type: string + example: "COMPANY LTD." + givenName: + type: string + example: "" + incorporationNumber: + type: string + example: "111111" + occupationDescription: + type: string + example: "" + address: + type: object + properties: + addressLine1: + type: string + example: "742 EVERGREEN TERR" + addressLine2: + type: string + example: "" + city: + type: string + example: "VICTORIA" + province: + type: string + example: "BC" + provinceName: + type: string + example: "BRITISH COLUMBIA" + country: + type: string + example: "CANADA" + postalCode: + type: string + example: "V8X 4S7" + taxAuthorities: + type: array + items: + type: object + properties: + authorityName: + type: string + example: "Victoria, City of" + descriptionsOfLand: + type: array + items: + type: object + properties: + parcelIdentifier: + type: string + example: "123-456-789" + fullLegalDescription: + type: string + example: "" + parcelStatus: + type: string + example: "A" + legalNotationsOnTitle: + type: array + items: + type: object + properties: + legalNotationNumber: + type: string + example: "AA0000000" + status: + type: string + example: "ACTIVE" + legalNotation: + type: object + properties: + originalLegalNotationNumber: + type: string + example: "AA00000000" + legalNotationText: + type: string + example: "" + duplicateCertificatesOfTitle: + type: array + items: + type: string + titleTransfersOrDispositions: + type: array + items: + type: string + chargesOnTitle: + type: array + items: + type: object + properties: + chargeNumber: + type: string + example: "A0000" + status: + type: string + example: "REGISTERED" + enteredDate: + type: string + format: date-time + example: "2000-01-01T00:00:00Z" + interAlia: + type: string + example: "Yes" + chargeRemarks: + type: string + example: "" + charge: + type: object + properties: + chargeNumber: + type: string + example: "A0000" + transactionType: + type: string + example: "UNDERSURFACE RIGHTS" + applicationReceivedDate: + type: string + format: date-time + example: "1970-01-01T00:00:00Z" + chargeOwnershipGroups: + type: array + items: + type: object + properties: + jointTenancyIndication: + type: boolean + example: false + interestFractionNumerator: + type: string + example: "1" + interestFractionDenominator: + type: string + example: "1" + ownershipRemarks: + type: string + example: "" + chargeOwners: + type: array + items: + type: object + properties: + lastNameOrCorpName1: + type: string + example: "" + incorporationNumber: + type: string + example: "" + certificatesOfCharge: + type: array + items: + type: object + correctionsAltos1: + type: array + items: + type: object + properties: + number: + type: string + example: "A0000" + referenceDescription: + type: string + example: "" + enteredDate: + type: string + format: date-time + example: "1980-01-01T00:00:00Z" + correctionDate: + type: string + format: date-time + example: "1980-01-01T00:00:00Z" + correctionText: + type: string + example: "" + corrections: + type: array + items: + type: object + chargeRelease: + type: object + properties: {} + productOrderParameters: + type: object + properties: + includeCancelledInfo: + type: boolean + example: false + landTitleDistrictCode: + type: string + example: NW + titleNumber: + type: string + example: BX247931 + productType: + type: string + example: title + status: + type: string + example: Processing diff --git a/express-api/src/routes/notifications.swagger.yaml b/express-api/src/routes/notifications.swagger.yaml new file mode 100644 index 000000000..1b5a42cc8 --- /dev/null +++ b/express-api/src/routes/notifications.swagger.yaml @@ -0,0 +1,208 @@ +### PATHS ### +paths: + /notifications/queue: + get: + security: + - bearerAuth: [] + tags: + - Notifications + summary: Gets a paginated list of notifications related to a single project. + parameters: + - in: query + name: projectId + schema: + type: string + example: '33' + - in: query + name: page + schema: + type: integer + example: 0 + - in: query + name: quantity + schema: + type: integer + example: 1 + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + page: + type: number + example: 0 + pageSize: + type: number + example: 0 + items: + type: array + items: + $ref: '#/components/schemas/NotificationQueue' + '400': + description: Bad Request + content: + text/plain: + schema: + type: string + example: Could not parse filter. + '403': + description: Forbidden + content: + text/plain: + schema: + type: string + example: User cannot access project outside their agencies. + /notifications/queue/{id}: + put: + security: + - bearerAuth: [] + tags: + - Notifications + summary: Resends a notification based on its internal ID. + parameters: + - in: query + name: id + schema: + type: integer + description: The ID of the NotificationQueue record. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationQueue' + '404': + description: Not found + content: + text/plain: + type: string + example: Notification not found. + delete: + security: + - bearerAuth: [] + tags: + - Notifications + summary: Cancels a notification based on its internal ID. + parameters: + - in: query + name: id + schema: + type: integer + description: The ID of the NotificationQueue record. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationQueue' + '404': + description: Not found + content: + text/plain: + type: string + example: Notification not found. + +### SCHEMAS ### +components: + schemas: + NotificationQueue: + type: object + properties: + Id: + type: integer + format: int32 + description: "Primary key, auto-generated." + Key: + type: string + format: uuid + description: "Unique identifier for the notification queue entry." + Status: + type: integer + format: int32 + description: "Status of the notification." + Priority: + type: string + maxLength: 50 + description: "Priority of the notification." + Encoding: + type: string + maxLength: 50 + description: "Encoding type used for the notification." + SendOn: + type: string + format: date-time + description: "Timestamp when the notification is scheduled to be sent." + To: + type: string + maxLength: 500 + nullable: true + description: "Recipient email address." + Subject: + type: string + maxLength: 200 + description: "Subject of the notification." + BodyType: + type: string + maxLength: 50 + description: "Type of the body content (e.g., text, HTML)." + Body: + type: string + description: "The main content of the notification." + Bcc: + type: string + maxLength: 500 + nullable: true + description: "Bcc email addresses." + Cc: + type: string + maxLength: 500 + nullable: true + description: "Cc email addresses." + Tag: + type: string + maxLength: 50 + nullable: true + description: "Tag for categorizing the notification." + ProjectId: + type: integer + format: int32 + nullable: true + description: "Foreign key referencing the associated project." + Project: + $ref: '#/components/schemas/Project' + ToAgencyId: + type: integer + format: int32 + nullable: true + description: "Foreign key referencing the associated agency." + TemplateId: + type: integer + format: int32 + description: "Foreign key referencing the associated notification template." + Template: + $ref: '#/components/schemas/NotificationTemplate' + ChesMessageId: + type: string + format: uuid + nullable: true + description: "UUID for the CHES message." + ChesTransactionId: + type: string + format: uuid + nullable: true + description: "UUID for the CHES transaction." + required: + - Key + - Status + - Priority + - Encoding + - SendOn + - Subject + - BodyType + - Body + - TemplateId diff --git a/express-api/src/routes/notificationsRouter.ts b/express-api/src/routes/notificationsRouter.ts index 96d1d7cf4..59b07a368 100644 --- a/express-api/src/routes/notificationsRouter.ts +++ b/express-api/src/routes/notificationsRouter.ts @@ -1,11 +1,12 @@ +import { Roles } from '@/constants/roles'; import controllers from '@/controllers'; import activeUserCheck from '@/middleware/activeUserCheck'; import catchErrors from '@/utilities/controllerErrorWrapper'; +import { protectedRoute } from '@bcgov/citz-imb-sso-express'; import express from 'express'; const router = express.Router(); -export const DISPOSAL_API_ROUTE = '/projects/disposal'; export const NOTIFICATION_QUEUE_ROUTE = '/queue'; export const NOTIFICATION_TEMPLATE_ROUTE = '/templates'; @@ -19,10 +20,7 @@ router router .route(`${NOTIFICATION_QUEUE_ROUTE}/:id`) - .put(activeUserCheck, catchErrors(resendNotificationById)); - -router - .route(`${NOTIFICATION_QUEUE_ROUTE}/:id`) - .delete(activeUserCheck, catchErrors(cancelNotificationById)); + .put(protectedRoute([Roles.ADMIN]), activeUserCheck, catchErrors(resendNotificationById)) + .delete(protectedRoute([Roles.ADMIN]), activeUserCheck, catchErrors(cancelNotificationById)); export default router; diff --git a/express-api/src/routes/parcels.swagger.yaml b/express-api/src/routes/parcels.swagger.yaml new file mode 100644 index 000000000..286548e39 --- /dev/null +++ b/express-api/src/routes/parcels.swagger.yaml @@ -0,0 +1,316 @@ +### PATHS ### +paths: + /parcels: + get: + security: + - bearerAuth: [] + tags: + - Parcels + summary: Gets a paginated list of parcels based on supplied filter. + parameters: + - in: query + name: pid + schema: + type: integer + description: Should not include leading 0s or hyphens. + - in: query + name: classificationId + schema: + type: integer + - in: query + name: agencyId + description: Can also be an array of integers + schema: + type: integer + - in: query + name: administrativeAreaId + schema: + type: integer + - in: query + name: propertyTypeId + schema: + type: integer + - in: query + name: isSensitive + schema: + type: boolean + - in: query + name: page + schema: + type: integer + description: Page number of requested results. Only necessary if quantity is specified. + - in: query + name: quantity + schema: + type: integer + description: The column name on which the records will be sorted. + - in: query + name: sort + schema: + type: string + description: Either DESC or ASC. + - in: query + name: includeRelations + schema: + type: boolean + description: Flag to join and select foreign key records. + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Parcel' + '400': + description: Bad Request + content: + text/plain: + schema: + type: string + example: Could not parse filter. + post: + security: + - bearerAuth: [] + tags: + - Parcels + summary: Submits a new Parcel. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ParcelPost' + responses: + '201': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Parcel' + '400': + description: Bad Request + content: + text/plain: + schema: + type: string + example: 'PID must be a number and in the format #########' + '409': + description: Conflict + content: + text/plain: + schema: + type: string + example: Parcel already exists. + /parcels/{id}: + get: + security: + - bearerAuth: [] + tags: + - Parcels + summary: Gets a single Parcel that matches the Id. + parameters: + - in: path + name: id + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Parcel' + '400': + description: Bad Request + content: + text/plain: + type: string + example: Parcel ID was invalid. + '403': + description: Forbidden + content: + text/plain: + type: string + example: You are not authorized to view this parcel. + '404': + description: Not Found + content: + text/plain: + type: string + example: Parcel matching this internal ID not found. + put: + security: + - bearerAuth: [] + tags: + - Parcels + summary: Updates a single Parcel that matches the Id. + parameters: + - in: path + name: id + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Parcel' + '400': + description: Bad Request + content: + text/plain: + type: string + example: Parcel ID was invalid or mismatched with body. + '403': + description: Forbidden + content: + text/plain: + type: string + example: This agency change is not permitted. + '404': + description: Not Found + content: + text/plain: + type: string + example: Parcel matching this internal ID not found. + delete: + security: + - bearerAuth: [] + tags: + - Parcels + summary: Deletes a single Parcel that matches the Id. + parameters: + - in: path + name: id + type: integer + responses: + '200': + description: OK + '400': + description: Bad Request + content: + text/plain: + schema: + type: string + example: Parcel ID was invalid. + '403': + description: Forbidden + content: + text/plain: + schema: + type: string + example: Parcel is involved in one or more projects with ID(s) ... + '404': + description: Not Found + content: + text/plain: + schema: + type: string + example: Parcel does not exist. + +### SCHEMAS ### +components: + schemas: + ParcelPost: + type: object + description: Object submitted in body of POST request. + properties: + Name: + type: string + description: "Name of the property." + Description: + type: string + description: "Description of the property." + ClassificationId: + type: integer + format: int32 + description: "Identifier for the classification of the property." + AgencyId: + type: integer + format: int32 + description: "Identifier for the agency associated with the property." + AdministrativeAreaId: + type: integer + format: int32 + description: "Identifier for the administrative area." + IsSensitive: + type: boolean + description: "Indicates whether the property is sensitive." + IsVisibleToOtherAgencies: + type: boolean + description: "Indicates whether the property is visible to other agencies." + Location: + type: object + properties: + x: + type: number + format: float + description: "X coordinate of the property's location." + y: + type: number + format: float + description: "Y coordinate of the property's location." + PropertyTypeId: + type: integer + format: int32 + description: "Identifier for the type of the property." + Address1: + type: string + description: "Primary address of the property." + Postal: + type: string + description: "Postal code of the property." + PID: + type: integer + format: int64 + description: "Parcel identifier for the property." + PIN: + type: string + nullable: true + description: "Parcel identification number for the property." + LandArea: + type: number + format: float + description: "Land area of the property in hectares." + Evaluations: + type: array + items: + $ref: '#/components/schemas/EvaluationPost' + Fiscals: + type: array + items: + $ref: '#/components/schemas/FiscalPost' + Parcel: + allOf: + - $ref: '#/components/schemas/ParcelPost' + - $ref: '#/components/schemas/CommonEntityProperties' + - type: object + properties: + Id: + type: integer + example: 300 + SiteId: + type: string + example: null + description: > + Site ID from BC Geocoder. Not currently in use. + Evaluations: + type: array + items: + $ref: '#/components/schemas/ParcelEvaluation' + Fiscals: + type: array + items: + $ref: '#/components/schemas/ParcelFiscal' + Zoning: + type: string + nullable: true + description: "Zoning information of the property." + ZoningPotential: + type: string + nullable: true + description: "Potential zoning information for the property." + ParentParcelId: + type: integer + nullable: true + description: "Identifier for the parent parcel, if applicable. Not used in current PIMS." diff --git a/express-api/src/routes/projects.swagger.yaml b/express-api/src/routes/projects.swagger.yaml new file mode 100644 index 000000000..d0dfb5674 --- /dev/null +++ b/express-api/src/routes/projects.swagger.yaml @@ -0,0 +1,620 @@ +### PATHS ### +paths: + /projects: + get: + security: + - bearerAuth: [] + tags: + - Projects + summary: Gets a list of projects based on supplied filter. + description: > + Used primarily for displaying projects in a table. + Non-generic query values must be in this format: `{searchType},{searchValue}` + parameters: + - in: query + name: name + schema: + type: string + example: contains,TEST + - in: query + name: projectNumber + schema: + type: string + example: contains,SPP + - in: query + name: status + schema: + type: string + example: is,Denied + - in: query + name: agency + schema: + type: string + example: is,Real Property Division + - in: query + name: updatedOn + schema: + type: string + example: before,Fri Aug 16 2024 17:00:00 GMT-0700 (Pacific Daylight Time) + - in: query + name: updatedBy + schema: + type: string + example: contains,Parker + - in: query + name: market + schema: + type: string + example: contains,705 + - in: query + name: netBook + schema: + type: string + example: contains,100 + - in: query + name: quickFilter + schema: + type: string + description: Applied as an OR WHERE search on relevant columns. + - in: query + name: quantity + schema: + type: string + description: Number of records to be returned. Equivalent to page size. + - in: query + name: page + schema: + type: string + description: Page number of requested results. Only necessary if quantity is specified. + - in: query + name: sortKey + schema: + type: string + description: The column name on which the records will be sorted. + - in: query + name: sortOrder + schema: + type: string + description: Either DESC or ASC. + - in: query + name: excelExport + schema: + type: boolean + example: false + description: If true, returns in format for Excel export. + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + totalCount: + type: integer + example: 1 + data: + type: array + items: + $ref: '#/components/schemas/ProjectJoinView' + '400': + description: Bad Request + content: + text/plain: + schema: + type: string + example: Could not parse filter. + /projects/disposal: + post: + security: + - bearerAuth: [] + tags: + - Projects + summary: Submits a new project. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectPost' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectPostResponse' + '400': + description: Bad Request + content: + text/plain: + schema: + type: string + example: Projects must have a name. + '403': + description: Forbidden + content: + text/plain: + schema: + type: string + example: Projects can not be added by user with Auditor role. + '404': + description: Not Found + content: + text/plain: + schema: + type: string + example: Agency with ID 1 not found. + '500': + description: Internal Server Error + content: + text/plain: + schema: + type: string + example: Error creating project. + /projects/disposal/{id}: + get: + security: + - bearerAuth: [] + tags: + - Projects + summary: Gets a single project that matches the Id. + parameters: + - in: path + name: id + type: integer + required: true + responses: + '200': + description: OK. + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + '400': + description: Bad Request + content: + text/plain: + schema: + type: string + example: Project ID was invalid. + '403': + description: Forbidden + content: + text/plain: + schema: + type: string + example: You are not authorized to view this project. + '404': + description: Not Found + content: + text/plain: + schema: + type: string + example: Project matching this internal ID not found. + put: + security: + - bearerAuth: [] + tags: + - Projects + summary: Updates a single project that matches the Id. + parameters: + - in: path + name: id + type: integer + required: true + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + responses: + '200': + description: OK. + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + '400': + description: Bad Request + content: + text/plain: + schema: + type: string + example: Invalid Project Id. + '403': + description: Forbidden + content: + text/plain: + schema: + type: string + example: Projects only editable by Administrator role. + '404': + description: Not Found + content: + text/plain: + schema: + type: string + example: Project does not exist. + delete: + security: + - bearerAuth: [] + tags: + - Projects + summary: Deletes a single project that matches the Id. + parameters: + - in: path + name: id + type: integer + required: true + responses: + '200': + description: OK + '400': + description: Bad Request + content: + text/plain: + schema: + type: string + example: Invalid Project Id. + '403': + description: Forbidden + content: + text/plain: + schema: + type: string + example: Projects can only be deleted by Administrator role. + '404': + description: Not Found + content: + text/plain: + schema: + type: string + example: Agency not found. + +### SCHEMAS ### +components: + schemas: + ProjectJoinView: + type: object + properties: + Id: + type: integer + format: int32 + description: "Unique identifier for the project." + ProjectNumber: + type: string + description: "Project number assigned to the project." + example: SPP-1234 + Name: + type: string + description: "Name of the project." + StatusId: + type: integer + format: int32 + description: "Identifier for the status of the project." + Status: + type: string + description: "Current status of the project." + AgencyId: + type: integer + format: int32 + description: "Identifier for the agency associated with the project." + Agency: + type: string + description: "Name of the agency associated with the project." + Market: + type: string + description: "Market value of the project." + example: '$3.00' + NetBook: + type: string + description: "Net book value of the project." + example: '$3.00' + UpdatedBy: + type: string + description: "Name of the person who last updated the project." + UpdatedOn: + type: string + format: date-time + description: "Timestamp when the project was last updated." + example: '2024-08-13T04:26:18.889Z' + ProjectPost: + type: object + properties: + project: + type: object + properties: + Name: + type: string + description: "Name of the project." + TierLevelId: + type: integer + format: int32 + description: "Identifier for the tier level." + Description: + type: string + description: "Description of the project." + Assessed: + type: number + format: double + description: "Assessed value of the project." + NetBook: + type: number + format: double + description: "Net book value of the project." + Market: + type: number + format: double + description: "Market value of the project." + Appraised: + type: number + format: double + description: "Appraised value of the project." + ProgramCost: + type: number + format: double + description: "Program cost associated with the project." + SalesCost: + type: number + format: double + description: "Sales cost associated with the project." + ExemptionNote: + type: string + description: "Note for exemption reasons." + Approval: + type: boolean + description: "Indicates if the project is approved." + Tasks: + type: array + items: + $ref: '#/components/schemas/ProjectTaskPost' + ReportedFiscalYear: + type: integer + format: int32 + description: "The fiscal year the project was reported." + ActualFiscalYear: + type: integer + format: int32 + description: "The actual fiscal year of the project." + Monetaries: + type: array + items: + $ref: '#/components/schemas/ProjectMonetaryPost' + Notes: + type: array + items: + $ref: '#/components/schemas/ProjectNotePost' + projectProperties: + type: object + properties: + parcels: + type: array + items: + type: integer + format: int32 + description: "List of parcel IDs associated with the project." + buildings: + type: array + items: + type: integer + format: int32 + description: "List of building IDs associated with the project." + ProjectPostResponse: + allOf: + - $ref: '#/components/schemas/ProjectPost/properties/project' + - $ref: '#/components/schemas/CommonEntityProperties' + - type: object + properties: + Id: + type: integer + format: int32 + description: "Unique identifier for the project." + AgencyId: + type: integer + format: int32 + description: "Identifier for the agency associated with the project." + ProjectType: + type: integer + format: int32 + description: "Type of the project." + StatusId: + type: integer + format: int32 + description: "Identifier for the project status." + RiskId: + type: integer + format: int32 + description: "Identifier for the risk associated with the project." + ProjectNumber: + type: string + description: "Project number." + Project: + allOf: + - $ref: '#/components/schemas/ProjectPostResponse' + - type: object + properties: + Tasks: + type: array + items: + $ref: '#/components/schemas/ProjectTask' + Monetaries: + type: array + items: + $ref: '#/components/schemas/ProjectMonetary' + Notes: + type: array + items: + $ref: '#/components/schemas/ProjectNote' + Timestamps: + type: array + items: + $ref: '#/components/schemas/ProjectTimestamp' + AgencyResponses: + type: array + items: + $ref: '#/components/schemas/ProjectAgencyResponse' + Buildings: + type: array + items: + $ref: '#/components/schemas/Building' + Parcels: + type: array + items: + $ref: '#/components/schemas/Parcel' + ProjectProperties: + type: array + items: + $ref: '#/components/schemas/ProjectProperty' + Notifications: + type: array + items: + $ref: '#/components/schemas/NotificationQueue' + StatusHistory: + type: array + items: + $ref: '#/components/schemas/StatusHistory' + ProjectTask: + allOf: + - $ref: '#/components/schemas/ProjectTaskPost' + - $ref: '#/components/schemas/CommonEntityProperties' + - type: object + properties: + ProjectId: + type: integer + example: 1 + ProjectTaskPost: + type: object + properties: + TaskId: + type: integer + format: int32 + description: "Unique identifier for the task." + IsCompleted: + type: boolean + description: "Indicates if the task is completed." + ProjectMonetary: + allOf: + - $ref: '#/components/schemas/ProjectMonetaryPost' + - $ref: '#/components/schemas/CommonEntityProperties' + - type: object + properties: + ProjectId: + type: integer + example: 1 + ProjectMonetaryPost: + type: object + properties: + MonetaryTypeId: + type: integer + format: int32 + description: "Identifier for the type of monetary value." + Value: + type: number + format: double + description: "The monetary value." + ProjectNote: + allOf: + - $ref: '#/components/schemas/ProjectNotePost' + - $ref: '#/components/schemas/CommonEntityProperties' + - type: object + properties: + ProjectId: + type: integer + example: 1 + ProjectNotePost: + type: object + properties: + NoteTypeId: + type: integer + format: int32 + description: "Identifier for the type of note." + Note: + type: string + description: "The note content." + ProjectTimestamp: + allOf: + - $ref: '#/components/schemas/ProjectTimestampPost' + - $ref: '#/components/schemas/CommonEntityProperties' + - type: object + properties: + ProjectId: + type: integer + example: 1 + ProjectTimestampPost: + type: object + properties: + TimestampTypeId: + type: integer + format: int32 + description: "Identifier for the type of note." + Note: + type: string + description: "The note content." + ProjectAgencyResponse: + allOf: + - $ref: '#/components/schemas/CommonEntityProperties' + - type: object + properties: + AgencyId: + type: integer + example: 2 + Note: + type: string + NotificationId: + type: integer + example: 233 + OfferAmount: + type: integer + example: 233 + ProjectId: + type: integer + example: 1111 + ReceivedOn: + type: string + example: '2024-08-16T07:00:00.000Z' + Response: + type: integer + example: 0 + description: Corresponds to a response option. + ProjectProperty: + allOf: + - $ref: '#/components/schemas/CommonEntityProperties' + - type: object + properties: + Building: + $ref: '#/components/schemas/Building' + BuildingId: + type: integer + example: 213 + Id: + type: integer + example: 0 + Parcel: + $ref: '#/components/schemas/Parcel' + ParcelId: + type: integer + example: 213 + ProjectId: + type: integer + example: 123 + PropertyTypeId: + type: integer + example: 0 + StatusHistory: + allOf: + - $ref: '#/components/schemas/CommonEntityProperties' + - type: object + properties: + Id: + type: integer + example: 123 + ProjectId: + type: integer + example: 123 + StatusId: + type: integer + example: 123 + WorkflowId: + type: integer + example: 123 + diff --git a/express-api/src/routes/projectsRouter.ts b/express-api/src/routes/projectsRouter.ts index 8a655db71..3be65ea44 100644 --- a/express-api/src/routes/projectsRouter.ts +++ b/express-api/src/routes/projectsRouter.ts @@ -6,7 +6,6 @@ import express from 'express'; const router = express.Router(); export const PROJECT_DISPOSAL = '/disposal'; -export const PROJECT_REPORTS = '/reports'; const { getDisposalProject, diff --git a/express-api/src/routes/properties.swagger.yaml b/express-api/src/routes/properties.swagger.yaml new file mode 100644 index 000000000..014a16b23 --- /dev/null +++ b/express-api/src/routes/properties.swagger.yaml @@ -0,0 +1,521 @@ +### PATHS ### +paths: + /properties: + get: + security: + - bearerAuth: [] + tags: + - Properties + summary: Gets a list of properties based on supplied filter. + description: > + Used primarily for displaying properties in a table. + Non-generic query values must be in this format: `{searchType},{searchValue}` + parameters: + - in: query + name: pid + schema: + type: string + example: contains,123345566 + - in: query + name: pin + schema: + type: string + - in: query + name: classification + schema: + type: string + example: is,Demolished + - in: query + name: agency + schema: + type: string + example: is,Real Property Division + - in: query + name: propertyType + schema: + type: string + example: is,Parcel + - in: query + name: address + schema: + type: string + example: contains,742 Evergreen Terr + - in: query + name: administrativeArea + schema: + type: string + example: contains,Victoria + - in: query + name: landArea + schema: + type: string + example: contains,100 + - in: query + name: updatedOn + schema: + type: string + example: before,Fri Aug 16 2024 17:00:00 GMT-0700 (Pacific Daylight Time) + - in: query + name: quickFilter + schema: + type: string + description: Applied as an OR WHERE search on relevant columns. + - in: query + name: quantity + schema: + type: string + description: Number of records to be returned. Equivalent to page size. + - in: query + name: page + schema: + type: string + description: Page number of requested results. Only necessary if quantity is specified. + - in: query + name: sortKey + schema: + type: string + description: The column name on which the records will be sorted. + - in: query + name: sortOrder + schema: + type: string + description: Either DESC or ASC. + - in: query + name: excelExport + schema: + type: boolean + example: false + description: If true, returns in format for Excel export. + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + totalCount: + type: integer + example: 1 + data: + type: array + items: + $ref: '#/components/schemas/PropertyJoinView' + '400': + description: Bad Request + content: + text/plain: + schema: + type: string + example: Could not parse filter. + /properties/search/fuzzy: + get: + security: + - bearerAuth: [] + tags: + - Properties + summary: Gets a list of surplus properties based on supplied keyword. + description: > + Used for searching for surplus properties in projects. + parameters: + - in: query + name: keyword + schema: + type: string + example: health + - in: query + name: take + schema: + type: string + example: 4 + description: The number of parcels and buildings (each) to be returned. + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + Parcels: + type: array + items: + $ref: '#/components/schemas/Parcel' + Buildings: + type: array + items: + $ref: '#/components/schemas/Building' + /properties/search/geo: + get: + security: + - bearerAuth: [] + tags: + - Properties + summary: Gets a list of properties in geojson format. + description: > + Used for populating the map component. + parameters: + - in: query + name: PID + description: Should not include leading zeroes or hyphens. + schema: + type: integer + example: 24545457 + - in: query + name: PIN + schema: + type: integer + - in: query + name: Address + schema: + type: string + - in: query + name: Name + schema: + type: string + - in: query + name: AgencyIds + description: Comma-separated list of IDs. + schema: + type: string + example: 81,166 + - in: query + name: AdministrativeAreaIds + description: Comma-separated list of IDs. + schema: + type: string + example: 219,300 + - in: query + name: ClassificationIds + description: Comma-separated list of IDs. + schema: + type: string + example: 0,1 + - in: query + name: PropertyTypeIds + description: Comma-separated list of IDs. + schema: + type: string + example: 0,1 + - in: query + name: RegionalDistrictIds + description: Comma-separated list of IDs. + schema: + type: string + example: 19,25 + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PropertyGeojson' + /properties/import: + post: + security: + - bearerAuth: [] + tags: + - Properties + summary: Submits a CSV file of properties to be imported. + description: > + Starts the Bulk Upload process. + requestBody: + content: + file/csv: + schema: + type: string + format: binary + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ImportResult' + '400': + description: Bad Request + content: + text/plain: + schema: + type: string + example: Could not parse file. + /properties/import/results: + get: + security: + - bearerAuth: [] + tags: + - Properties + summary: Retrieves a list of previous property import results. + parameters: + - in: query + name: page + schema: + type: integer + example: 0 + - in: query + name: quantity + schema: + type: integer + example: 1 + - in: query + name: sortKey + schema: + type: string + example: CreatedOn + - in: query + name: sortOrder + schema: + type: string + example: DESC + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ImportResult' + '400': + description: Bad Request + content: + text/plain: + schema: + type: string + example: Could not parse filter. + +### SCHEMAS ### +components: + schemas: + ImportResult: + allOf: + - $ref: '#/components/schemas/CommonEntityProperties' + - type: object + properties: + CompletionPercentage: + type: number + example: 100 + description: Will be -1 if the import process encounters an error. + FileName: + type: string + example: file.csv + Id: + type: integer + example: 10 + Results: + type: array + items: + type: object + properties: + action: + type: string + example: error + reason: + type: string + example: Missing PID. + PropertyGeojson: + type: object + properties: + type: + type: string + example: Feature + geometry: + type: object + properties: + type: + type: string + example: Point + coordinates: + type: array + example: [-123.416, 48.491] + properties: + type: object + properties: + Address1: + type: string + example: 742 Evergreen Terr + AdministrativeAreaId: + type: integer + example: 300 + AgencyId: + type: integer + example: 81 + ClassificationId: + type: integer + example: 0 + Id: + type: integer + example: 1792 + Location: + type: object + properties: + x: + type: number + example: -123.416 + y: + type: number + example: 48.491 + Name: + type: string + example: Building Number 5 + PID: + type: integer + example: 123456789 + PIN: + type: integer + example: null + PropertyTypeId: + type: integer + example: 1 + PropertyJoinView: + type: object + properties: + Address: + type: string + example: 742 Evergreen Terr + AdministrativeArea: + type: string + example: Victoria + AdministrativeAreaId: + type: integer + example: 300 + Agency: + type: string + example: Real Property Division + AgencyId: + type: integer + example: 110 + Classification: + type: string + example: Surplus Active + ClassificationId: + type: integer + example: 2 + Id: + type: integer + example: 11111 + description: Internal ID for parcel or building. + IsSensitive: + type: boolean + example: false + LandArea: + type: integer + example: 99 + description: Area in hectres. + PID: + type: integer + example: 111111111 + PIN: + type: integer + example: 123123 + PropertyType: + type: string + example: Parcel + PropertyTypeId: + type: integer + example: 0 + UpdatedOn: + type: string + format: date-time + example: '2024-08-21T05:01:43.735Z' + EvaluationCommon: + type: object + properties: + EvaluationKeyId: + type: integer + example: 0 + Value: + type: number + example: 100 + Year: + type: integer + example: 2024 + EvaluationPost: + allOf: + - $ref: '#/components/schemas/EvaluationCommon' + - type: object + properties: + isNew: + type: boolean + value: true + Evaluation: + allOf: + - $ref: '#/components/schemas/EvaluationCommon' + - $ref: '#/components/schemas/CommonEntityProperties' + - type: object + properties: + Note: + type: string + example: 'Evaulation note' + isNew: null # needed specifically so it doesn't show up + BuildingEvaluation: + allOf: + - $ref: '#/components/schemas/Evaluation' + - type: object + properties: + BuildingId: + type: integer + example: 1 + ParcelEvaluation: + allOf: + - $ref: '#/components/schemas/Evaluation' + - type: object + properties: + ParcelId: + type: integer + example: 1 + FiscalCommon: + type: object + properties: + EffectiveDate: + type: string + example: '2024-03-09T08:00:00.000Z' + FiscalKeyId: + type: integer + example: 0 + FiscalYear: + type: string + example: 2024 + description: Is a string due to historical 24/23 storage. + Value: + type: number + example: 100 + isNew: + type: boolean + example: true + FiscalPost: + allOf: + - $ref: '#/components/schemas/FiscalCommon' + - type: object + properties: + isNew: + type: boolean + value: true + Fiscal: + allOf: + - $ref: '#/components/schemas/FiscalCommon' + - $ref: '#/components/schemas/CommonEntityProperties' + - type: object + properties: + Note: + type: string + example: 'Fiscal note' + isNew: null # needed specifically so it doesn't show up + BuildingFiscal: + allOf: + - $ref: '#/components/schemas/Fiscal' + - type: object + properties: + BuildingId: + type: integer + example: 1 + ParcelFiscal: + allOf: + - $ref: '#/components/schemas/Fiscal' + - type: object + properties: + ParcelId: + type: integer + example: 1 diff --git a/express-api/src/routes/reports.swagger.yaml b/express-api/src/routes/reports.swagger.yaml new file mode 100644 index 000000000..f711654ad --- /dev/null +++ b/express-api/src/routes/reports.swagger.yaml @@ -0,0 +1,73 @@ +### PATHS ### +paths: + /reports/error: + post: + security: + - bearerAuth: [] + tags: + - Reports + summary: Submits an error report. + description: > + Used for the ErrorFallback page. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorReport' + responses: + '200': + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ErrorReport' + - type: object + properties: + chesResponse: + $ref: '#/components/schemas/ChesSentEmailResponse' + + +### SCHEMAS ### +components: + schemas: + ErrorReport: + type: object + properties: + user: + type: object + properties: + client_roles: + type: array + example: ['General User'] + email: + type: string + example: email@gov.bc.ca + preferred_username: + type: string + example: username@idir + display_name: + type: string + example: 'Doe, John CITZ:EX' + first_name: + type: string + example: John + last_name: + type: string + example: Doe + userMessage: + type: string + example: I found an error! + error: + type: object + properties: + message: + type: string + example: Error at line 42. + stack: + type: string + timestamp: + type: string + format: date-time + url: + type: string + example: pims.gov.bc.ca/parcels/123 diff --git a/express-api/src/routes/rolesRouter.ts b/express-api/src/routes/rolesRouter.ts deleted file mode 100644 index a087159c6..000000000 --- a/express-api/src/routes/rolesRouter.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Roles } from '@/constants/roles'; -import controllers from '@/controllers'; -import activeUserCheck from '@/middleware/activeUserCheck'; -import catchErrors from '@/utilities/controllerErrorWrapper'; -import { protectedRoute } from '@bcgov/citz-imb-sso-express'; -import express from 'express'; - -const router = express.Router(); - -const { addRole, deleteRoleById, getRoleById, getRoles, updateRoleById } = controllers; - -// Endpoints for Roles -router.route(`/`).get(catchErrors(getRoles)).post(activeUserCheck, catchErrors(addRole)); - -router - .route(`/:id`) - .get(catchErrors(getRoleById)) - .put(protectedRoute([Roles.ADMIN]), activeUserCheck, catchErrors(updateRoleById)) - .delete(protectedRoute([Roles.ADMIN]), activeUserCheck, catchErrors(deleteRoleById)); - -export default router; diff --git a/express-api/src/routes/tools.swagger.yaml b/express-api/src/routes/tools.swagger.yaml new file mode 100644 index 000000000..37c42389e --- /dev/null +++ b/express-api/src/routes/tools.swagger.yaml @@ -0,0 +1,123 @@ +### PATHS ### +paths: + /tools/geocoder/parcels/pids/{siteID}: + get: + security: + - bearerAuth: [] + tags: + - Tools + summary: Returns an object with the siteID and a pids property. + description: > + The `pids` property seems to only be a single PID string. + This response comes from the BC Geocoder Service. + Capable of any error code from BC Geocoder. + parameters: + - in: path + name: siteID + schema: + type: string + format: uuid + responses: + '200': + content: + application/json: + schema: + type: object + properties: + siteID: + type: string + format: uuid + pids: + type: string + example: 014128446 + description: Has leading zeroes. Seemingly only one PID despite pluralization. + /tools/geocoder/addresses: + get: + security: + - bearerAuth: [] + tags: + - Tools + summary: Returns a list of matching addresses from BC Geocoder. + description: > + This response comes from the BC Geocoder Service. + Capable of any error code from BC Geocoder. + parameters: + - in: path + name: address + schema: + type: string + example: 742 Evergreen Terr + - in: path + name: minScore + schema: + type: integer + example: 30 + - in: path + name: maxResults + schema: + type: integer + example: 5 + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/GeocoderAddress' + '400': + content: + text/plain: + schema: + type: string + example: Failed to fetch data +### SCHEMAS ### +components: + schemas: + GeocoderAddress: + type: object + properties: + siteId: + type: string + format: uuid + example: d2430ab0-d79c-4201-a386-226749028653 + fullAddress: + type: string + example: 1010 Bristol Rd, Saanich, BC + address1: + type: string + example: 1010 Bristol Rd + administrativeArea: + type: string + example: Saanich + provinceCode: + type: string + example: BC + longitude: + type: number + format: double + example: -123.3637344 + latitude: + type: number + format: double + example: 48.461893 + score: + type: integer + example: 89 + ChesSentEmailResponse: + type: object + properties: + messages: + type: array + items: + type: object + properties: + msgId: + type: string + format: uuid + to: + type: string + example: email@gov.bc.ca + txId: + type: string + format: uuid diff --git a/express-api/src/routes/toolsRouter.ts b/express-api/src/routes/toolsRouter.ts index 19668b8e0..0624b4ba7 100644 --- a/express-api/src/routes/toolsRouter.ts +++ b/express-api/src/routes/toolsRouter.ts @@ -5,21 +5,7 @@ import express from 'express'; const router = express.Router(); -const { - sendChesMessage, - getChesMessageStatusById, - getChesMessageStatuses, - cancelChesMessageById, - cancelChesMessages, - searchGeocoderAddresses, - searchGeocoderSiteId, -} = controllers; - -router.route(`/ches`).post(activeUserCheck, catchErrors(sendChesMessage)); -router.route(`/ches/status/`).get(activeUserCheck, catchErrors(getChesMessageStatuses)); -router.route(`/ches/status/:messageId`).get(activeUserCheck, catchErrors(getChesMessageStatusById)); -router.route(`/ches/cancel/:messageId`).delete(activeUserCheck, catchErrors(cancelChesMessageById)); -router.route(`/ches/cancel/`).delete(activeUserCheck, catchErrors(cancelChesMessages)); +const { searchGeocoderAddresses, searchGeocoderSiteId } = controllers; router.route(`/geocoder/addresses`).get(activeUserCheck, catchErrors(searchGeocoderAddresses)); router diff --git a/express-api/src/routes/users.swagger.yaml b/express-api/src/routes/users.swagger.yaml new file mode 100644 index 000000000..a88fb5cbc --- /dev/null +++ b/express-api/src/routes/users.swagger.yaml @@ -0,0 +1,388 @@ +### PATHS ### +paths: + /users: + get: + security: + - bearerAuth: [] + tags: + - Users + summary: Returns a list of users in PIMS. + description: > + The string parameters are looking for exact matches. + Partial matches will not work. + parameters: + - in: query + name: page + schema: + type: integer + example: 0 + - in: query + name: quantity + schema: + type: integer + example: 100 + - in: query + name: sortOrder + schema: + type: string + example: ASC + - in: query + name: sortKey + schema: + type: string + example: FirstName + - in: query + name: id + schema: + type: string + format: uuid + - in: query + name: username + schema: + type: string + example: d2430ab0d79c4201a386226749028653@idir + - in: query + name: displayName + schema: + type: string + example: Doe, John + - in: query + name: lastName + schema: + type: string + example: Doe + - in: query + name: firstName + schema: + type: string + example: John + - in: query + name: email + schema: + type: string + example: email@gov.bc.ca + - in: query + name: agencyId + schema: + type: integer + example: 100 + - in: query + name: agency + schema: + type: string + example: Real Property + - in: query + name: role + schema: + type: string + example: General User + - in: query + name: position + schema: + type: string + example: Superstar + - in: query + name: guid + schema: + type: string + format: uuid + - in: query + name: status + schema: + type: string + example: Active + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + '400': + description: Bad Request + content: + text/plain: + schema: + type: string + example: Failed to parse filter query. + /users/{id}: + get: + security: + - bearerAuth: [] + tags: + - Users + summary: Returns a single user with the provided ID. + parameters: + - in: path + name: id + schema: + type: string + format: uuid + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + description: Bad Request + content: + text/plain: + schema: + type: string + example: Could not parse UUID. + '403': + description: Forbidden + content: + text/plain: + schema: + type: string + example: User does not have permission to view this user. + '404': + description: Not Found + content: + text/plain: + schema: + type: string + example: User not found. + put: + security: + - bearerAuth: [] + tags: + - Users + summary: Updates a single user with the provided ID. + parameters: + - in: path + name: id + schema: + type: string + format: uuid + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserPut' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + description: Bad Request + content: + text/plain: + schema: + type: string + example: Could not parse UUID. + '404': + description: Not Found + content: + text/plain: + schema: + type: string + example: User not found. + /users/self: + get: + security: + - bearerAuth: [] + tags: + - Users + summary: Returns a single user matching the requesting user. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '204': + description: No Content + /users/access/request: + post: + security: + - bearerAuth: [] + tags: + - Users + summary: Adds a user with OnHold status. + description: > + Most user info comes from the user's token. + requestBody: + content: + application/json: + schema: + type: object + properties: + AgencyId: + type: integer + example: 2 + Position: + type: string + example: Superstar + Note: + type: string + example: Please give me access. + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/User' + /users/agencies/{username}: + get: + security: + - bearerAuth: [] + tags: + - Users + summary: Returns an array of agencies that the user has permissions in. + description: > + This can include the user's agency and it's children if the user + belongs to a parent agency. + parameters: + - in: path + name: username + schema: + type: string + example: 59fa487c44c546468f73716ab6fd265f@idir + responses: + '200': + content: + application/json: + schema: + type: array + items: + type: integer + +### SCHEMAS ### +components: + schemas: + UserPut: + type: object + properties: + Id: + type: string + format: uuid + FirstName: + type: string + example: John + LastName: + type: string + example: Doe + Email: + type: string + format: email + example: john.doe@gov.bc.ca + Position: + type: string + example: Architect + AgencyId: + type: integer + example: 2 + Status: + type: string + example: Disabled + Role: + type: object + properties: + Id: + type: string + format: uuid + Description: + type: string + Name: + type: string + example: General User + User: + allOf: + - $ref: '#/components/schemas/CommonEntityProperties' + - type: object + properties: + Id: + type: string + format: uuid + example: 59fa487c-44c5-4646-8f73-716ab6fd265f + Username: + type: string + example: 59fa487c44c546468f73716ab6fd265f@idir + DisplayName: + type: string + example: Doe, John + FirstName: + type: string + example: John + MiddleName: + type: string + nullable: true + example: null + LastName: + type: string + example: Doe + Email: + type: string + format: email + example: john.doe@gov.bc.ca + Position: + type: string + example: Architect + IsDisabled: + type: boolean + example: false + Note: + type: string + nullable: true + example: A real troublemaker. + LastLogin: + type: string + format: date-time + ApprovedById: + type: string + format: uuid + nullable: true + example: null + ApprovedOn: + type: string + format: date-time + nullable: true + example: null + KeycloakUserId: + type: string + format: uuid + example: 59fa487c-44c5-4646-8f73-716ab6fd265f + AgencyId: + type: integer + example: 2 + Agency: + $ref: '#/components/schemas/Agency' + RoleId: + type: string + format: uuid + Role: + $ref: '#/components/schemas/Role' + Status: + type: string + example: Active + Role: + allOf: + - $ref: '#/components/schemas/CommonEntityProperties' + - type: object + properties: + Id: + type: string + format: uuid + Description: + type: string + example: This role does x, y, and z. + IsDisabled: + type: boolean + example: false + Name: + type: string + example: General User + SortOrder: + type: integer + example: 0 diff --git a/express-api/src/routes/usersRouter.ts b/express-api/src/routes/usersRouter.ts index 6f1a0a9d9..ae188d8bc 100644 --- a/express-api/src/routes/usersRouter.ts +++ b/express-api/src/routes/usersRouter.ts @@ -7,42 +7,18 @@ import express from 'express'; const router = express.Router(); -const { - getUserInfo, - getSelf, - submitUserAccessRequest, - getUserAgencies, - addUser, - deleteUserById, - getUserById, - getUserRolesByName, - getUsers, - updateUserById, - getAllRoles, - updateUserRolesByName, -} = controllers; +const { getSelf, submitUserAccessRequest, getUserAgencies, getUserById, getUsers, updateUserById } = + controllers; -router.route(`/info`).get(catchErrors(getUserInfo)); router.route(`/self`).get(catchErrors(getSelf)); router.route(`/access/requests`).post(catchErrors(submitUserAccessRequest)); router.route(`/agencies/:username`).get(activeUserCheck, catchErrors(getUserAgencies)); -router - .route(`/`) - .get(activeUserCheck, catchErrors(getUsers)) - .post(protectedRoute([Roles.ADMIN]), activeUserCheck, catchErrors(addUser)); - -router.route(`/roles`).get(activeUserCheck, catchErrors(getAllRoles)); - -router - .route(`/roles/:username`) - .get(activeUserCheck, catchErrors(getUserRolesByName)) - .put(protectedRoute([Roles.ADMIN]), activeUserCheck, catchErrors(updateUserRolesByName)); +router.route(`/`).get(activeUserCheck, catchErrors(getUsers)); router .route(`/:id`) .get(activeUserCheck, catchErrors(getUserById)) - .put(protectedRoute([Roles.ADMIN]), activeUserCheck, catchErrors(updateUserById)) - .delete(protectedRoute([Roles.ADMIN]), activeUserCheck, catchErrors(deleteUserById)); + .put(protectedRoute([Roles.ADMIN]), activeUserCheck, catchErrors(updateUserById)); export default router; diff --git a/express-api/src/services/administrativeAreas/administrativeAreaSchema.ts b/express-api/src/services/administrativeAreas/administrativeAreaSchema.ts index 259cc30ac..a48af739f 100644 --- a/express-api/src/services/administrativeAreas/administrativeAreaSchema.ts +++ b/express-api/src/services/administrativeAreas/administrativeAreaSchema.ts @@ -5,13 +5,9 @@ export const AdministrativeAreaFilterSchema = z.object({ quantity: z.coerce.number().optional(), sortKey: z.string().optional(), sortOrder: z.string().optional(), - sortRelation: z.string().optional(), name: z.string().optional(), - provinceId: z.string().optional(), - regionalDistrictId: z.number().int().optional(), regionalDistrictName: z.string().optional(), isDisabled: z.string().optional(), - updatedOn: z.string().optional(), createdOn: z.string().optional(), quickFilter: z.string().optional(), }); diff --git a/express-api/src/services/administrativeAreas/administrativeAreasServices.ts b/express-api/src/services/administrativeAreas/administrativeAreasServices.ts index cc705f1ce..5b94c5b98 100644 --- a/express-api/src/services/administrativeAreas/administrativeAreasServices.ts +++ b/express-api/src/services/administrativeAreas/administrativeAreasServices.ts @@ -39,7 +39,6 @@ const collectFindOptions = (filter: AdministrativeAreaFilter) => { if (filter.isDisabled) options.push(constructFindOptionFromQueryBoolean('IsDisabled', filter.isDisabled)); if (filter.createdOn) options.push(constructFindOptionFromQuery('CreatedOn', filter.createdOn)); - if (filter.updatedOn) options.push(constructFindOptionFromQuery('UpdatedOn', filter.updatedOn)); return options; }; @@ -54,7 +53,7 @@ const getAdministrativeAreas = async (filter: AdministrativeAreaFilter) => { .createQueryBuilder() .where( new Brackets((qb) => { - options.forEach((option) => qb.orWhere(option)); + options.forEach((option) => qb.andWhere(option)); }), ); @@ -101,7 +100,7 @@ const addAdministrativeArea = async (adminArea: AdministrativeArea) => { where: [{ Id: adminArea.Id }, { Name: adminArea.Name }], }); if (existing) { - throw new ErrorWithCode('Administrative area already exists.'); + throw new ErrorWithCode('Administrative area already exists.', 409); } return AppDataSource.getRepository(AdministrativeArea).save(adminArea); }; diff --git a/express-api/src/services/agencies/agencySchema.ts b/express-api/src/services/agencies/agencySchema.ts index 71bbc4940..6ee0c7ca2 100644 --- a/express-api/src/services/agencies/agencySchema.ts +++ b/express-api/src/services/agencies/agencySchema.ts @@ -22,20 +22,17 @@ export const AgencyCreationSchema = z.object({ export const AgencyFilterSchema = z.object({ name: z.string().optional(), - parentId: z.coerce.number().int().optional(), parentName: z.string().optional(), isDisabled: z.string().optional(), - sortOrder: z.string().optional(), - page: z.coerce.number().optional(), - quantity: z.coerce.number().optional(), - sortKey: z.string().optional(), - id: z.coerce.number().optional(), - status: z.string().optional(), email: z.string().optional(), sendEmail: z.string().optional(), updatedOn: z.string().optional(), createdOn: z.string().optional(), code: z.string().optional(), + page: z.coerce.number().optional(), + quantity: z.coerce.number().optional(), + sortKey: z.string().optional(), + sortOrder: z.string().optional(), quickFilter: z.string().optional(), }); diff --git a/express-api/src/services/agencies/agencyServices.ts b/express-api/src/services/agencies/agencyServices.ts index 417ec8104..5606dd404 100644 --- a/express-api/src/services/agencies/agencyServices.ts +++ b/express-api/src/services/agencies/agencyServices.ts @@ -61,7 +61,7 @@ export const getAgencies = async (filter: AgencyFilter) => { .createQueryBuilder() .where( new Brackets((qb) => { - options.forEach((option) => qb.orWhere(option)); + options.forEach((option) => qb.andWhere(option)); }), ); diff --git a/express-api/src/services/buildings/buildingSchema.ts b/express-api/src/services/buildings/buildingSchema.ts index 99c83a0c0..0efdf7cb2 100644 --- a/express-api/src/services/buildings/buildingSchema.ts +++ b/express-api/src/services/buildings/buildingSchema.ts @@ -5,16 +5,17 @@ export const BuildingFilterSchema = z.object({ classificationId: z.coerce.number().nonnegative().optional(), agencyId: z.coerce.number().nonnegative().optional() || - z.array(z.number().int().nonnegative()).optional(), + z.array(z.coerce.number().int().nonnegative()).optional(), administrativeAreaId: z.coerce.number().nonnegative().optional(), propertyTypeId: z.coerce.number().nonnegative().optional(), buildingConstructionTypeId: z.coerce.number().nonnegative().optional(), buildingPredominateUseId: z.coerce.number().nonnegative().optional(), buildingOccupantTypeId: z.coerce.number().nonnegative().optional(), - isSensitive: z.boolean().optional(), + isSensitive: z.coerce.boolean().optional(), page: z.coerce.number().optional(), quantity: z.coerce.number().optional(), sort: z.string().optional(), + includeRelations: z.coerce.boolean().optional(), }); export type BuildingFilter = z.infer; diff --git a/express-api/src/services/buildings/buildingServices.ts b/express-api/src/services/buildings/buildingServices.ts index 2f61a6592..7c9fe6d61 100644 --- a/express-api/src/services/buildings/buildingServices.ts +++ b/express-api/src/services/buildings/buildingServices.ts @@ -20,6 +20,7 @@ const buildingRepo = AppDataSource.getRepository(Building); * @throws {ErrorWithCode} If the building already exists or is unable to be added. */ export const addBuilding = async (building: DeepPartial) => { + // TODO: Why would an incoming building have an id? Is there a better way to check for existing buildings? const existingBuilding = building.Id ? await getBuildingById(building.Id) : null; if (existingBuilding) { throw new ErrorWithCode('Building already exists.', 409); @@ -174,7 +175,7 @@ export const deleteBuildingById = async (buildingId: number, username: string) = await queryRunner.rollbackTransaction(); logger.warn(e.message); if (e instanceof ErrorWithCode) throw e; - throw new ErrorWithCode(`Error updating project: ${e.message}`, 500); + throw new ErrorWithCode(`Error updating building: ${e.message}`, 500); } finally { await queryRunner.release(); } diff --git a/express-api/src/services/geocoder/geocoderService.ts b/express-api/src/services/geocoder/geocoderService.ts index 673a5037c..29d0249ac 100644 --- a/express-api/src/services/geocoder/geocoderService.ts +++ b/express-api/src/services/geocoder/geocoderService.ts @@ -51,7 +51,7 @@ export const getSiteAddresses = async ( const featureCollection: IFeatureCollectionModel = responseData; const addressInformation: IAddressModel[] = featureCollection.features.map((feature) => mapFeatureToAddress(feature), - ); // mapFeatureToAddress(featureCollection.features[0]); + ); return addressInformation; }; diff --git a/express-api/src/services/notifications/notificationServices.ts b/express-api/src/services/notifications/notificationServices.ts index 2bfdfdb97..fc7914d55 100644 --- a/express-api/src/services/notifications/notificationServices.ts +++ b/express-api/src/services/notifications/notificationServices.ts @@ -726,7 +726,7 @@ const generateProjectWatchNotifications = async ( return notificationsInserted; }; -const cancelNotificationById = async (id: number) => { +const cancelNotificationById = async (id: number, user: User) => { const notification = await AppDataSource.getRepository(NotificationQueue).findOne({ where: { Id: id }, }); @@ -735,6 +735,7 @@ const cancelNotificationById = async (id: number) => { return AppDataSource.getRepository(NotificationQueue).save({ Id: notification.Id, Status: NotificationStatus.Cancelled, + UpdatedById: user.Id, }); } else { return notification; diff --git a/express-api/src/services/parcels/parcelServices.ts b/express-api/src/services/parcels/parcelServices.ts index f423df0a1..f99cde25d 100644 --- a/express-api/src/services/parcels/parcelServices.ts +++ b/express-api/src/services/parcels/parcelServices.ts @@ -24,7 +24,7 @@ const addParcel = async (parcel: DeepPartial) => { const stringPID = numberPID.toString(); if (parcel.PID != null && (stringPID.length > 9 || isNaN(numberPID))) { - throw new ErrorWithCode('PID must be a number and in the format #########'); + throw new ErrorWithCode('PID must be a number and in the format #########', 400); } const existingParcel = parcel.PID != null ? await getParcelByPid(numberPID) : undefined; diff --git a/express-api/src/services/projects/projectsServices.ts b/express-api/src/services/projects/projectsServices.ts index 275e0710e..5d9297d3f 100644 --- a/express-api/src/services/projects/projectsServices.ts +++ b/express-api/src/services/projects/projectsServices.ts @@ -911,7 +911,7 @@ const getProjects = async (filter: ProjectFilter) => { .createQueryBuilder() .where( new Brackets((qb) => { - options.forEach((option) => qb.orWhere(option)); + options.forEach((option) => qb.andWhere(option)); }), ); diff --git a/express-api/src/services/properties/propertiesServices.ts b/express-api/src/services/properties/propertiesServices.ts index cea45ea9d..2c0149f1b 100644 --- a/express-api/src/services/properties/propertiesServices.ts +++ b/express-api/src/services/properties/propertiesServices.ts @@ -805,7 +805,7 @@ const getPropertiesUnion = async (filter: PropertyUnionFilter) => { .createQueryBuilder() .where( new Brackets((qb) => { - options.forEach((option) => qb.orWhere(option)); + options.forEach((option) => qb.andWhere(option)); }), ); diff --git a/express-api/src/services/users/usersServices.ts b/express-api/src/services/users/usersServices.ts index f623092c9..4016cdb8f 100644 --- a/express-api/src/services/users/usersServices.ts +++ b/express-api/src/services/users/usersServices.ts @@ -42,38 +42,13 @@ const normalizeKeycloakUser = (kcUser: SSOUser): NormalizedKeycloakUser => { }; }; -/** - * Activates a user in the system based on the provided SSO user information. - * If the user does not exist internally, creates a new user with the normalized SSO user data. - * If the user already exists, updates the last login timestamp and Keycloak user ID. - * @param ssoUser The SSO user information to activate in the system. - * @returns {Promise} A promise that resolves once the user is activated or updated. - */ -const activateUser = async (ssoUser: SSOUser) => { - const normalizedUser = normalizeKeycloakUser(ssoUser); - const internalUser = await getUser(ssoUser.preferred_username); - if (!internalUser) { - const { first_name, last_name, username, guid } = normalizedUser; - AppDataSource.getRepository(User).insert({ - Username: username, - FirstName: first_name, - LastName: last_name, - KeycloakUserId: guid, - }); - } else { - internalUser.LastLogin = new Date(); - internalUser.KeycloakUserId = normalizedUser.guid; - AppDataSource.getRepository(User).update({ Id: internalUser.Id }, internalUser); - } -}; - /** * Adds a user from Keycloak to the system with 'OnHold' status. * @param ssoUser The Keycloak user to be added * @param agencyId The ID of the agency the user belongs to * @param position The position of the user * @param note Additional notes about the user - * @returns The generated map of the inserted user + * @returns The inserted user */ const addKeycloakUserOnHold = async ( ssoUser: SSOUser, @@ -89,8 +64,9 @@ const addKeycloakUserOnHold = async ( const systemUser = await AppDataSource.getRepository(User).findOne({ where: { Username: 'system' }, }); - const result = await AppDataSource.getRepository(User).insert({ - Id: randomUUID(), + const id = randomUUID(); + await AppDataSource.getRepository(User).insert({ + Id: id, FirstName: normalizedKc.first_name, LastName: normalizedKc.last_name, Email: normalizedKc.email, @@ -107,7 +83,8 @@ const addKeycloakUserOnHold = async ( CreatedById: systemUser.Id, LastLogin: new Date(), }); - return result.generatedMaps[0]; + const newUser = await AppDataSource.getRepository(User).findOne({ where: { Id: id } }); + return newUser; }; /** @@ -190,6 +167,9 @@ const getUsers = async (filter: UserFiltering) => { AgencyId: filter.agencyId ? In(typeof filter.agencyId === 'number' ? [filter.agencyId] : filter.agencyId) : undefined, + Agency: { + Name: filter.agency, + }, Role: { Name: filter.role, }, @@ -246,14 +226,15 @@ const updateUser = async (user: DeepPartial) => { if (!resource) { throw new ErrorWithCode('Resource does not exist.', 404); } - const retUser = await AppDataSource.getRepository(User).update(user.Id, { + await AppDataSource.getRepository(User).update(user.Id, { ...user, DisplayName: `${user.LastName}, ${user.FirstName}`, }); if (roleName) { await KeycloakService.updateKeycloakUserRoles(resource.Username, [roleName]); } - return retUser.generatedMaps[0]; + const retUser = await AppDataSource.getRepository(User).findOne({ where: { Id: user.Id } }); + return retUser; }; /** @@ -304,7 +285,6 @@ const updateKeycloakUserRoles = async (username: string, roleNames: string[]) => const userServices = { getUser, - activateUser, addKeycloakUserOnHold, hasAgencies, getAgencies, diff --git a/express-api/src/swagger/common.swagger.yaml b/express-api/src/swagger/common.swagger.yaml new file mode 100644 index 000000000..209698417 --- /dev/null +++ b/express-api/src/swagger/common.swagger.yaml @@ -0,0 +1,44 @@ +# Components common to all routes. Can be referenced in other swagger.yaml files. +components: + securitySchemes: + bearerAuth: # arbitrary name for the security scheme + type: http + scheme: bearer + bearerFormat: JWT + schemas: + CommonEntityProperties: + type: object + properties: + CreatedOn: + type: date + example: '2023-01-18T01:58:40.246Z' + UpdatedOn: + type: date + example: '2023-01-18T01:58:40.246Z' + DeletedOn: + type: date + example: null + CreatedBy: + type: string + example: '00000000-0000-0000-0000-000000000000' + UpdatedBy: + type: string + example: '00000000-0000-0000-0000-000000000000' + DeletedBy: + type: string + example: null + responses: + 400BadRequest: + description: Bad Request + content: + text/plain: + schema: + type: string + example: 'Response could not be processed.' + 429TooManyRequests: + description: Too Many Requests + content: + text/plain: + schema: + type: string + example: 'Too many requests, please try again later.' diff --git a/express-api/src/swagger/swagger-output.json b/express-api/src/swagger/swagger-output.json deleted file mode 100644 index dd83db544..000000000 --- a/express-api/src/swagger/swagger-output.json +++ /dev/null @@ -1,1731 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "version": "3.0.0", - "title": "PIMS Express API", - "description": "A REST API that supports the Property Inventory Management System (PIMS).", - "contact": { - "name": "Support", - "email": "support@pims.gov.bc.ca" - }, - "license": { - "name": "APACHE", - "url": "https://github.com/bcgov/PIMS/blob/dev/LICENSE" - } - }, - "servers": [ - { - "url": "/api/v2", - "description": "" - } - ], - "paths": { - "/accessRequests": { - "get": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - }, - "delete": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/administrativeAreas": { - "get": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - }, - "post": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/administrativeAreas/filter": { - "post": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/administrativeAreas/{id}": { - "get": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - }, - "put": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - }, - "delete": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/claims": { - "get": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - }, - "post": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/claims/{id}": { - "get": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - }, - "put": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - }, - "delete": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/roles": { - "get": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - }, - "post": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/roles/{id}": { - "get": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - }, - "put": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - }, - "delete": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/users": { - "get": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - }, - "post": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/users/my/agency": { - "post": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/users/roles": { - "get": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/users/roles/{username}": { - "get": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - }, - "put": { - "description": "", - "parameters": [ - { - "name": "username", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - } - } - } - }, - "/users/{id}": { - "get": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - }, - "put": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - }, - "delete": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/": { - "get": { - "tags": [ - "Health" - ], - "description": "Returns a 200 (OK) status if API is reached.", - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - } - } - }, - "post": { - "tags": [ - "Agencies - Admin" - ], - "description": "Adds a new agency to the datasource.", - "responses": { - "201": { - "description": "Created" - }, - "400": { - "description": "Bad Request" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/{id}": { - "get": { - "tags": [ - "Agencies - Admin" - ], - "description": "Returns an agency that matches the supplied ID.", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - }, - "404": { - "description": "Not Found" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - }, - "delete": { - "tags": [ - "Agencies - Admin" - ], - "description": "Deletes an agency that matches the supplied ID.", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - }, - "patch": { - "tags": [ - "Agencies - Admin" - ], - "description": "Updates an agency that matches the supplied ID.", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - } - }, - "security": [ - { - "bearerAuth": [] - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "Id": { - "example": "any" - } - } - } - } - } - } - } - }, - "${BUILDINGS_ROUTE}/{buildingId}": { - "get": { - "description": "", - "parameters": [ - { - "name": "BUILDINGS_ROUTE", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "buildingId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - }, - "put": { - "description": "", - "parameters": [ - { - "name": "BUILDINGS_ROUTE", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "buildingId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - }, - "delete": { - "description": "", - "parameters": [ - { - "name": "BUILDINGS_ROUTE", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "buildingId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "${BUILDINGS_ROUTE}/filter": { - "post": { - "description": "", - "parameters": [ - { - "name": "BUILDINGS_ROUTE", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "${BUILDINGS_ROUTE}/check/pid-available": { - "get": { - "description": "", - "parameters": [ - { - "name": "BUILDINGS_ROUTE", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "${BUILDINGS_ROUTE}/check/pin-available": { - "get": { - "description": "", - "parameters": [ - { - "name": "BUILDINGS_ROUTE", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "${BUILDINGS_ROUTE}/{buildingId}/financials": { - "put": { - "description": "", - "parameters": [ - { - "name": "BUILDINGS_ROUTE", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "buildingId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "/agencies": { - "get": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/property/classifications": { - "get": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/project/tier/levels": { - "get": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/project/risks": { - "get": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/all": { - "get": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/land/title": { - "get": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "${DISPOSAL_API_ROUTE}/{id}": { - "get": { - "description": "", - "parameters": [ - { - "name": "DISPOSAL_API_ROUTE", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - }, - "delete": { - "description": "", - "parameters": [ - { - "name": "DISPOSAL_API_ROUTE", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "${NOTIFICATION_QUEUE_ROUTE}/filter": { - "post": { - "description": "", - "parameters": [ - { - "name": "NOTIFICATION_QUEUE_ROUTE", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "${NOTIFICATION_QUEUE_ROUTE}/{id}": { - "get": { - "description": "", - "parameters": [ - { - "name": "NOTIFICATION_QUEUE_ROUTE", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - }, - "put": { - "description": "", - "parameters": [ - { - "name": "NOTIFICATION_QUEUE_ROUTE", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - }, - "delete": { - "description": "", - "parameters": [ - { - "name": "NOTIFICATION_QUEUE_ROUTE", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "${NOTIFICATION_QUEUE_ROUTE}/{id}/resend": { - "put": { - "description": "", - "parameters": [ - { - "name": "NOTIFICATION_QUEUE_ROUTE", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "${NOTIFICATION_TEMPLATE_ROUTE}/{id}": { - "get": { - "description": "", - "parameters": [ - { - "name": "NOTIFICATION_TEMPLATE_ROUTE", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - }, - "put": { - "description": "", - "parameters": [ - { - "name": "NOTIFICATION_TEMPLATE_ROUTE", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - }, - "delete": { - "description": "", - "parameters": [ - { - "name": "NOTIFICATION_TEMPLATE_ROUTE", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "${NOTIFICATION_TEMPLATE_ROUTE}/{templateId}/projects/{projectId}": { - "post": { - "description": "", - "parameters": [ - { - "name": "NOTIFICATION_TEMPLATE_ROUTE", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "templateId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "projectId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "${PARCELS_ROUTE}/{parcelId}": { - "get": { - "description": "", - "parameters": [ - { - "name": "PARCELS_ROUTE", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "parcelId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - }, - "put": { - "description": "", - "parameters": [ - { - "name": "PARCELS_ROUTE", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "parcelId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - }, - "delete": { - "description": "", - "parameters": [ - { - "name": "PARCELS_ROUTE", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "parcelId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "${PARCELS_URL}/": { - "get": { - "description": "", - "parameters": [ - { - "name": "PARCELS_URL", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - }, - "post": { - "description": "", - "parameters": [ - { - "name": "PARCELS_URL", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "${PARCELS_ROUTE}/filter": { - "post": { - "description": "", - "parameters": [ - { - "name": "PARCELS_ROUTE", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "${PARCELS_ROUTE}/check/pid-available": { - "get": { - "description": "", - "parameters": [ - { - "name": "PARCELS_ROUTE", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "${PARCELS_ROUTE}/check/pin-available": { - "get": { - "description": "", - "parameters": [ - { - "name": "PARCELS_ROUTE", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "${PARCELS_ROUTE}/{parcelId}/financials": { - "put": { - "description": "", - "parameters": [ - { - "name": "PARCELS_ROUTE", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "parcelId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "${PROJECT_DISPOSAL}/{projectId}": { - "get": { - "description": "", - "parameters": [ - { - "name": "PROJECT_DISPOSAL", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "projectId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - }, - "put": { - "description": "", - "parameters": [ - { - "name": "PROJECT_DISPOSAL", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "projectId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - }, - "delete": { - "description": "", - "parameters": [ - { - "name": "PROJECT_DISPOSAL", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "projectId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "${PROJECT_DISPOSAL}/workflows": { - "put": { - "description": "", - "parameters": [ - { - "name": "PROJECT_DISPOSAL", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "${PROJECT_REPORTS}/{reportId}": { - "get": { - "description": "", - "parameters": [ - { - "name": "PROJECT_REPORTS", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "reportId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - }, - "put": { - "description": "", - "parameters": [ - { - "name": "PROJECT_REPORTS", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "reportId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - }, - "delete": { - "description": "", - "parameters": [ - { - "name": "PROJECT_REPORTS", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "reportId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "${PROJECT_REPORTS}/snapshots/{reportId}": { - "get": { - "description": "", - "parameters": [ - { - "name": "PROJECT_REPORTS", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "reportId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - }, - "post": { - "description": "", - "parameters": [ - { - "name": "PROJECT_REPORTS", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "reportId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "${PROJECT_REPORTS}/refresh/{reportId}": { - "get": { - "description": "", - "parameters": [ - { - "name": "PROJECT_REPORTS", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "reportId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "/projects/status": { - "get": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/projects/status/{statusCode}/tasks": { - "get": { - "description": "", - "parameters": [ - { - "name": "statusCode", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "/projects/workflows/{workflowCode}/status": { - "get": { - "description": "", - "parameters": [ - { - "name": "workflowCode", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "/projects/workflows/{workflowCode}/tasks": { - "get": { - "description": "", - "parameters": [ - { - "name": "workflowCode", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "/search": { - "get": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/search/filter": { - "post": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/search/geo/filter": { - "post": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/search/page": { - "get": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/search/page/filter": { - "post": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/projects": { - "get": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/properties": { - "get": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/ches/status/": { - "get": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/ches/status/{messageId}": { - "get": { - "description": "", - "parameters": [ - { - "name": "messageId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "/ches/cancel/{messageId}": { - "delete": { - "description": "", - "parameters": [ - { - "name": "messageId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "/geocoder/addresses": { - "get": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/geocoder/parcels/pids/{siteId}": { - "get": { - "description": "", - "parameters": [ - { - "name": "siteId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "/import/properties": { - "post": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - }, - "delete": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/import/properties/financials": { - "patch": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/import/projects": { - "post": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/info": { - "get": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/self": { - "get": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/access/requests": { - "post": { - "description": "", - "responses": { - "default": { - "description": "" - } - } - } - }, - "/agencies/{username}": { - "get": { - "description": "", - "parameters": [ - { - "name": "username", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - } - }, - "components": { - "securitySchemes": { - "bearerAuth": { - "type": "http", - "scheme": "bearer", - "bearerFormat": "JWT" - } - } - } -} \ No newline at end of file diff --git a/express-api/src/swagger/swagger.mjs b/express-api/src/swagger/swagger.mjs deleted file mode 100644 index 0f26a9430..000000000 --- a/express-api/src/swagger/swagger.mjs +++ /dev/null @@ -1,50 +0,0 @@ -import swaggerAutogen from 'swagger-autogen'; - -const doc = { - info: { - version: '3.0.0', // by default: '1.0.0' - title: 'PIMS Express API', // by default: 'REST API' - description: 'A REST API that supports the Property Inventory Management System (PIMS).', // by default: '' - contact: { - name: 'Support', - email: 'support@pims.gov.bc.ca', - }, - license: { - name: 'APACHE', - url: 'https://github.com/bcgov/PIMS/blob/dev/LICENSE', - }, - }, - servers: [ - { - url: '/api/v2', // by default: 'http://localhost:3000' - description: '', // by default: '' - }, - // { ... } - ], - tags: [ - // by default: empty Array - // { - // name: '', // Tag name - // description: '', // Tag description - // }, - // { ... } - ], - components: { - securitySchemes: { - bearerAuth: { - // arbitrary name for the security scheme - type: 'http', - scheme: 'bearer', - bearerFormat: 'JWT', - }, - }, - }, // by default: empty object -}; - -const outputFile = './swagger-output.json'; -const routes = ['../routes/**/*.ts']; - -/* NOTE: If you are using the express Router, you must pass in the 'routes' only the -root file where the route starts, such as index.js, app.js, routes.js, etc ... */ - -swaggerAutogen({ openapi: '3.0.0' })(outputFile, routes, doc); diff --git a/express-api/src/swagger/swaggerConfig.ts b/express-api/src/swagger/swaggerConfig.ts new file mode 100644 index 000000000..19af466e9 --- /dev/null +++ b/express-api/src/swagger/swaggerConfig.ts @@ -0,0 +1,22 @@ +const OPENAPI_OPTIONS = { + definition: { + openapi: '3.0.0', + info: { + version: '3.0.0', + title: 'PIMS Express API', // by default: 'REST API' + description: 'A REST API that supports the Property Inventory Management System (PIMS).', // by default: '' + contact: { + name: 'Support', + email: 'support@pims.gov.bc.ca', // TODO: Is this email real? + }, + license: { + name: 'APACHE', + url: 'https://github.com/bcgov/PIMS/blob/dev/LICENSE', + }, + }, + servers: [{ url: `${process.env.BACKEND_URL}/v2` }], + }, + apis: ['./**/*.swagger.yaml'], +}; + +export default OPENAPI_OPTIONS; diff --git a/express-api/src/utilities/customErrors/ErrorWithCode.ts b/express-api/src/utilities/customErrors/ErrorWithCode.ts index 707944385..dc0ae1894 100644 --- a/express-api/src/utilities/customErrors/ErrorWithCode.ts +++ b/express-api/src/utilities/customErrors/ErrorWithCode.ts @@ -5,11 +5,11 @@ * @extends Error * * @param {string} message - The error message. - * @param {number} code - The error code. Defaults to 400. + * @param {number} code - The error code. Defaults to 500. * * @example * const err = new ErrorWithCode('test'); - * console.log(err.code); // 400 + * console.log(err.code); // 500 * * @example * const err = new ErrorWithCode('test', 401); @@ -19,7 +19,7 @@ export class ErrorWithCode extends Error { public code: number; - constructor(message: string, code: number = 400) { + constructor(message: string, code: number = 500) { super(message); this.code = code; } diff --git a/express-api/tests/integration/notifications/notifications.test.ts b/express-api/tests/integration/notifications/notifications.test.ts index eb7fc0349..d5e784079 100644 --- a/express-api/tests/integration/notifications/notifications.test.ts +++ b/express-api/tests/integration/notifications/notifications.test.ts @@ -1,12 +1,10 @@ import supertest from 'supertest'; import app from '@/express'; import { - DISPOSAL_API_ROUTE, NOTIFICATION_QUEUE_ROUTE, NOTIFICATION_TEMPLATE_ROUTE, } from '@/routes/notificationsRouter'; import { - DisposalNotificationFilter, Notification, NotificationQueueFilter, NotificationTemplate, @@ -79,68 +77,6 @@ const makeTemplate = (): NotificationTemplate => { }; describe('INTEGRATION - Notifications', () => { - describe('GET /projects/disposal/:id/notifications', () => { - xit('should return status 200 with notifications', async () => { - const response = await request.get(`/notifications/${DISPOSAL_API_ROUTE}/1/notifications`); - expect(response.status).toBe(200); - expect(response.body).toBeDefined(); - }); - - xit('should return status 404 for non-existent project ID', async () => { - const response = await request.get(`/notifications/${DISPOSAL_API_ROUTE}/999/notifications`); - expect(response.status).toBe(404); - }); - - xit('should return status 400 for invalid project ID', async () => { - const response = await request.get( - `/notifications/${DISPOSAL_API_ROUTE}/invalid_id/notifications`, - ); - expect(response.status).toBe(400); - }); - }); - - describe('POST /projects/disposal/notifications', () => { - xit('should return status 200 for successful request', async () => { - const filter: DisposalNotificationFilter = { - page: 0, - projectId: 0, - }; - const response = await request - .post(`/notifications/${DISPOSAL_API_ROUTE}/notifications`) - .send(filter); - expect(response.status).toBe(200); - }); - - xit('should return status 400 for malformed request', async () => { - const filter = { bad: 0 }; - const response = await request - .post(`/notifications/${DISPOSAL_API_ROUTE}/notifications`) - .send(filter); - expect(response.status).toBe(400); - }); - }); - - describe('PUT /projects/disposal/:id/notifications', () => { - xit('should return status 200 for successful request', async () => { - const response = await request.delete(`/notifications/${DISPOSAL_API_ROUTE}/1/notifications`); - expect(response.status).toBe(200); - }); - - xit('should return status 404 for non-existent project ID', async () => { - const response = await request.delete( - `/notifications/${DISPOSAL_API_ROUTE}/999/notifications`, - ); - expect(response.status).toBe(404); - }); - - xit('should return status 400 for invalid project ID', async () => { - const response = await request.delete( - `/notifications/${DISPOSAL_API_ROUTE}/invalid_id/notifications`, - ); - expect(response.status).toBe(400); - }); - }); - describe('GET /notifications/queue', () => { xit('should return status 200 for successful request', async () => { const response = await request.get(NOTIFICATION_QUEUE_ROUTE); diff --git a/express-api/tests/integration/projects/projects.test.ts b/express-api/tests/integration/projects/projects.test.ts index d84cfddfb..cbe4ce876 100644 --- a/express-api/tests/integration/projects/projects.test.ts +++ b/express-api/tests/integration/projects/projects.test.ts @@ -1,7 +1,7 @@ import supertest from 'supertest'; import app from '@/express'; -import { PROJECT_DISPOSAL, PROJECT_REPORTS } from '@/routes/projectsRouter'; -import { Project, ProjectReport, ProjectSnapshot } from '@/controllers/projects/projectsSchema'; +import { PROJECT_DISPOSAL } from '@/routes/projectsRouter'; +import { Project } from '@/controllers/projects/projectsSchema'; import { faker } from '@faker-js/faker'; const request = supertest(app); @@ -111,48 +111,6 @@ describe('INTEGRATION - Project Routes', () => { }; }; - const makeReportSnapshot = (): ProjectSnapshot => { - return { - createdOn: faker.date.past().toISOString(), - updatedOn: faker.date.past().toISOString(), - updatedByName: faker.person.firstName(), - updatedByEmail: faker.internet.email(), - rowVersion: faker.number.binary(), - id: faker.number.int(), - projectId: faker.number.int(), - project: makeProject(), - snapshotOn: faker.date.past().toISOString(), - netBook: faker.number.int(), - market: faker.number.int(), - assessed: faker.number.int(), - appraised: faker.number.int(), - salesCost: faker.number.int(), - netProceeds: faker.number.int(), - baselineIntegrity: faker.number.int(), - programCost: faker.number.int(), - gainLoss: faker.number.int(), - ocgFinancialStatement: faker.number.int(), - interestComponent: faker.number.int(), - saleWithLeaseInPlace: faker.datatype.boolean(), - }; - }; - - const makeProjectReport = (): ProjectReport => { - return { - createdOn: faker.date.past().toISOString(), - updatedOn: faker.date.past().toISOString(), - updatedByName: faker.person.firstName(), - updatedByEmail: faker.internet.email(), - rowVersion: faker.number.binary(), - id: faker.number.int(), - isFinal: faker.datatype.boolean(), - name: faker.person.firstName(), - from: faker.date.past().toISOString(), - to: faker.date.future().toISOString(), - reportType: '?', - snapshots: [makeReportSnapshot()], - }; - }; // PROJECT_DISPOSAL Routes describe(`GET ${PROJECT_DISPOSAL}/:projectId`, () => { xit('should return status 200 with the project', async () => { @@ -208,80 +166,6 @@ describe('INTEGRATION - Project Routes', () => { }); }); - // PROJECT_REPORTS Routes - describe(`GET ${PROJECT_REPORTS}`, () => { - xit('should return status 200 with all project reports', async () => { - const response = await request.get(`${API_ROUTE}${PROJECT_REPORTS}`); - expect(response.status).toBe(200); - }); - }); - - describe(`GET ${PROJECT_REPORTS}/:reportId`, () => { - xit('should return status 200 with the specified project report', async () => { - const response = await request.get(`${API_ROUTE}${PROJECT_REPORTS}/1`); - expect(response.status).toBe(200); - }); - - xit('should return status 404 when the specified project report does not exist', async () => { - const response = await request.get(`${API_ROUTE}${PROJECT_REPORTS}/nonexistentId`); - expect(response.status).toBe(404); - }); - }); - - describe(`PUT ${PROJECT_REPORTS}/:reportId`, () => { - xit('should return status 200 when updating the project report', async () => { - const reportBody = makeProjectReport(); - const response = await request.put(`${API_ROUTE}${PROJECT_REPORTS}/1`).send(reportBody); - expect(response.status).toBe(200); - }); - - xit('should return status 404 when updating a nonexistent project report', async () => { - const response = await request.put(`${API_ROUTE}${PROJECT_REPORTS}/nonexistentId`); - expect(response.status).toBe(404); - }); - }); - - describe(`DELETE ${PROJECT_REPORTS}/:reportId`, () => { - xit('should return status 200 when deleting the project report', async () => { - const response = await request.delete(`${API_ROUTE}${PROJECT_REPORTS}/1`); - expect(response.status).toBe(200); - }); - - xit('should return status 404 when deleting a nonexistent project report', async () => { - const response = await request.delete(`${API_ROUTE}${PROJECT_REPORTS}/nonexistentId`); - expect(response.status).toBe(404); - }); - }); - - describe(`POST ${PROJECT_REPORTS}/:reportId`, () => { - xit('should return status 200 when adding a new project report', async () => { - const reportBody = makeProjectReport(); - const response = await request.post(`${API_ROUTE}${PROJECT_REPORTS}/1`).send(reportBody); - expect(response.status).toBe(200); - }); - }); - - describe(`GET ${PROJECT_REPORTS}/snapshots/:reportId`, () => { - xit('should return status 200 with project report snapshots', async () => { - const response = await request.get(`${API_ROUTE}${PROJECT_REPORTS}/snapshots/1`); - expect(response.status).toBe(200); - }); - }); - - describe(`POST ${PROJECT_REPORTS}/snapshots/:reportId`, () => { - xit('should return status 200 when generating project report snapshots', async () => { - const response = await request.post(`${API_ROUTE}${PROJECT_REPORTS}/snapshots/1`); - expect(response.status).toBe(200); - }); - }); - - describe(`GET ${PROJECT_REPORTS}/refresh/:reportId`, () => { - xit('should return status 200 when refreshing project snapshots', async () => { - const response = await request.get(`${API_ROUTE}${PROJECT_REPORTS}/refresh/1`); - expect(response.status).toBe(200); - }); - }); - describe(`GET /projects/status`, () => { xit('should return status 200 with all project statuses', async () => { const response = await request.get(`${API_ROUTE}/projects/status`); diff --git a/express-api/tests/integration/roles/roles.test.ts b/express-api/tests/integration/roles/roles.test.ts deleted file mode 100644 index c64acf9c8..000000000 --- a/express-api/tests/integration/roles/roles.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import supertest from 'supertest'; -import { faker } from '@faker-js/faker'; -import app from '@/express'; -import { UUID } from 'crypto'; - -const request = supertest(app); - -const ROLES_PATH = '/v2/roles'; -const mockRole = { - createdOn: faker.date.anytime().toLocaleString(), - updatedOn: faker.date.anytime().toLocaleString(), - updatedById: faker.string.uuid() as UUID, - createdById: faker.string.uuid() as UUID, - id: faker.string.uuid() as UUID, - name: faker.location.city(), - isDisabled: false, - isVisible: true, - description: '', - sortOrder: 0, - type: '', -}; -describe('INTEGRATION - Roles', () => { - // TODO: figure out how to mock keycloak - const TOKEN = ''; - describe(`GET ${ROLES_PATH}`, () => { - it('should return 401 Unauthorized if invalid token provided', async () => { - const response = await request.get(`${ROLES_PATH}`).set('Authorization', `Bearer notAToken`); - expect(response.status).toBe(401); - }); - - xit('should return status 200 with a list of roles', async () => { - const response = await request.get(`${ROLES_PATH}`).set('Authorization', `Bearer ${TOKEN}`); - expect(response.status).toBe(200); - }); - }); - - describe(`POST ${ROLES_PATH}`, () => { - it('should return 401 Unauthorized if invalid token provided', async () => { - const response = await request.post(`${ROLES_PATH}`).set('Authorization', `Bearer notAToken`); - expect(response.status).toBe(401); - }); - - xit('should return 201 Created and a body of what was created', async () => { - const response = await request - .post(`${ROLES_PATH}`) - .set('Authorization', `Bearer ${TOKEN}`) - .send(mockRole); - expect(response.status).toBe(201); - expect(response.body.id).toBe(mockRole.id); - }); - }); - - describe(`GET ${ROLES_PATH}/name/:name`, () => { - it('should return 401 Unauthorized if invalid token provided', async () => { - const response = await request - .get(`${ROLES_PATH}/name/${mockRole.name}`) - .set('Authorization', `Bearer notAToken`); - expect(response.status).toBe(401); - }); - - xit('should return 200 and the matching role data', async () => { - const response = await request - .get(`${ROLES_PATH}/name/${mockRole.name}`) - .set('Authorization', `Bearer ${TOKEN}`); - expect(response.status).toBe(200); - expect(response.body.name).toBe(mockRole.name); - }); - }); - - describe(`GET ${ROLES_PATH}/:id`, () => { - it('should return 401 Unauthorized if invalid token provided', async () => { - const response = await request - .get(`${ROLES_PATH}/${mockRole.id}`) - .set('Authorization', `Bearer notAToken`); - expect(response.status).toBe(401); - }); - - xit('should return 200 and the matching role data', async () => { - const response = await request - .get(`${ROLES_PATH}/${mockRole.id}`) - .set('Authorization', `Bearer ${TOKEN}`); - expect(response.status).toBe(200); - expect(response.body.id).toBe(mockRole.id); - }); - }); - - describe(`PUT ${ROLES_PATH}/:id`, () => { - it('should return 401 Unauthorized if invalid token provided', async () => { - const response = await request - .put(`${ROLES_PATH}/${mockRole.id}`) - .set('Authorization', `Bearer notAToken`); - expect(response.status).toBe(401); - }); - - xit('should return 200 and the updated role data', async () => { - const response = await request - .put(`${ROLES_PATH}/${mockRole.id}`) - .set('Authorization', `Bearer ${TOKEN}`) - .send({ ...mockRole, name: 'new name' }); - expect(response.status).toBe(200); - expect(response.body.name).toBe('new name'); - }); - }); - - describe(`DELETE ${ROLES_PATH}/:id`, () => { - it('should return 401 Unauthorized if invalid token provided', async () => { - const response = await request - .delete(`${ROLES_PATH}/${mockRole.id}`) - .set('Authorization', `Bearer notAToken`); - expect(response.status).toBe(401); - }); - - xit('should return 204 and original should be gone', async () => { - const response = await request - .delete(`${ROLES_PATH}/${mockRole.id}`) - .set('Authorization', `Bearer ${TOKEN}`); - expect(response.status).toBe(204); - // Then it should not be found in database after - const notFoundResponse = await request - .delete(`${ROLES_PATH}/${mockRole.id}`) - .set('Authorization', `Bearer ${TOKEN}`); - expect(notFoundResponse.status).toBe(404); - }); - }); -}); diff --git a/express-api/tests/unit/controllers/notifications/notificationsController.test.ts b/express-api/tests/unit/controllers/notifications/notificationsController.test.ts index 95a10deb0..2d6ded36c 100644 --- a/express-api/tests/unit/controllers/notifications/notificationsController.test.ts +++ b/express-api/tests/unit/controllers/notifications/notificationsController.test.ts @@ -125,21 +125,21 @@ describe('UNIT - Testing controllers for notifications routes.', () => { describe('resendNotifcationById', () => { it('should read a notification and try to send it through ches again, status 200', async () => { mockRequest.params.id = '1'; - mockRequest.user = produceSSO({ client_roles: [Roles.ADMIN] }); + mockRequest.setUser({ client_roles: [Roles.ADMIN] }); await controllers.resendNotificationById(mockRequest, mockResponse); expect(mockResponse.statusValue).toBe(200); expect(mockResponse.sendValue.Id).toBe(1); }); it('should 404 if notif not found', async () => { mockRequest.params.id = '1'; - mockRequest.user = produceSSO({ client_roles: [Roles.ADMIN] }); + mockRequest.setUser({ client_roles: [Roles.ADMIN] }); _getNotifById.mockImplementationOnce(() => null); await controllers.resendNotificationById(mockRequest, mockResponse); expect(mockResponse.statusValue).toBe(404); }); it('should 403 if user lacks permissions', async () => { mockRequest.params.id = '1'; - mockRequest.user = produceSSO({ client_roles: [] }); + mockRequest.setUser({ client_roles: [], hasRoles: () => false }); await controllers.resendNotificationById(mockRequest, mockResponse); expect(mockResponse.statusValue).toBe(403); }); @@ -147,7 +147,7 @@ describe('UNIT - Testing controllers for notifications routes.', () => { describe('cancelNotificationById', () => { it('should try to cancel a notification, status 200', async () => { mockRequest.params.id = '1'; - mockRequest.user = produceSSO({ client_roles: [Roles.ADMIN] }); + mockRequest.setUser({ client_roles: [Roles.ADMIN] }); await controllers.cancelNotificationById(mockRequest, mockResponse); expect(mockResponse.statusValue).toBe(200); expect(mockResponse.sendValue.Id).toBe(1); @@ -155,14 +155,14 @@ describe('UNIT - Testing controllers for notifications routes.', () => { }); it('should 404 if no notif found', async () => { mockRequest.params.id = '1'; - mockRequest.user = produceSSO({ client_roles: [Roles.ADMIN] }); + mockRequest.setUser({ client_roles: [Roles.ADMIN] }); _getNotifById.mockImplementationOnce(() => null); await controllers.cancelNotificationById(mockRequest, mockResponse); expect(mockResponse.statusValue).toBe(404); }); it('should 400 if the notification came back with non-cancelled status', async () => { mockRequest.params.id = '1'; - mockRequest.user = produceSSO({ client_roles: [Roles.ADMIN] }); + mockRequest.setUser({ client_roles: [Roles.ADMIN] }); _cancelNotifById.mockImplementationOnce((id: number) => produceNotificationQueue({ Id: id, Status: NotificationStatus.Completed }), ); @@ -173,7 +173,7 @@ describe('UNIT - Testing controllers for notifications routes.', () => { }); it('should 403 if user lacks permissions', async () => { mockRequest.params.id = '1'; - mockRequest.user = produceSSO({ client_roles: [] }); + mockRequest.setUser({ client_roles: [], hasRoles: () => false }); await controllers.cancelNotificationById(mockRequest, mockResponse); expect(mockResponse.statusValue).toBe(403); }); diff --git a/express-api/tests/unit/controllers/tools/toolsController.test.ts b/express-api/tests/unit/controllers/tools/toolsController.test.ts index 0715f7427..c965b8f3e 100644 --- a/express-api/tests/unit/controllers/tools/toolsController.test.ts +++ b/express-api/tests/unit/controllers/tools/toolsController.test.ts @@ -4,9 +4,7 @@ import { MockReq, MockRes, getRequestHandlerMocks, - produceEmail, produceEmailStatus, - produceSSO, produceGeocoderAddress, producePidsResponse, } from '../../../testUtils/factories'; @@ -146,23 +144,6 @@ describe('UNIT - Tools', () => { }); }); - describe('POST /tools/ches', () => { - it('should return status 201', async () => { - mockRequest.body = produceEmail({}); - mockRequest.user = produceSSO(); - await controllers.sendChesMessage(mockRequest, mockResponse); - expect(mockResponse.statusValue).toBe(201); - }); - it('should throw an error when sendEmailAsync throws an error', async () => { - _sendEmailAsync.mockImplementationOnce(() => { - throw new Error(); - }); - expect( - async () => await controllers.sendChesMessage(mockRequest, mockResponse), - ).rejects.toThrow(); - }); - }); - describe('GET /tools/geocoder/addresses', () => { it('should return 200 on just an address', async () => { mockRequest.query.address = '1999 Tester St'; diff --git a/express-api/tests/unit/controllers/users/usersController.test.ts b/express-api/tests/unit/controllers/users/usersController.test.ts index ea901b3ec..438afc943 100644 --- a/express-api/tests/unit/controllers/users/usersController.test.ts +++ b/express-api/tests/unit/controllers/users/usersController.test.ts @@ -14,20 +14,8 @@ import { ErrorWithCode } from '@/utilities/customErrors/ErrorWithCode'; import { Roles } from '@/constants/roles'; import { UUID } from 'crypto'; -const { - addUser, - getUserById, - getUserRolesByName, - getAllRoles, - getUsers, - deleteUserById, - updateUserById, - updateUserRolesByName, - getUserInfo, - submitUserAccessRequest, - getUserAgencies, - getSelf, -} = controllers; +const { getUserById, getUsers, updateUserById, submitUserAccessRequest, getUserAgencies, getSelf } = + controllers; const _activateUser = jest.fn(); const _addKeycloakUserOnHold = jest @@ -97,34 +85,6 @@ describe('UNIT - Testing controllers for users routes.', () => { mockResponse = mockRes; }); - describe('getUserInfo', () => { - it('should return status 200 and keycloak info', async () => { - const header = { a: faker.string.alphanumeric() }; - const payload = { b: faker.string.alphanumeric() }; - mockRequest.token = btoa(JSON.stringify(header)) + '.' + btoa(JSON.stringify(payload)); - await getUserInfo(mockRequest, mockResponse); - expect(mockResponse.statusValue).toBe(200); - expect(mockResponse.sendValue.b).toBe(payload.b); - }); - - it('should return 400 when an invalid JWT is sent', async () => { - mockRequest.token = 'hello'; - await getUserInfo(mockRequest, mockResponse); - expect(mockResponse.statusValue).toBe(400); - }); - - it('should return 400 when no JWT is sent', async () => { - await getUserInfo(mockRequest, mockResponse); - expect(mockResponse.statusValue).toBe(400); - }); - - // FIXME: I don't think we should be throwing errors from controllers - // it('should throw an error when either side of the jwt cannot be parsed', () => { - // mockRequest.token = 'hello.goodbye'; - // expect(() => {getUserInfo(mockRequest, mockResponse)}).toThrow(); - // }) - }); - describe('submitUserAccessRequest', () => { it('should return status 201 and an access request', async () => { mockRequest.user = produceSSO(); @@ -259,28 +219,6 @@ describe('UNIT - Users Admin', () => { mockResponse = mockRes; }); - describe('Controller addUser', () => { - const user = produceUser(); - beforeEach(() => { - _addUser.mockImplementation(() => user); - mockRequest.body = user; - }); - - it('should return status 201 and the new user', async () => { - await addUser(mockRequest, mockResponse); - expect(mockResponse.statusValue).toBe(201); - expect(mockResponse.sendValue.Id).toBe(mockRequest.body.Id); - }); - - it('should return status 400 when the userService.addUser throws an error', async () => { - _addUser.mockImplementationOnce(() => { - throw new Error(); - }); - await addUser(mockRequest, mockResponse); - expect(mockResponse.statusValue).toBe(400); - }); - }); - describe('Controller updateUserById', () => { const user = produceUser(); user.Email = 'newEmail@gov.bc.ca'; @@ -310,101 +248,4 @@ describe('UNIT - Users Admin', () => { expect(async () => await updateUserById(mockRequest, mockResponse)).rejects.toThrow(); }); }); - - describe('Controller deleteUserById', () => { - const user = produceUser(); - beforeEach(() => { - _deleteUser.mockImplementation(() => user); - mockRequest.params.id = user.Id; - mockRequest.body = user; - }); - - it('should return status 200', async () => { - await deleteUserById(mockRequest, mockResponse); - expect(mockResponse.statusValue).toBe(200); - expect(mockResponse.sendValue.Id).toBe(user.Id); - }); - - it('should return status 400 if the param ID does not match the body ID', async () => { - mockRequest.params.id = faker.string.uuid() as UUID; - await deleteUserById(mockRequest, mockResponse); - expect(mockResponse.statusValue).toBe(400); - expect(mockResponse.sendValue).toBe('The param ID does not match the request body.'); - }); - - it('should throw an error if userService.deleteUser throws an error', async () => { - _deleteUser.mockImplementationOnce(() => { - throw new Error(); - }); - expect(async () => await deleteUserById(mockRequest, mockResponse)).rejects.toThrow(); - }); - }); - - describe('Controller getUserRolesByName', () => { - beforeEach(() => { - mockRequest.params.username = 'test'; - }); - - it('should return status 200 and a list of their roles', async () => { - await getUserRolesByName(mockRequest, mockResponse); - expect(mockResponse.statusValue).toBe(200); - // Only role is Admin - expect(mockResponse.sendValue).toHaveLength(1); - expect(mockResponse.sendValue.at(0)).toBe('admin'); - }); - - it('should return status 400 if params.username is not provided', async () => { - mockRequest.params = {}; - await getUserRolesByName(mockRequest, mockResponse); - expect(mockResponse.statusValue).toBe(400); - expect(mockResponse.sendValue).toBe('Username was empty.'); - }); - }); - - describe('Controller getAllRoles', () => { - it('should return status 200 and a list of roles assigned to a user', async () => { - await getAllRoles(mockRequest, mockResponse); - expect(mockResponse.statusValue).toBe(200); - expect(mockResponse.sendValue.at(0)).toBe('admin'); - }); - - it('should throw an error when internal service throws', async () => { - _getRoles.mockImplementationOnce(() => { - throw new Error(); - }); - expect(async () => await getAllRoles(mockRequest, mockResponse)).rejects.toThrow(); - }); - }); - - describe('Controller updateUserRolesByName', () => { - const user = produceUser(); - beforeEach(() => { - _updateUserRoles.mockImplementation((username, roles) => roles); - mockRequest.params = { - username: user.Username, - }; - mockRequest.body = ['admin', 'auditor']; - }); - it('should return status 200 and a list of updated roles', async () => { - await updateUserRolesByName(mockRequest, mockResponse); - expect(mockResponse.statusValue).toBe(200); - // TODO: Check the return value. It's currently undefined for some reason. - }); - - it('should return status 400 if the request body was not parsed successfully', async () => { - mockRequest.body = { - notGood: true, - }; - await updateUserRolesByName(mockRequest, mockResponse); - expect(mockResponse.statusValue).toBe(400); - expect(mockResponse.sendValue).toBe('Request body was wrong format.'); - }); - - it('should return 400 if params.username was not provided', async () => { - mockRequest.params = {}; - await updateUserRolesByName(mockRequest, mockResponse); - expect(mockResponse.statusValue).toBe(400); - expect(mockResponse.sendValue).toBe('Username was empty.'); - }); - }); }); diff --git a/express-api/tests/unit/services/administrativeAreas/adminstrativeAreasService.test.ts b/express-api/tests/unit/services/administrativeAreas/adminstrativeAreasService.test.ts index 9528d8f3d..b51b8662f 100644 --- a/express-api/tests/unit/services/administrativeAreas/adminstrativeAreasService.test.ts +++ b/express-api/tests/unit/services/administrativeAreas/adminstrativeAreasService.test.ts @@ -51,7 +51,6 @@ describe('UNIT - admin area services', () => { page: 1, quickFilter: 'wow', createdOn: faker.date.recent().toUTCString(), - updatedOn: faker.date.recent().toUTCString(), }); expect(Array.isArray(areas.data)).toBe(true); expect(areas.totalCount).toBe(1); diff --git a/express-api/tests/unit/services/notifications/notificationServices.test.ts b/express-api/tests/unit/services/notifications/notificationServices.test.ts index 8de46a33a..e22036641 100644 --- a/express-api/tests/unit/services/notifications/notificationServices.test.ts +++ b/express-api/tests/unit/services/notifications/notificationServices.test.ts @@ -173,6 +173,8 @@ const _getStatusByIdAsync = jest.spyOn(chesServices, 'getStatusByIdAsync').mockR msgId: randomUUID(), }); +const testUser = produceUser(); + describe('UNIT - Notification Services', () => { beforeEach(() => { jest.clearAllMocks(); @@ -492,7 +494,7 @@ describe('cancelNotificationById', () => { createdTS: Date.now(), msgId: randomUUID(), })); - const result = await notificationServices.cancelNotificationById(1); + const result = await notificationServices.cancelNotificationById(1, testUser); expect(result.Status).toBe(NotificationStatus.Cancelled); }); it('should return unmodified notification in the case of a non cancelled notification', async () => { @@ -506,7 +508,7 @@ describe('cancelNotificationById', () => { })); const notif = produceNotificationQueue({ ChesMessageId: randomUUID() }); _notifQueueFindOne.mockImplementationOnce(() => notif); - const result = await notificationServices.cancelNotificationById(1); + const result = await notificationServices.cancelNotificationById(1, testUser); expect(result.ChesMessageId).toBe(notif.ChesMessageId); expect(result.Status).toBe(notif.Status); }); diff --git a/express-api/tests/unit/services/users/usersServices.test.ts b/express-api/tests/unit/services/users/usersServices.test.ts index 96a542ca5..b945a1053 100644 --- a/express-api/tests/unit/services/users/usersServices.test.ts +++ b/express-api/tests/unit/services/users/usersServices.test.ts @@ -104,60 +104,6 @@ describe('UNIT - User services', () => { }); }); - describe('activateUser', () => { - it('updates a user based off the kc username', async () => { - const found = produceUser(); - found.Username = 'test'; - _usersFindOneBy.mockResolvedValueOnce(found); - const user = await userServices.activateUser(ssoUser); - expect(_usersFindOneBy).toHaveBeenCalledTimes(1); - expect(_usersUpdate).toHaveBeenCalledTimes(1); - }); - it('adds a new user based off the kc username', async () => { - _usersFindOneBy.mockResolvedValueOnce(null); - const user = await userServices.activateUser(ssoUser); - expect(_usersFindOneBy).toHaveBeenCalledTimes(1); - expect(_usersInsert).toHaveBeenCalledTimes(1); - }); - }); - - // describe('getAccessRequest', () => { - // it('should get the latest accessRequest', async () => { - // const request = await userServices.getAccessRequest(kcUser); - // expect(AppDataSource.getRepository(AccessRequest).createQueryBuilder).toHaveBeenCalledTimes( - // 1, - // ); - // }); - // }); - - // describe('getAccessRequestById', () => { - // xit('should get the accessRequest at the id specified', async () => { - // const user = produceUser(); - // const req = produceRequest(); - // req.User = user; - // req.UserId = user.Id; - // _usersFindOneBy.mockResolvedValueOnce(user); - // _requestQueryGetOne.mockImplementationOnce(() => req); - // const request = await userServices.getAccessRequestById(req.Id, kcUser); - // }); - // }); - - // describe('deleteAccessRequest', () => { - // it('should return a deleted access request', async () => { - // const req = await userServices.deleteAccessRequest(produceRequest()); - // expect(_requestRemove).toHaveBeenCalledTimes(1); - // }); - - // it('should throw an error if the access request does not exist', () => { - // jest - // .spyOn(AppDataSource.getRepository(AccessRequest), 'findOne') - // .mockImplementationOnce(() => undefined); - // expect( - // async () => await userServices.deleteAccessRequest(produceRequest()), - // ).rejects.toThrow(); - // }); - // }); - describe('addAccessRequest', () => { it('should add and return an access request', async () => { const agencyId = faker.number.int(); @@ -167,21 +113,6 @@ describe('UNIT - User services', () => { }); }); - // describe('updateAccessRequest', () => { - // xit('should update and return the access request', async () => { - // const req = produceRequest(); - // //_usersFindOneBy.mockResolvedValueOnce(req.UserId); - // // eslint-disable-next-line @typescript-eslint/no-explicit-any - // req.RoleId = {} as any; - // const request = await userServices.updateAccessRequest(req, kcUser); - // expect(_requestUpdate).toHaveBeenCalledTimes(1); - // }); - - // it('should throw an error if the provided access request is null', () => { - // expect(async () => await userServices.updateAccessRequest(null, kcUser)).rejects.toThrow(); - // }); - // }); - describe('getAgencies', () => { it('should return an array of agency ids', async () => { const agencies = await userServices.getAgencies('test'); @@ -243,6 +174,11 @@ describe('UNIT - User services', () => { describe('updateUser', () => { it('should update and return the added user', async () => { const user = produceUser(); + // Mocked once for the initial get and again for the return. + jest + .spyOn(AppDataSource.getRepository(User), 'findOne') + .mockImplementationOnce(async () => produceUser({ ...user })) + .mockImplementationOnce(async () => produceUser({ ...user })); const retUser = await userServices.updateUser(user); expect(_usersUpdate).toHaveBeenCalledTimes(1); expect(user.Id).toBe(retUser.Id); diff --git a/express-api/tests/unit/utilities/ErrorWithCode.test.ts b/express-api/tests/unit/utilities/ErrorWithCode.test.ts index 8e39b8c88..a4550511c 100644 --- a/express-api/tests/unit/utilities/ErrorWithCode.test.ts +++ b/express-api/tests/unit/utilities/ErrorWithCode.test.ts @@ -1,8 +1,8 @@ import { ErrorWithCode } from '@/utilities/customErrors/ErrorWithCode'; describe('UNIT - ErrorWithCode', () => { - it('should have a default 400 error code', () => { + it('should have a default 500 error code', () => { const err = new ErrorWithCode('test'); - expect(err.code).toEqual(400); + expect(err.code).toEqual(500); }); it('should use the error code provided', () => { diff --git a/react-app/src/components/users/UserDetail.tsx b/react-app/src/components/users/UserDetail.tsx index 4b25244d1..7d46436d8 100644 --- a/react-app/src/components/users/UserDetail.tsx +++ b/react-app/src/components/users/UserDetail.tsx @@ -10,13 +10,12 @@ import useDataLoader from '@/hooks/useDataLoader'; import { User } from '@/hooks/api/useUsersApi'; import { AuthContext } from '@/contexts/authContext'; import { Agency } from '@/hooks/api/useAgencyApi'; -import { Role } from '@/hooks/api/useRolesApi'; import TextFormField from '../form/TextFormField'; import DetailViewNavigation from '../display/DetailViewNavigation'; import { useGroupedAgenciesApi } from '@/hooks/api/useGroupedAgenciesApi'; import { useParams } from 'react-router-dom'; import useDataSubmitter from '@/hooks/useDataSubmitter'; -import { Roles } from '@/constants/roles'; +import { Role, Roles } from '@/constants/roles'; import { LookupContext } from '@/contexts/lookupContext'; interface IUserDetail { diff --git a/react-app/src/components/users/UsersTable.tsx b/react-app/src/components/users/UsersTable.tsx index 8faf852ec..a6e21738f 100644 --- a/react-app/src/components/users/UsersTable.tsx +++ b/react-app/src/components/users/UsersTable.tsx @@ -8,9 +8,9 @@ import { IUser } from '@/interfaces/IUser'; import { dateFormatter, statusChipFormatter } from '@/utilities/formatters'; import { GridApiCommunity } from '@mui/x-data-grid/internals'; import { Agency } from '@/hooks/api/useAgencyApi'; -import { Role } from '@/hooks/api/useRolesApi'; import { User } from '@/hooks/api/useUsersApi'; import { LookupContext } from '@/contexts/lookupContext'; +import { Role } from '@/constants/roles'; const CustomMenuItem = (props: PropsWithChildren & { value: string }) => { const theme = useTheme(); diff --git a/react-app/src/constants/roles.ts b/react-app/src/constants/roles.ts index ffb73787e..f618938cc 100644 --- a/react-app/src/constants/roles.ts +++ b/react-app/src/constants/roles.ts @@ -7,3 +7,12 @@ export enum Roles { GENERAL_USER = 'General User', AUDITOR = 'Auditor', } + +export interface KeycloakRole { + name: string; +} + +export interface Role { + Id: string; + Name: string; +} diff --git a/react-app/src/hooks/api/useLookupApi.ts b/react-app/src/hooks/api/useLookupApi.ts index 6aa0eb2e5..be6bb6cb8 100644 --- a/react-app/src/hooks/api/useLookupApi.ts +++ b/react-app/src/hooks/api/useLookupApi.ts @@ -2,9 +2,9 @@ import { ProjectRisk, TierLevel } from '@/hooks/api/useProjectsApi'; import { IFetch } from '../useFetch'; import { BuildingConstructionType, BuildingPredominateUse } from '@/hooks/api/useBuildingsApi'; import { PropertyClassification } from '@/interfaces/IProperty'; -import { Role } from '@/hooks/api/useRolesApi'; import { Agency } from '@/hooks/api/useAgencyApi'; import { AdministrativeArea } from '@/hooks/api/useAdministrativeAreaApi'; +import { Role } from '@/constants/roles'; export interface LookupObject { Name: string; diff --git a/react-app/src/hooks/api/useRolesApi.ts b/react-app/src/hooks/api/useRolesApi.ts deleted file mode 100644 index 8c8d262ca..000000000 --- a/react-app/src/hooks/api/useRolesApi.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { IFetch } from '../useFetch'; - -export interface KeycloakRole { - name: string; -} - -export interface Role { - Id: string; - Name: string; -} - -const useRolesApi = (absoluteFetch: IFetch) => { - const getKeycloakRoles = async () => { - const { parsedBody } = await absoluteFetch.get(`/users/roles/`); - return parsedBody as KeycloakRole[]; - }; - - const getInternalRoles = async () => { - const { parsedBody } = await absoluteFetch.get(`/roles/`); - return parsedBody as Role[]; - }; - - return { - getKeycloakRoles, - getInternalRoles, - }; -}; - -export default useRolesApi; diff --git a/react-app/src/hooks/api/useUsersApi.ts b/react-app/src/hooks/api/useUsersApi.ts index 1608626b6..aec158090 100644 --- a/react-app/src/hooks/api/useUsersApi.ts +++ b/react-app/src/hooks/api/useUsersApi.ts @@ -1,10 +1,9 @@ import { CommonFiltering } from '@/interfaces/ICommonFiltering'; import { FetchResponse, IFetch } from '../useFetch'; import { Agency } from './useAgencyApi'; -import { KeycloakRole, Role } from './useRolesApi'; +import { Role } from '@/constants/roles'; export interface User { - //temp interface, should standardize somehow Id: string; Username: string; FirstName: string; @@ -29,10 +28,6 @@ export interface AccessRequest { } const useUsersApi = (absoluteFetch: IFetch) => { - const getLatestAccessRequest = async () => { - const { parsedBody } = await absoluteFetch.get(`/users/access/requests`); - return parsedBody; - }; const getSelf = async (): Promise => { const a = await absoluteFetch.get(`/users/self`); return a.parsedBody as User; @@ -56,27 +51,16 @@ const useUsersApi = (absoluteFetch: IFetch) => { const response = await absoluteFetch.put(`/users/${userId}`, user); return response; }; - const updateUserRole = async (username: string, role: string) => { - const { parsedBody } = await absoluteFetch.put(`/users/roles/${username}`, [role]); - return parsedBody as KeycloakRole[]; - }; - const deleteUser = async (userId: string) => { - const { parsedBody } = await absoluteFetch.del(`/users/${userId}`, { Id: userId }); - return parsedBody; - }; const getUsersAgencyIds = async (username: string): Promise => { const { parsedBody } = await absoluteFetch.get(`/users/agencies/${username}`); return parsedBody as number[]; }; return { - getLatestAccessRequest, getSelf, submitAccessRequest, getAllUsers, getUserById, updateUser, - deleteUser, - updateUserRole, getUsersAgencyIds, }; }; diff --git a/react-app/src/hooks/usePimsApi.ts b/react-app/src/hooks/usePimsApi.ts index d23936bb5..70bf60f1a 100644 --- a/react-app/src/hooks/usePimsApi.ts +++ b/react-app/src/hooks/usePimsApi.ts @@ -3,7 +3,6 @@ import { useContext } from 'react'; import useFetch from './useFetch'; import useUsersApi from './api/useUsersApi'; import useAgencyApi from './api/useAgencyApi'; -import useRolesApi from './api/useRolesApi'; import useReportsApi from '@/hooks/api/useReportsApi'; import useBuildingsApi from './api/useBuildingsApi'; import useParcelsApi from './api/useParcelsApi'; @@ -27,7 +26,6 @@ const usePimsApi = () => { const users = useUsersApi(fetch); const agencies = useAgencyApi(fetch); - const roles = useRolesApi(fetch); const reports = useReportsApi(fetch); const buildings = useBuildingsApi(fetch); const parcels = useParcelsApi(fetch); @@ -44,7 +42,6 @@ const usePimsApi = () => { return { users, agencies, - roles, reports, buildings, parcels,