Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User Management Navigator Supervisor Backend Implementation #120

Merged
merged 1 commit into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions app/src/controllers/accessRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { userService, accessRequestService } from '../services';

import type { NextFunction, Request, Response } from 'express';

import type { UserAccessRequest } from '../types';

const controller = {
// Request to create user & access
createUserAccessRevokeRequest: async (req: Request, res: Response, next: NextFunction) => {
// TODO check if the calling user is a supervisor or an admin
try {
let response;
const { user, accessRequest } = req.body;
if (accessRequest?.grant === false) {
response = await accessRequestService.createUserAccessRevokeRequest(accessRequest);
res.status(201).json(response);
} else {
const userResponse = await userService.createUserIfNew(user);
if (userResponse) {
accessRequest.userId = userResponse.userId;
response = userResponse as UserAccessRequest;
response.accessRequest = await accessRequestService.createUserAccessRevokeRequest(accessRequest);
res.status(201).json(response);
} else {
// TODO check if the user is a proponent
// Put an entry in accessRequest table
// Send 409 if the user is not a proponent
res.status(409).json({ message: 'User already exists' });
}
}
} catch (e: unknown) {
next(e);
}
},

getAccessRequests: async (req: Request, res: Response, next: NextFunction) => {
try {
const response = await accessRequestService.getAccessRequests();
res.status(200).json(response);
} catch (e: unknown) {
next(e);
}
}
};

export default controller;
1 change: 1 addition & 0 deletions app/src/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as accessRequestController } from './accessRequest';
export { default as documentController } from './document';
export { default as enquiryController } from './enquiry';
export { default as noteController } from './note';
Expand Down
3 changes: 2 additions & 1 deletion app/src/controllers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ const controller = {
firstName: req.query.firstName as string,
fullName: req.query.fullName as string,
lastName: req.query.lastName as string,
active: isTruthy(req.query.active as string)
active: isTruthy(req.query.active as string),
role: req.query.role as string
});
res.status(200).json(response);
} catch (e: unknown) {
Expand Down
51 changes: 51 additions & 0 deletions app/src/db/migrations/20240717000000_007_access-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import stamps from '../stamps';

import type { Knex } from 'knex';

export async function up(knex: Knex): Promise<void> {
return (
Promise.resolve()
// Create the access_request table
.then(() =>
knex.schema.createTable('access_request', (table) => {
table.uuid('access_request_id').primary();
table.uuid('user_id').notNullable().references('user_id').inTable('user');
table.text('role');
table
.enu('status', ['Approved', 'Pending', 'Rejected'], {
useNative: true,
enumName: 'access_request_status_enum'
})
.defaultTo('Pending')
.notNullable();
table.boolean('grant').notNullable();
stamps(knex, table);
})
)

.then(() =>
knex.schema.raw(`create trigger before_update_access_request_trigger
before update on public.access_request
for each row execute procedure public.set_updated_at();`)
)

.then(() =>
knex.schema.raw(`CREATE TRIGGER audit_access_request_trigger
AFTER UPDATE OR DELETE ON access_request
FOR EACH ROW EXECUTE PROCEDURE audit.if_modified_func();`)
)
);
}

export async function down(knex: Knex): Promise<void> {
return (
Promise.resolve()
// Drop triggers
.then(() => knex.schema.raw('DROP TRIGGER IF EXISTS before_update_access_request_trigger ON access_request'))
.then(() => knex.schema.raw('DROP TRIGGER IF EXISTS audit_access_request_trigger ON access_request'))
// Drop the access_request table
.then(() => knex.schema.dropTableIfExists('access_request'))
// Drop the access_request_status_enum type
.then(() => knex.schema.raw('DROP TYPE IF EXISTS access_request_status_enum'))
);
}
33 changes: 33 additions & 0 deletions app/src/db/models/access_request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Prisma } from '@prisma/client';

import { AccessRequestStatus } from '../../utils/enums/application';

import type { Stamps } from '../stamps';
import type { AccessRequest } from '../../types/AccessRequest'; // Import the access_request_status_enum type

// Define types
const _accessRequest = Prisma.validator<Prisma.access_requestDefaultArgs>()({});

type PrismaRelationAccessRequest = Omit<Prisma.access_requestGetPayload<typeof _accessRequest>, keyof Stamps>;

export default {
toPrismaModel(input: AccessRequest): PrismaRelationAccessRequest {
return {
access_request_id: input.accessRequestId,
grant: input.grant,
role: input.role,
status: input.status as AccessRequestStatus, // Cast the status property to AccessRequestStatus enum
user_id: input.userId
};
},

fromPrismaModel(input: PrismaRelationAccessRequest): AccessRequest {
return {
accessRequestId: input.access_request_id,
grant: input.grant,
role: input.role,
userId: input.user_id as string,
status: input.status as AccessRequestStatus // Cast the status property to AccessRequestStatus enum
};
}
};
1 change: 1 addition & 0 deletions app/src/db/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { default as activity } from './activity';
export { default as access_request } from './access_request';
export { default as document } from './document';
export { default as enquiry } from './enquiry';
export { default as identity_provider } from './identity_provider';
Expand Down
20 changes: 20 additions & 0 deletions app/src/db/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ model user {
created_at DateTime? @default(now()) @db.Timestamptz(6)
updated_by String?
updated_at DateTime? @db.Timestamptz(6)
access_request access_request[]
enquiry enquiry[]
submission submission[]
identity_provider identity_provider? @relation(fields: [idp], references: [idp], onDelete: Cascade, map: "user_idp_foreign")
Expand Down Expand Up @@ -245,3 +246,22 @@ model enquiry {
activity activity @relation(fields: [activity_id], references: [activity_id], onDelete: Cascade, map: "enquiry_activity_id_foreign")
user user? @relation(fields: [assigned_user_id], references: [user_id], onDelete: Cascade, map: "enquiry_assigned_user_id_foreign")
}

model access_request {
access_request_id String @id @db.Uuid
user_id String @db.Uuid
role String?
status access_request_status_enum @default(Pending)
grant Boolean
created_by String? @default("00000000-0000-0000-0000-000000000000")
created_at DateTime? @default(now()) @db.Timestamptz(6)
updated_by String?
updated_at DateTime? @db.Timestamptz(6)
user user @relation(fields: [user_id], references: [user_id], onDelete: NoAction, onUpdate: NoAction, map: "access_request_user_id_foreign")
}

enum access_request_status_enum {
Approved
Pending
Rejected
}
30 changes: 30 additions & 0 deletions app/src/routes/v1/accessRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import express from 'express';
import { accessRequestController } from '../../controllers';
import { requireSomeAuth } from '../../middleware/requireSomeAuth';
import { accessRequestValidator } from '../../validators';

import type { NextFunction, Request, Response } from 'express';

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

// Request to create/revoke a user and access request - called by supervisor(201) & admin(200)
router.post(
'/',
accessRequestValidator.userAccessRevokeRequest,
(req: Request, res: Response, next: NextFunction): void => {
accessRequestController.createUserAccessRevokeRequest(req, res, next);
}
);

// Approve/Deny access/revoke request - called by admin (200)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
router.patch('/', (req: Request, res: Response, next: NextFunction): void => {
// TODO: approve/deny access request
});

router.get('/', (req: Request, res: Response, next: NextFunction): void => {
accessRequestController.getAccessRequests(req, res, next);
});

export default router;
14 changes: 13 additions & 1 deletion app/src/routes/v1/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { currentUser } from '../../middleware/authentication';

import express from 'express';

import accessRequest from './accessRequest';
import document from './document';
import enquiry from './enquiry';
import note from './note';
Expand All @@ -17,10 +18,21 @@ router.use(currentUser);
// Base v1 Responder
router.get('/', (_req, res) => {
res.status(200).json({
endpoints: ['/document', '/enquiry', '/note', '/permit', '/roadmap', '/sso', '/submission', '/user']
endpoints: [
'/accessRequest',
'/document',
'/enquiry',
'/note',
'/permit',
'/roadmap',
'/sso',
'/submission',
'/user'
]
});
});

router.use('/accessRequest', accessRequest);
router.use('/document', document);
router.use('/enquiry', enquiry);
router.use('/note', note);
Expand Down
2 changes: 1 addition & 1 deletion app/src/routes/v1/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { NextFunction, Request, Response } from 'express';
const router = express.Router();
router.use(requireSomeAuth);

// Submission endpoint
// Search for users
router.get('/', userValidator.searchUsers, (req: Request, res: Response, next: NextFunction): void => {
userController.searchUsers(req, res, next);
});
Expand Down
59 changes: 59 additions & 0 deletions app/src/services/accessRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// import jwt from 'jsonwebtoken';
// import { Prisma } from '@prisma/client';
import { v4 as uuidv4 } from 'uuid';

import prisma from '../db/dataConnection';
import { access_request } from '../db/models';
import { AccessRequestStatus } from '../utils/enums/application';

import type { AccessRequest } from '../types';

/**
* The User DB Service
*/
const service = {
/**
* @function createUserAccessRequest
* Create an access_request record
* @param {object} data Incoming accessRequest data
* @returns {Promise<object>} The result of running the insert operation
* @throws The error encountered upon db transaction failure
*/
createUserAccessRevokeRequest: async (accessRequest: AccessRequest) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any

const newAccessRequest = {
accessRequestId: uuidv4(),
userId: accessRequest.userId,
grant: accessRequest.grant as boolean,
role: accessRequest.role as string,
status: AccessRequestStatus.PENDING
};
const accessRequestResponse = await prisma.access_request.create({
data: access_request.toPrismaModel(newAccessRequest)
});

return access_request.fromPrismaModel(accessRequestResponse);
},

/**
* @function getAccessRequests
* Get all access requests
* @returns {Promise<object>} The result of running the find operation
*/
getAccessRequests: async () => {
const response = await prisma.access_request.findMany();
return response.map((x) => access_request.fromPrismaModel(x));
},

/**
* @function updateUserRole
* Updates user role
* @returns {Promise<object>} The result of running the put operation
*/
updateUserRole: async () => {
// TODO: Implement updateUserRole
}
};

export default service;
1 change: 1 addition & 0 deletions app/src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as accessRequestService } from './accessRequest';
export { default as activityService } from './activity';
export { default as comsService } from './coms';
export { default as documentService } from './document';
Expand Down
28 changes: 28 additions & 0 deletions app/src/services/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { v4 as uuidv4, NIL } from 'uuid';

import prisma from '../db/dataConnection';
import { identity_provider, user } from '../db/models';
import { AccessRole } from '../utils/enums/application';
import { parseIdentityKeyClaims } from '../utils/utils';

import type { User, UserSearchParameters } from '../types';
Expand Down Expand Up @@ -117,6 +118,28 @@ const service = {
return response;
},

/**
* @function createUserIfNew
* Create a user DB record if it does not exist
* @param {object} data Incoming user data
* @returns {Promise<object>} The result of running the insert operation
* @throws The error encountered upon db transaction failure
*/
createUserIfNew: async (data: User) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let response: any;
response = await prisma.user.findFirst({
where: {
identity_id: data.identityId,
idp: data.idp
}
});

if (!response) {
response = await service.createUser(data);
return user.fromPrismaModel(response);
} else return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Transaction not required at this level.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

},
/**
* @function getCurrentUserId
* Gets userId (primary identifier of a user in db) of currentUser.
Expand Down Expand Up @@ -262,6 +285,11 @@ const service = {
}
});

if ((params.role as string) === AccessRole.PCNS_NAVIGATOR) {
// eslint-disable-next-line max-len
// TODO filter out any use without a role of PCNS_NAVIGATOR|PCNS_READ_ONLY or a pending access request from the response
}

return response.map((x) => user.fromPrismaModel(x));
},

Expand Down
10 changes: 10 additions & 0 deletions app/src/types/AccessRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { IStamps } from '../interfaces/IStamps';
import { AccessRequestStatus } from '../utils/enums/application';

export type AccessRequest = {
accessRequestId: string; // Primary key
grant: boolean;
role: string | null;
status: AccessRequestStatus;
userId: string;
} & Partial<IStamps>;
4 changes: 4 additions & 0 deletions app/src/types/UserAccessRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { AccessRequest, User } from '.';
export type UserAccessRequest = {
accessRequest?: AccessRequest;
} & User;
1 change: 1 addition & 0 deletions app/src/types/UserSearchParameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export type UserSearchParameters = {
fullName?: string;
lastName?: string;
active?: boolean;
role?: string;
};
Loading
Loading