Skip to content

Commit

Permalink
user mgmt supervisor backend implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
sanjaytkbabu committed Jul 23, 2024
1 parent b318a9f commit 195d013
Show file tree
Hide file tree
Showing 22 changed files with 562 additions and 80 deletions.
37 changes: 37 additions & 0 deletions app/src/controllers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,34 @@ import { addDashesToUuid, mixedQueryToArray, isTruthy } from '../utils/utils';
import type { NextFunction, Request, Response } from 'express';

const controller = {
// Request to create user & access
createUserAccessRequest: async (req: Request, res: Response, next: NextFunction) => {
try {
const response = await userService.createUserAccessRequest(req.body);
res.status(201).json(response);
} catch (e: unknown) {
next(e);
}
},

getNavsAndAccessRequests: async (req: Request, res: Response, next: NextFunction) => {
try {
const response = await userService.getNavsAndAccessRequests();
res.status(200).json(response);
} catch (e: unknown) {
next(e);
}
},
// Request to revoke user access
revokeUserAccessRequest: async (req: Request, res: Response, next: NextFunction) => {
try {
const response = await userService.revokeUserAccessRequest(req.body);
res.status(200).json(response);
} catch (e: unknown) {
next(e);
}
},

searchUsers: async (req: Request, res: Response, next: NextFunction) => {
try {
const userIds = mixedQueryToArray(req.query.userId as string);
Expand All @@ -23,6 +51,15 @@ const controller = {
} catch (e: unknown) {
next(e);
}
},

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

Expand Down
57 changes: 57 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,57 @@
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')
.onUpdate('CASCADE')
.onDelete('CASCADE');
// TODO: make a reference to role table
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);
table.unique(['user_id']);
})
)

.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'))
);
}
32 changes: 32 additions & 0 deletions app/src/db/models/access_request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Prisma } from '@prisma/client';

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

// 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 UserStatus, // Cast the status property to UserStatus 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 UserStatus // Cast the status property to UserStatus 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
11 changes: 11 additions & 0 deletions app/src/db/models/user.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { Prisma } from '@prisma/client';

import access_request from './access_request';

import type { Stamps } from '../stamps';
import type { User } from '../../types/User';

// Define types
const _user = Prisma.validator<Prisma.userDefaultArgs>()({});
const _userWithAccessRequestGraph = Prisma.validator<Prisma.userDefaultArgs>()({ include: { access_request: true } });

type PrismaRelationUser = Omit<Prisma.userGetPayload<typeof _user>, keyof Stamps>;
type PrismaGraphUserAccessRequest = Prisma.userGetPayload<typeof _userWithAccessRequestGraph>;

export default {
toPrismaModel(input: User): PrismaRelationUser {
Expand Down Expand Up @@ -35,5 +39,12 @@ export default {
lastName: input.last_name,
active: input.active
};
},
fromPrismaModelWithAccessRequest(input: PrismaGraphUserAccessRequest): User {
const user = this.fromPrismaModel(input);
if (input.access_request) {
user.accessRequest = access_request.fromPrismaModel(input.access_request);
}
return user;
}
};
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 @unique(map: "access_request_user_id_unique") @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: Cascade, map: "access_request_user_id_foreign")
}

enum access_request_status_enum {
Approved
Pending
Rejected
}
18 changes: 17 additions & 1 deletion app/src/routes/v1/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,25 @@ 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);
});
// Request to revoke a user
router.put('/request/access', (req: Request, res: Response, next: NextFunction): void => {
userController.revokeUserAccessRequest(req, res, next);
});
// Request to create a user and access request
router.post('/request/access', (req: Request, res: Response, next: NextFunction): void => {
userController.createUserAccessRequest(req, res, next);
});
// Update user role
router.put('/role', (req: Request, res: Response, next: NextFunction): void => {
userController.updateUserRole(req, res, next);
});

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

export default router;
91 changes: 89 additions & 2 deletions app/src/services/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { Prisma } from '@prisma/client';
import { v4 as uuidv4, NIL } from 'uuid';

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

import type { User, UserSearchParameters } from '../types';
import type { AccessRequest, User, UserSearchParameters } from '../types';

const trxWrapper = (etrx: Prisma.TransactionClient | undefined = undefined) => (etrx ? etrx : prisma);

Expand Down Expand Up @@ -117,6 +118,46 @@ const service = {
return response;
},

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

if (!oldUser) {
await prisma.$transaction(async (trx) => {
response = await service.createUser(data, trx);

const newAccessRequest = {
accessRequestId: uuidv4(),
userId: response?.user_id,
grant: data.accessRequest?.grant as boolean,
role: data.accessRequest?.role as string,
status: UserStatus.PENDING
};
const accessreqeustresponse = await trx.access_request.create({
data: access_request.toPrismaModel(newAccessRequest)
});

response.access_request = accessreqeustresponse;
});
return user.fromPrismaModelWithAccessRequest(response);
} else {
return user.fromPrismaModel(oldUser);
}
},

/**
* @function getCurrentUserId
* Gets userId (primary identifier of a user in db) of currentUser.
Expand All @@ -136,6 +177,24 @@ const service = {
return user && user.user_id ? user.user_id : defaultValue;
},

/**
* @function listNavigators
* Lists all the
* @param {boolean} [active] Optional boolean on user active status
* @returns {Promise<object>} The result of running the find operation
*/
getNavsAndAccessRequests: async () => {
const response = await prisma.user.findMany({
include: {
access_request: true
},
where: {
active: true
}
});
return response.map((x) => user.fromPrismaModelWithAccessRequest(x));
},

/**
* @function listIdps
* Lists all known identity providers
Expand Down Expand Up @@ -324,6 +383,34 @@ const service = {
// Nothing to update
return oldUser;
}
},
/**
* @function revokeUserAccessRequest
* Updates user access
* @returns {Promise<object>} The result of running the put operation
*/
revokeUserAccessRequest: async (data: AccessRequest) => {
const newAccessRequest = {
accessRequestId: uuidv4(),
userId: data.userId,
grant: data.grant,
role: null,
status: UserStatus.PENDING
};
const response = await prisma.access_request.create({
data: access_request.toPrismaModel(newAccessRequest)
});

return access_request.fromPrismaModel(response);
},
/**
* @function updateUserRole
* Updates user role
* @returns {Promise<object>} The result of running the put operation
*/
updateUserRole: async (user: User) => {
console.log(user);

Check warning on line 412 in app/src/services/user.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (16.x)

Unexpected console statement

Check warning on line 412 in app/src/services/user.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (18.x)

Unexpected console statement

Check warning on line 412 in app/src/services/user.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (20.x)

Unexpected console statement
// TODO: Implement updateUserRole
}
};

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 { UserStatus } from '../utils/enums/application';

export type AccessRequest = {
accessRequestId: string; // Primary key
grant: boolean;
role: string | null;
status: UserStatus;
userId: string;
} & Partial<IStamps>;
3 changes: 3 additions & 0 deletions app/src/types/User.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { IStamps } from '../interfaces/IStamps';

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

export type User = {
userId?: string; // Primary Key
identityId: string;
Expand All @@ -10,4 +12,5 @@ export type User = {
fullName: string | null;
lastName: string | null;
active: boolean;
accessRequest?: AccessRequest | null;
} & Partial<IStamps>;
1 change: 1 addition & 0 deletions app/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export type { Activity } from './Activity';
export type { AccessRequest } from './AccessRequest';
export type { BringForward } from './BringForward';
export type { ChefsFormConfig, ChefsFormConfigData } from './ChefsFormConfig';
export type { ChefsSubmissionExport } from './ChefsSubmissionExport';
Expand Down
5 changes: 5 additions & 0 deletions app/src/utils/enums/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ export enum Initiative {
HOUSING = 'HOUSING'
}

export enum UserStatus {
APPROVED = 'Approved',
PENDING = 'Pending',
REJECTED = 'Rejected'
}
export enum Regex {
/**
* Generic email regex modified to require domain of at least 2 characters
Expand Down
Loading

0 comments on commit 195d013

Please sign in to comment.