diff --git a/apps/server/prisma/migrations/20250203024748_add_user_scope_field/migration.sql b/apps/server/prisma/migrations/20250203024748_add_user_scope_field/migration.sql new file mode 100644 index 00000000..b329601b --- /dev/null +++ b/apps/server/prisma/migrations/20250203024748_add_user_scope_field/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - You are about to drop the column `isAdmin` on the `Employee` table. All the data in the column will be lost. + +*/ +-- CreateEnum +CREATE TYPE "EmployeeScope" AS ENUM ('BASE_USER', 'ADMIN'); + +-- AlterTable +ALTER TABLE "Employee" DROP COLUMN "isAdmin", +ADD COLUMN "scope" "EmployeeScope" NOT NULL DEFAULT 'BASE_USER'; diff --git a/apps/server/prisma/schema.prisma b/apps/server/prisma/schema.prisma index 09fa4057..1510271e 100644 --- a/apps/server/prisma/schema.prisma +++ b/apps/server/prisma/schema.prisma @@ -18,6 +18,11 @@ enum SignerType { USER_LIST } +enum EmployeeScope { + BASE_USER + ADMIN +} + // `Departments` represent the various departments that employees could work in. model Department { id String @id @default(uuid()) @db.Uuid @@ -39,7 +44,7 @@ model Employee { lastName String @db.VarChar(255) email String @unique @db.VarChar(255) signatureLink String @db.VarChar(255) - isAdmin Boolean @default(false) @db.Boolean + scope EmployeeScope @default(BASE_USER) pswdHash String? @db.VarChar(255) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/apps/server/prisma/seed.ts b/apps/server/prisma/seed.ts index 5fc852a4..9560cc61 100644 --- a/apps/server/prisma/seed.ts +++ b/apps/server/prisma/seed.ts @@ -1,4 +1,4 @@ -import { PrismaClient, SignerType } from '@prisma/client'; +import { PrismaClient, SignerType, EmployeeScope } from '@prisma/client'; import { v4 as uuidv4 } from 'uuid'; const prisma = new PrismaClient(); @@ -49,6 +49,7 @@ type EmployeeData = { email: string; positionId: string; signatureLink: string; + scope: EmployeeScope; }; // update or insert employee to database based on the employee id @@ -65,6 +66,7 @@ async function upsertEmployee(empData: EmployeeData) { position: { connect: { id: empData.positionId }, }, + scope: empData.scope, }, }); } @@ -389,6 +391,7 @@ async function main() { email: 'zhang.iri@northeastern.edu', positionId: CHIEF_OF_STAFF_UUID, signatureLink: DEV_SIGNATURE_LINK, + scope: EmployeeScope.BASE_USER, }, { id: KAI_ZHENG_UUID, @@ -397,6 +400,7 @@ async function main() { email: 'zheng.kaiy@northeastern.edu', positionId: CHIEF_FIN_OFFICER_UUID, signatureLink: DEV_SIGNATURE_LINK, + scope: EmployeeScope.BASE_USER, }, { id: ANGELA_WEIGL_UUID, @@ -405,6 +409,7 @@ async function main() { email: 'weigl.a@northeastern.edu', positionId: AGG_DIR_UUID, signatureLink: DEV_SIGNATURE_LINK, + scope: EmployeeScope.BASE_USER, }, { id: ANSHUL_SHIRUDE_UUID, @@ -413,6 +418,7 @@ async function main() { email: 'shirude.a@northeastern.edu', positionId: CHIEF_LEARNING_ENGAGEMENT_UUID, signatureLink: DEV_SIGNATURE_LINK, + scope: EmployeeScope.BASE_USER, }, ]; diff --git a/apps/server/src/app.controller.ts b/apps/server/src/app.controller.ts index 6574d926..3f1037d8 100644 --- a/apps/server/src/app.controller.ts +++ b/apps/server/src/app.controller.ts @@ -136,6 +136,7 @@ export class AppController { password: employeeDto.password, signatureLink: employeeDto.signatureLink, positionId: '', + scope: employeeDto.scope, }; const newEmployee = await this.authService.register( diff --git a/apps/server/src/auth/auth.service.spec.ts b/apps/server/src/auth/auth.service.spec.ts index 540b95e3..95bfab96 100644 --- a/apps/server/src/auth/auth.service.spec.ts +++ b/apps/server/src/auth/auth.service.spec.ts @@ -8,6 +8,7 @@ import { EmployeeEntity } from '../employees/entities/employee.entity'; import { PositionBaseEntity } from '../positions/entities/position.entity'; import { DepartmentsService } from '../departments/departments.service'; import { PositionsService } from '../positions/positions.service'; +import { EmployeeScope } from '@prisma/client'; describe('AuthService', () => { let service: AuthService; @@ -60,7 +61,7 @@ describe('AuthService', () => { updatedAt: new Date(1672531200), }, email: 'info@mfa.org', - isAdmin: false, + scope: EmployeeScope.BASE_USER, pswdHash: 'password', createdAt: new Date(1672531200), updatedAt: new Date(1672531200), @@ -89,7 +90,7 @@ describe('AuthService', () => { updatedAt: new Date(1672531200), }, email: 'info@mfa.org', - isAdmin: false, + scope: EmployeeScope.BASE_USER, createdAt: new Date(1672531200), updatedAt: new Date(1672531200), refreshToken: null, @@ -147,7 +148,7 @@ describe('AuthService', () => { firstName: 'First', lastName: 'Last', positionId: 'position-id', - isAdmin: false, + scope: EmployeeScope.BASE_USER, pswdHash: null, createdAt: new Date(0), updatedAt: new Date(0), diff --git a/apps/server/src/auth/auth.service.ts b/apps/server/src/auth/auth.service.ts index f0ecc831..7decae5c 100644 --- a/apps/server/src/auth/auth.service.ts +++ b/apps/server/src/auth/auth.service.ts @@ -50,7 +50,7 @@ export class AuthService { sub: user.id, positionId: user.positionId, departmentId: user.position.departmentId, - isAdmin: user.isAdmin, + scope: user.scope, }; const [accessToken, refreshToken] = await Promise.all([ diff --git a/apps/server/src/auth/dto/register-employee.dto.ts b/apps/server/src/auth/dto/register-employee.dto.ts index c742ad41..9c5619da 100644 --- a/apps/server/src/auth/dto/register-employee.dto.ts +++ b/apps/server/src/auth/dto/register-employee.dto.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; +import { EmployeeScope } from '@prisma/client'; import { IsNotEmpty, IsString, MinLength, IsEmail } from 'class-validator'; export class RegisterEmployeeDto { @@ -37,4 +38,8 @@ export class RegisterEmployeeDto { @IsNotEmpty() @ApiProperty() signatureLink: string; + + @IsNotEmpty() + @ApiProperty({ enum: EmployeeScope }) + scope: EmployeeScope; } diff --git a/apps/server/src/auth/guards/admin-auth.guard.ts b/apps/server/src/auth/guards/admin-auth.guard.ts index d75f4128..fe0e97dd 100644 --- a/apps/server/src/auth/guards/admin-auth.guard.ts +++ b/apps/server/src/auth/guards/admin-auth.guard.ts @@ -4,6 +4,7 @@ import { UnauthorizedException, } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; +import { EmployeeScope } from '@prisma/client'; import { Request } from 'express'; @Injectable() @@ -23,7 +24,7 @@ export class AdminAuthGuard extends AuthGuard('jwt') { handleRequest(err: any, user: any, info: any) { // You can throw an exception based on either "info" or "err" arguments - if (err || !user || !user.isAdmin) { + if (err || !user || !(user.scope == EmployeeScope.ADMIN)) { console.log(info); throw err || new UnauthorizedException(); } diff --git a/apps/server/src/employees/dto/create-employee.dto.ts b/apps/server/src/employees/dto/create-employee.dto.ts index 8f705560..b4f69b28 100644 --- a/apps/server/src/employees/dto/create-employee.dto.ts +++ b/apps/server/src/employees/dto/create-employee.dto.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; +import { EmployeeScope } from '@prisma/client'; import { IsEmail, IsNotEmpty, @@ -38,4 +39,9 @@ export class CreateEmployeeDto { @IsNotEmpty() @ApiProperty() signatureLink: string; + + @IsString() + @IsNotEmpty() + @ApiProperty({ enum: EmployeeScope }) + scope: EmployeeScope; } diff --git a/apps/server/src/employees/employees.controller.spec.ts b/apps/server/src/employees/employees.controller.spec.ts index b5fa6fdc..b54661ea 100644 --- a/apps/server/src/employees/employees.controller.spec.ts +++ b/apps/server/src/employees/employees.controller.spec.ts @@ -4,6 +4,7 @@ import { EmployeesService } from './employees.service'; import { PrismaService } from '../prisma/prisma.service'; import { EmployeeEntity } from './entities/employee.entity'; import { LoggerServiceImpl } from '../logger/logger.service'; +import { EmployeeScope } from '@prisma/client'; describe('EmployeesController', () => { let controller: EmployeesController; @@ -47,7 +48,7 @@ describe('EmployeesController', () => { }, }, email: 'john.doe@example.com', - isAdmin: false, + scope: EmployeeScope.BASE_USER, pswdHash: 'thisIsASecureHash', createdAt: new Date(1672531200), updatedAt: new Date(1672531200), @@ -74,7 +75,7 @@ describe('EmployeesController', () => { }, }, email: 'bilbo.baggins@example.com', - isAdmin: false, + scope: EmployeeScope.BASE_USER, pswdHash: 'thisIsASecureHash', createdAt: new Date(1672531200), updatedAt: new Date(1672531200), @@ -104,7 +105,7 @@ describe('EmployeesController', () => { }, }, email: 'john.doe@example.com', - isAdmin: false, + scope: EmployeeScope.BASE_USER, pswdHash: 'thisIsASecureHash', createdAt: new Date(1672531200), updatedAt: new Date(1672531200), @@ -131,7 +132,7 @@ describe('EmployeesController', () => { }, }, email: 'bilbo.baggins@example.com', - isAdmin: false, + scope: EmployeeScope.BASE_USER, pswdHash: 'thisIsASecureHash', createdAt: new Date(1672531200), updatedAt: new Date(1672531200), diff --git a/apps/server/src/employees/employees.service.ts b/apps/server/src/employees/employees.service.ts index b888e54d..6e5f457f 100644 --- a/apps/server/src/employees/employees.service.ts +++ b/apps/server/src/employees/employees.service.ts @@ -25,6 +25,7 @@ export class EmployeesService { createEmployeeDto.password, await bcrypt.genSalt(), ), + scope: createEmployeeDto.scope, }, include: { position: { diff --git a/apps/server/src/employees/entities/employee.entity.ts b/apps/server/src/employees/entities/employee.entity.ts index 227fc3e6..8ea65a4f 100644 --- a/apps/server/src/employees/entities/employee.entity.ts +++ b/apps/server/src/employees/entities/employee.entity.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Employee } from '@prisma/client'; +import { Employee, EmployeeScope } from '@prisma/client'; import { PositionBaseEntity } from './../../positions/entities/position.entity'; import { Exclude } from 'class-transformer'; @@ -19,12 +19,12 @@ export class EmployeeBaseEntity implements Employee { @ApiProperty() email: string; - @ApiProperty() - isAdmin: boolean; - @ApiProperty() signatureLink: string; + @ApiProperty({ enum: EmployeeScope }) + scope: EmployeeScope; + @Exclude() pswdHash: string | null; diff --git a/apps/server/src/metadata.ts b/apps/server/src/metadata.ts index 7b8a176a..24609201 100644 --- a/apps/server/src/metadata.ts +++ b/apps/server/src/metadata.ts @@ -1,3 +1,5 @@ +import { EmployeeScope } from '@prisma/client'; + /* eslint-disable */ export default async () => { const t = { @@ -107,6 +109,7 @@ export default async () => { positionId: { required: true, type: () => String }, email: { required: true, type: () => String }, password: { required: true, type: () => String, minLength: 5 }, + scope: { required: true, type: () => EmployeeScope }, }, }, ], diff --git a/apps/server/src/signatures/signatures.controller.spec.ts b/apps/server/src/signatures/signatures.controller.spec.ts index 50e643e1..1d284831 100644 --- a/apps/server/src/signatures/signatures.controller.spec.ts +++ b/apps/server/src/signatures/signatures.controller.spec.ts @@ -8,7 +8,7 @@ import { PositionsService } from '../positions/positions.service'; import { UpdateSignatureSignerDto } from './dto/update-signature-signer.dto'; import { SignerType } from '@prisma/client'; -let mockSignatureService = { +const mockSignatureService = { updateSigner: async ( signatureId: string, updateSignatureSignerDto: UpdateSignatureSignerDto, @@ -57,7 +57,7 @@ describe('SignaturesController', () => { }); it('should update signer', async () => { - let response = await controller.updateSignatureSigner('signature-id', { + const response = await controller.updateSignatureSigner('signature-id', { signerType: SignerType.DEPARTMENT, signerDepartmentId: 'department-id', }); diff --git a/apps/web/src/client/schemas.gen.ts b/apps/web/src/client/schemas.gen.ts index b335c10d..94706d63 100644 --- a/apps/web/src/client/schemas.gen.ts +++ b/apps/web/src/client/schemas.gen.ts @@ -35,6 +35,10 @@ export const RegisterEmployeeDtoSchema = { signatureLink: { type: 'string', }, + scope: { + type: 'string', + enum: ['BASE_USER', 'ADMIN'], + }, }, required: [ 'firstName', @@ -44,6 +48,7 @@ export const RegisterEmployeeDtoSchema = { 'positionName', 'departmentName', 'signatureLink', + 'scope', ], } as const; @@ -121,12 +126,13 @@ export const EmployeeEntitySchema = { email: { type: 'string', }, - isAdmin: { - type: 'boolean', - }, signatureLink: { type: 'string', }, + scope: { + type: 'string', + enum: ['BASE_USER', 'ADMIN'], + }, position: { $ref: '#/components/schemas/PositionBaseEntity', }, @@ -155,8 +161,8 @@ export const EmployeeEntitySchema = { 'firstName', 'lastName', 'email', - 'isAdmin', 'signatureLink', + 'scope', 'position', 'positionId', 'pswdHash', @@ -188,6 +194,10 @@ export const CreateEmployeeDtoSchema = { signatureLink: { type: 'string', }, + scope: { + type: 'string', + enum: ['BASE_USER', 'ADMIN'], + }, }, required: [ 'firstName', @@ -196,6 +206,7 @@ export const CreateEmployeeDtoSchema = { 'email', 'password', 'signatureLink', + 'scope', ], } as const; @@ -214,6 +225,10 @@ export const UpdateEmployeeDtoSchema = { signatureLink: { type: 'string', }, + scope: { + type: 'string', + enum: ['BASE_USER', 'ADMIN'], + }, }, } as const; @@ -245,12 +260,13 @@ export const EmployeeBaseEntitySchema = { email: { type: 'string', }, - isAdmin: { - type: 'boolean', - }, signatureLink: { type: 'string', }, + scope: { + type: 'string', + enum: ['BASE_USER', 'ADMIN'], + }, positionId: { type: 'string', }, @@ -276,8 +292,8 @@ export const EmployeeBaseEntitySchema = { 'firstName', 'lastName', 'email', - 'isAdmin', 'signatureLink', + 'scope', 'positionId', 'pswdHash', 'createdAt', diff --git a/apps/web/src/client/types.gen.ts b/apps/web/src/client/types.gen.ts index e86b19ba..a3f7fa7f 100644 --- a/apps/web/src/client/types.gen.ts +++ b/apps/web/src/client/types.gen.ts @@ -4,6 +4,11 @@ export type JwtEntity = { accessToken: string; }; +export enum Scope { + BASE_USER = 'BASE_USER', + ADMIN = 'ADMIN', +} + export type RegisterEmployeeDto = { firstName: string; lastName: string; @@ -12,6 +17,7 @@ export type RegisterEmployeeDto = { positionName: string; departmentName: string; signatureLink: string; + scope: 'BASE_USER' | 'ADMIN'; }; export type DepartmentEntity = { @@ -36,8 +42,8 @@ export type EmployeeEntity = { firstName: string; lastName: string; email: string; - isAdmin: boolean; signatureLink: string; + scope: 'BASE_USER' | 'ADMIN'; position: PositionBaseEntity; positionId: string; pswdHash: string | null; @@ -53,6 +59,7 @@ export type CreateEmployeeDto = { email: string; password: string; signatureLink: string; + scope: 'BASE_USER' | 'ADMIN'; }; export type UpdateEmployeeDto = { @@ -60,6 +67,7 @@ export type UpdateEmployeeDto = { lastName?: string; positionId?: string; signatureLink?: string; + scope?: 'BASE_USER' | 'ADMIN'; }; export type CreatePositionDto = { @@ -72,8 +80,8 @@ export type EmployeeBaseEntity = { firstName: string; lastName: string; email: string; - isAdmin: boolean; signatureLink: string; + scope: 'BASE_USER' | 'ADMIN'; positionId: string; pswdHash: string | null; createdAt: string; diff --git a/apps/web/src/context/AuthContext.tsx b/apps/web/src/context/AuthContext.tsx index 3715554a..3e1e0d6a 100644 --- a/apps/web/src/context/AuthContext.tsx +++ b/apps/web/src/context/AuthContext.tsx @@ -6,6 +6,8 @@ import { useMsal } from '@azure/msal-react'; import { loginRequest } from '@web/authConfig'; import { callMsGraph } from '@web/graph'; import { useMutation } from '@tanstack/react-query'; +import { Scope } from '@web/client'; + import { appControllerLogin, appControllerLogout, @@ -58,7 +60,7 @@ export const AuthProvider = ({ children }: any) => { email: decoded.email, firstName: decoded.firstName, lastName: decoded.lastName, - isAdmin: decoded.isAdmin, + scope: decoded.scope, }; setUser(user); @@ -105,7 +107,7 @@ export const AuthProvider = ({ children }: any) => { email: employee.data.email, firstName: employee.data.firstName, lastName: employee.data.lastName, - isAdmin: employee.data.isAdmin, + scope: Scope.BASE_USER, }); }) .catch(async (_error) => { @@ -207,6 +209,7 @@ export const AuthProvider = ({ children }: any) => { position: string, department: string, signatureLink: string, + scope: RegisterEmployeeDto['scope'], ) => { const employee: RegisterEmployeeDto = { email: email, @@ -216,6 +219,7 @@ export const AuthProvider = ({ children }: any) => { departmentName: department, positionName: position, signatureLink: signatureLink, + scope: scope, }; registerEmployeeMutation.mutate( diff --git a/apps/web/src/context/types.ts b/apps/web/src/context/types.ts index c26361c8..79902f77 100644 --- a/apps/web/src/context/types.ts +++ b/apps/web/src/context/types.ts @@ -3,6 +3,8 @@ import { FormFields, FieldGroups, } from '@web/components/createFormTemplate/createFormTemplateEditor/FormEditor'; +import { EmployeeScope } from '@prisma/client'; +import { RegisterEmployeeDto } from '@web/client'; // for storage in context export type User = { @@ -12,7 +14,7 @@ export type User = { email: string; firstName: string; lastName: string; - isAdmin: boolean; + scope: EmployeeScope; }; // jwt payload returned from server export type jwtPayload = { @@ -22,7 +24,7 @@ export type jwtPayload = { email: string; firstName: string; lastName: string; - isAdmin: boolean; + scope: EmployeeScope; }; export interface AuthContextType { @@ -38,6 +40,7 @@ export interface AuthContextType { position: string, department: string, signatureLink: string, + scope: RegisterEmployeeDto['scope'], ) => void; logout: () => void; } diff --git a/apps/web/src/pages/register.tsx b/apps/web/src/pages/register.tsx index 7022c695..3f4dc2ce 100644 --- a/apps/web/src/pages/register.tsx +++ b/apps/web/src/pages/register.tsx @@ -4,16 +4,12 @@ import { useAuth } from '@web/hooks/useAuth'; import { useQuery } from '@tanstack/react-query'; import { useBlob } from '@web/hooks/useBlob'; import { SignaturePad } from './../components/SignaturePad'; -import { - departmentsControllerFindAll, - positionsControllerFindAllInDepartmentName, - DepartmentEntity, - PositionEntity, -} from '@web/client'; +import { DepartmentEntity, PositionEntity } from '@web/client'; import { departmentsControllerFindAllOptions, positionsControllerFindAllInDepartmentNameOptions, } from '@web/client/@tanstack/react-query.gen'; +import { Scope } from '@web/client'; export default function Register() { const { completeRegistration, userData } = useAuth(); @@ -116,6 +112,7 @@ export default function Register() { currentDepartmentName, currentPositionName, uploadedBlob?.url || 'http://localhost:3002/signature.png', + Scope.BASE_USER, ); };