Skip to content
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
48 changes: 48 additions & 0 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"private": true,
"type": "module",
"dependencies": {
"@casl/ability": "^6.7.3",
"@types/jest": "^30.0.0",
"@types/node": "^24.0.3",
"@types/react": "^19.1.8",
Expand Down
59 changes: 59 additions & 0 deletions server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"license": "ISC",
"description": "",
"dependencies": {
"@casl/ability": "^6.7.3",
"@casl/mongoose": "^8.0.3",
"bcryptjs": "^3.0.1",
"compression": "^1.8.0",
"cors": "^2.8.5",
Expand Down
5 changes: 4 additions & 1 deletion server/src/middleware/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import User from '@/models/users';
import { AuthenticatedRequest } from '@/types/auth';
import { verifyAuthToken } from '@/utils/authTokenHandler';
import defineAbilitiesForUser from '@/utils/roleBasedAccess';

//dotenv.config({ path: './.env' });

Expand Down Expand Up @@ -34,7 +35,6 @@

// Add the decoded token to the request object
req.user = {
id: decodedAuthToken.id || decodedAuthToken.employeeId,
employeeId: decodedAuthToken.employeeId,
role: decodedAuthToken.role,
firstName: decodedAuthToken.firstName
Expand Down Expand Up @@ -64,8 +64,11 @@
return;
}

// Add role authorization to the request
req.authorization = defineAbilitiesForUser(req, user.id, user.permissions);

next();
} catch (err: any) {

Check warning on line 71 in server/src/middleware/auth.ts

View workflow job for this annotation

GitHub Actions / lint-and-type-check (server)

Unexpected any. Specify a different type
res.status(401).json({ message: `Invalid Token: ${err.name}` });
return;
}
Expand Down
9 changes: 9 additions & 0 deletions server/src/models/users.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import mongoose, { Model, Schema } from 'mongoose';

import { IUser } from '@/types/models';
import { ACTION_ENUM, SUBJECT_ENUM, CONDITION_ENUM } from '@/utils/roleDefinitions';

// User schema definition
// This schema defines the structure of the user data in the MongoDB database.
Expand All @@ -27,6 +28,14 @@ const userSchema = new Schema<IUser>(
type: String,
enum: ['Pending', 'Approved', 'Rejected'] as const,
default: 'Pending'
},
permissions: {
type: [{
action: { type: String, enum: ACTION_ENUM, required: true },
subject: { type: String, enum: SUBJECT_ENUM, required: true },
condition: { type: String, enum: CONDITION_ENUM, required: true }
}],
default: []
}
},
{ timestamps: true }
Expand Down
44 changes: 33 additions & 11 deletions server/src/routes/auth.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { subject } from '@casl/ability';
import { accessibleBy } from '@casl/mongoose';
import express, { Request, Response } from 'express';
import twilio from 'twilio';

Expand All @@ -10,6 +12,7 @@ import {
SignupRequest
} from '@/types/auth';
import { generateAuthToken } from '@/utils/authTokenHandler';
import { ACTIONS, SUBJECTS } from '@/utils/roleDefinitions';

const router = express.Router();

Expand Down Expand Up @@ -184,13 +187,13 @@ router.get(
'/users',
[auth],
async (req: AuthenticatedRequest, res: Response): Promise<void> => {
if (req.user?.role !== 'Admin') {
if (!req.authorization) {
res.sendStatus(403);
return;
}
try {
const users = await User.find(
{},
accessibleBy(req.authorization).ofType(SUBJECTS.USER),
'firstName lastName role approvalStatus'
);
res.json(users);
Expand All @@ -205,9 +208,14 @@ router.get(

router.put(
'/users/:id/approve',
auth,
[auth],
async (req: AuthenticatedRequest, res: Response): Promise<void> => {
if (req.user?.role !== 'Admin') {
if (
!req.authorization?.can(
ACTIONS.CUSTOM.APPROVE,
subject(SUBJECTS.USER, { _id: req.params.id }),
)
) {
res.sendStatus(403);
return;
}
Expand Down Expand Up @@ -241,7 +249,7 @@ router.post(
'/preapprove',
auth,
async (req: AuthenticatedRequest, res: Response): Promise<void> => {
if (req.user?.role !== 'Admin') {
if (!req.authorization?.can(ACTIONS.CUSTOM.PREAPPROVE, SUBJECTS.USER)) {
res.sendStatus(403);
return;
}
Expand Down Expand Up @@ -278,8 +286,10 @@ router.get(
auth,
async (req: AuthenticatedRequest, res: Response): Promise<void> => {
if (
!['Admin', 'Manager'].includes(req.user?.role || '') &&
req.user?.employeeId !== req.params.employeeId
!req.authorization?.can(
ACTIONS.CASL.READ,
subject(SUBJECTS.USER, { employeeId: req.params.employeeId })
)
) {
res.sendStatus(403);
return;
Expand Down Expand Up @@ -310,8 +320,10 @@ router.put(
auth,
async (req: AuthenticatedRequest, res: Response): Promise<void> => {
if (
req.user?.role !== 'Admin' &&
req.user?.employeeId !== req.params.employeeId
!req.authorization?.can(
ACTIONS.CASL.UPDATE,
subject(SUBJECTS.USER, { employeeId: req.params.employeeId })
)
) {
res.sendStatus(403);
return;
Expand Down Expand Up @@ -348,7 +360,12 @@ router.get(
'/users/by-id/:id',
auth,
async (req: AuthenticatedRequest, res: Response): Promise<void> => {
if (req.user?.role !== 'Admin') {
if (
!req.authorization?.can(
ACTIONS.CASL.READ,
subject(SUBJECTS.USER, { _id: req.params.id })
)
) {
res.sendStatus(403);
return;
}
Expand All @@ -375,7 +392,12 @@ router.put(
'/users/by-id/:id',
auth,
async (req: AuthenticatedRequest, res: Response): Promise<void> => {
if (req.user?.role !== 'Admin') {
if (
!req.authorization?.can(
ACTIONS.CASL.UPDATE,
subject(SUBJECTS.USER, { _id: req.params.id })
)
) {
res.sendStatus(403);
return;
}
Expand Down
4 changes: 2 additions & 2 deletions server/src/types/auth.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { Ability } from '@/utils/roleDefinitions';
import { Request } from 'express';
import { JwtPayload } from 'jsonwebtoken';

export interface AuthenticatedRequest extends Request {
user?: {
id: string;
employeeId: string;
role: string;
firstName: string;
};
authorization?: Ability;
}

export interface JWTPayload extends JwtPayload {
id: string;
employeeId: string;
role: string;
}
Expand All @@ -30,4 +30,4 @@
role: string;
}

export interface LoginRequest extends VerifyOTPRequest {}

Check failure on line 33 in server/src/types/auth.ts

View workflow job for this annotation

GitHub Actions / lint-and-type-check (server)

An interface declaring no members is equivalent to its supertype
8 changes: 8 additions & 0 deletions server/src/types/models.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Document, Types } from 'mongoose';
import { Action, Subject, Condition } from '@/utils/roleDefinitions';

export interface IReferralCode {
// QR code that will be distributed to participants, and will be used to create child surveys
Expand All @@ -15,7 +16,7 @@
employeeName: string;
// All survey responses stored as key-value pairs
// Keys are question IDs, values are the responses
responses: Record<string, any>;

Check warning on line 19 in server/src/types/models.ts

View workflow job for this annotation

GitHub Actions / lint-and-type-check (server)

Unexpected any. Specify a different type
// Date the survey was created
createdAt: Date;
// Date the survey was last updated
Expand Down Expand Up @@ -43,4 +44,11 @@
approvalStatus: 'Pending' | 'Approved' | 'Rejected';
createdAt: Date;
updatedAt: Date;
permissions: IPermission[];
}

export interface IPermission {
action: Action;
subject: Subject;
condition?: Condition;
}
Loading
Loading