From 2724c728a61fb61f986c4bc51adc64412f7ee3f2 Mon Sep 17 00:00:00 2001 From: Caden Cheng Date: Sun, 1 Mar 2026 20:39:51 -0800 Subject: [PATCH 1/5] Add user model with RBAC fields --- models/User.ts | 101 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 92 insertions(+), 9 deletions(-) diff --git a/models/User.ts b/models/User.ts index bbc79c3..02a4318 100644 --- a/models/User.ts +++ b/models/User.ts @@ -1,9 +1,92 @@ -// Example - -export type User = { - id: number; - name: string; - email: string; - createdAt: string; - updatedAt: string; -}; + import mongoose, { Schema, Document } from "mongoose"; + + export type Role = "PI" | "LAB_MANAGER" | "RESEARCHER" | "VIEWER"; + + // describes what one lab membership looks like + export interface ILabMembership { + labId: string; + role: Role; + } + + // shape of a user document in mongodb + export interface IUser extends Document { + ucsdId: string; + email: string; + name: { + first: string; + last: string; + }; + permissions: Role[]; + labs: ILabMembership[]; + notificationPreferences: { + email: boolean; + sms: boolean; + inApp: boolean; + }; + safety: { + trainingCompleted: string[]; + clearanceLevel: string; + lastReviwedAt: Date; + }; + profile: { + title: string; + department: string; + phone: string; + } + status: "ACTIVE" | "INACTIVE" | "SUSPENDED"; + createdAt: Date; + lastLoginAt: Date; + } + + +const userSchema = new Schema({ + ucsdId: { type: String }, + email: { type: String, required: true, unique: true }, + + name: { + first: { type: String, required: true }, + last: { type: String, required: true }, + }, + permissions: [ + { type: String, enum: ["PI", "LAB_MANAGER", "RESEARCHER", "VIEWER"] } + ], + + labs: [ + { + labId: { type: String, required: true }, + role: { + type: String, + enum: ["PI", "LAB_MANAGER", "RESEARCHER", "VIEWER"], + required: true, + }, + }, + ], + notificationPreferences: { + email: { type: Boolean, default: true }, + inApp: { type: Boolean, default: true }, + sms: { type: Boolean, default: false }, + }, + safety: { + trainingCompleted: [{ type: String }], + clearenceLevel: { type: String }, + lastReviewedAt: { type: Date }, + }, + + profile: { + title: { type: String }, + department: { type: String }, + phone: { type: String }, + }, + status: { + type: String, + enum: ["ACTIVE", "INACTIVE", "SUSPENDED"], + default: "ACTIVE", + }, + lastLoginAt: { type: Date }, + +}, +{ + timestamps: true, +}); + +export const User = mongoose.models.User || mongoose.model("User", userSchema); \ No newline at end of file From 704cf85436a7ec5129c638d2785dacec7411be73 Mon Sep 17 00:00:00 2001 From: Caden Cheng Date: Sun, 1 Mar 2026 21:16:48 -0800 Subject: [PATCH 2/5] Updated user model schema with comments --- models/User.ts | 89 +++++++++++++++++++++++++++----------------------- 1 file changed, 48 insertions(+), 41 deletions(-) diff --git a/models/User.ts b/models/User.ts index 02a4318..819fb46 100644 --- a/models/User.ts +++ b/models/User.ts @@ -1,55 +1,58 @@ - import mongoose, { Schema, Document } from "mongoose"; +import mongoose, { Schema, Document } from "mongoose"; - export type Role = "PI" | "LAB_MANAGER" | "RESEARCHER" | "VIEWER"; +export type Role = "PI" | "LAB_MANAGER" | "RESEARCHER" | "VIEWER"; - // describes what one lab membership looks like - export interface ILabMembership { - labId: string; - role: Role; - } +// describes what one lab membership looks like +export interface ILabMembership { + labId: string; + role: Role; +} - // shape of a user document in mongodb - export interface IUser extends Document { - ucsdId: string; - email: string; - name: { - first: string; - last: string; - }; - permissions: Role[]; - labs: ILabMembership[]; - notificationPreferences: { - email: boolean; - sms: boolean; - inApp: boolean; - }; - safety: { - trainingCompleted: string[]; - clearanceLevel: string; - lastReviwedAt: Date; - }; - profile: { - title: string; - department: string; - phone: string; - } - status: "ACTIVE" | "INACTIVE" | "SUSPENDED"; - createdAt: Date; - lastLoginAt: Date; +// shape of a user document in mongodb +export interface IUser extends Document { + ucsdId: string; // ucsd pid + email: string; // ucsd email + name: { + first: string; + last: string; + }; + permissions: Role[]; + labs: ILabMembership[]; + notificationPreferences: { + email: boolean; + sms: boolean; + inApp: boolean; + }; + safety: { + trainingCompleted: string[]; + clearanceLevel: string; + lastReviwedAt: Date; + }; + profile: { + title: string; + department: string; + phone: string; } + status: "ACTIVE" | "INACTIVE" | "SUSPENDED"; + createdAt: Date; + lastLoginAt: Date; +} - +// mongoose schema to valid the data const userSchema = new Schema({ - ucsdId: { type: String }, - email: { type: String, required: true, unique: true }, + + ucsdId: { type: String }, // ucsd pid + email: { type: String, required: true, unique: true }, // ucsd email name: { first: { type: String, required: true }, last: { type: String, required: true }, }, - permissions: [ + permissions: [ // global roles - enum restricts to only these 4 valid roles { type: String, enum: ["PI", "LAB_MANAGER", "RESEARCHER", "VIEWER"] } ], + + // each entry represents membership in one lab labs: [ { @@ -61,11 +64,13 @@ const userSchema = new Schema({ }, }, ], - notificationPreferences: { + notificationPreferences: { // default notification preferences email: { type: Boolean, default: true }, inApp: { type: Boolean, default: true }, - sms: { type: Boolean, default: false }, + sms: { type: Boolean, default: true }, }, + + // checks for if you completed training and if you are cleared safety: { trainingCompleted: [{ type: String }], clearenceLevel: { type: String }, @@ -89,4 +94,6 @@ const userSchema = new Schema({ timestamps: true, }); + +//creates and exports the mongoosem odel export const User = mongoose.models.User || mongoose.model("User", userSchema); \ No newline at end of file From f5c885bc0fde5548b50d24ae2bdecfdd530bc125 Mon Sep 17 00:00:00 2001 From: Caden Cheng Date: Tue, 3 Mar 2026 21:17:09 -0800 Subject: [PATCH 3/5] Add getSession() and ROLE_PERMISSIONS to rbac.ts --- lib/rbac.ts | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 lib/rbac.ts diff --git a/lib/rbac.ts b/lib/rbac.ts new file mode 100644 index 0000000..3456cb3 --- /dev/null +++ b/lib/rbac.ts @@ -0,0 +1,53 @@ +import { connectToDatabase } from "@/lib/mongoose"; +import { auth } from "@/auth"; +import { User, Role } from "@/models/User"; + +// establish the role permissions + +const ROLE_PERMISSIONS: Record = { + PI: [ + "inventory:create", + "inventory:update", + "inventory:delete", + "inventory:set_threshold", + "transfer:approve", + "payment:approve", + "lab:manage_users", + ], + LAB_MANAGER: [ + "inventory:create", + "inventory:update", + "inventory:set_threshold", + "transfer:request" + ], + RESEARCHER: [ + "inventory:view", + "transfer:request", + "listing:create" + ], + VIEWER: [ + "inventory:view" + ] +}; + +// checks if the user has permission to perform an action + +export async function getSession(permission: string) { + const session = await auth(); + + if (!session?.user?.email) { + return { allowed: false, user: null }; + } + + await connectToDatabase(); + const user = await User.findOne({ email: session.user.email }); + + if (!user){ + return { allowed: false, user: null }; + } + + const allowed = (user.permissions as Role[]).some( + (role) => ROLE_PERMISSIONS[role]?.includes(permission) + ); + return { allowed, user }; +} \ No newline at end of file From 5120b174a07c4962e8c4f5b8075d27c2514c162b Mon Sep 17 00:00:00 2001 From: Caden Cheng Date: Sun, 8 Mar 2026 22:16:40 -0700 Subject: [PATCH 4/5] Updated based on comments --- lib/rbac.ts | 18 ++++++++++-------- models/User.ts | 24 +++++++++++++----------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/lib/rbac.ts b/lib/rbac.ts index 3456cb3..b35973a 100644 --- a/lib/rbac.ts +++ b/lib/rbac.ts @@ -1,6 +1,6 @@ import { connectToDatabase } from "@/lib/mongoose"; import { auth } from "@/auth"; -import { User, Role } from "@/models/User"; +import { User, Role, IUser } from "@/models/User"; // establish the role permissions @@ -36,18 +36,20 @@ export async function getSession(permission: string) { const session = await auth(); if (!session?.user?.email) { - return { allowed: false, user: null }; + return { allowed: false, user: null, reason: "Unauthenticated" }; } await connectToDatabase(); - const user = await User.findOne({ email: session.user.email }); + const user = await User.findOne({ email: session.user.email }).lean(); if (!user){ - return { allowed: false, user: null }; + return { allowed: false, user: null, reason: "User not found" }; } - const allowed = (user.permissions as Role[]).some( - (role) => ROLE_PERMISSIONS[role]?.includes(permission) - ); - return { allowed, user }; + if (user.status !== "ACTIVE") { + return { allowed: false, user, reason: "Account inactive" }; + } + + const allowed = ROLE_PERMISSIONS[user.role]?.includes(permission) ?? false; + return { allowed, user, reason: allowed ? undefined : "Insufficient permissions" }; } \ No newline at end of file diff --git a/models/User.ts b/models/User.ts index 819fb46..e42a3bd 100644 --- a/models/User.ts +++ b/models/User.ts @@ -1,10 +1,10 @@ -import mongoose, { Schema, Document } from "mongoose"; +import mongoose, { Schema, Document, Types } from "mongoose"; export type Role = "PI" | "LAB_MANAGER" | "RESEARCHER" | "VIEWER"; // describes what one lab membership looks like export interface ILabMembership { - labId: string; + labId: Types.ObjectId; role: Role; } @@ -16,7 +16,7 @@ export interface IUser extends Document { first: string; last: string; }; - permissions: Role[]; + role: Role; labs: ILabMembership[]; notificationPreferences: { email: boolean; @@ -26,7 +26,7 @@ export interface IUser extends Document { safety: { trainingCompleted: string[]; clearanceLevel: string; - lastReviwedAt: Date; + lastReviewedAt: Date; }; profile: { title: string; @@ -41,22 +41,24 @@ export interface IUser extends Document { // mongoose schema to valid the data const userSchema = new Schema({ - ucsdId: { type: String }, // ucsd pid + ucsdId: { type: String, required: true, unique: true }, // ucsd pid email: { type: String, required: true, unique: true }, // ucsd email name: { first: { type: String, required: true }, last: { type: String, required: true }, }, - permissions: [ // global roles - enum restricts to only these 4 valid roles - { type: String, enum: ["PI", "LAB_MANAGER", "RESEARCHER", "VIEWER"] } - ], + role: { // global role - enum restricts to only these 4 valid roles + type: String, + enum: ["PI", "LAB_MANAGER", "RESEARCHER", "VIEWER"], + required: true, + }, // each entry represents membership in one lab labs: [ { - labId: { type: String, required: true }, + labId: { type: Schema.Types.ObjectId, ref: "Lab", required: true }, role: { type: String, enum: ["PI", "LAB_MANAGER", "RESEARCHER", "VIEWER"], @@ -67,13 +69,13 @@ const userSchema = new Schema({ notificationPreferences: { // default notification preferences email: { type: Boolean, default: true }, inApp: { type: Boolean, default: true }, - sms: { type: Boolean, default: true }, + sms: { type: Boolean, default: false }, }, // checks for if you completed training and if you are cleared safety: { trainingCompleted: [{ type: String }], - clearenceLevel: { type: String }, + clearanceLevel: { type: String }, lastReviewedAt: { type: Date }, }, From 152e178cd4503859ce311a8deaa76d05d5f035a1 Mon Sep 17 00:00:00 2001 From: Caden Cheng Date: Mon, 6 Apr 2026 20:07:09 -0700 Subject: [PATCH 5/5] Add email validation and labs.labsId index to User model --- models/User.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/models/User.ts b/models/User.ts index e42a3bd..a312097 100644 --- a/models/User.ts +++ b/models/User.ts @@ -1,6 +1,7 @@ import mongoose, { Schema, Document, Types } from "mongoose"; export type Role = "PI" | "LAB_MANAGER" | "RESEARCHER" | "VIEWER"; +export type Status = "ACTIVE" | "INACTIVE" | "SUSPENDED"; // describes what one lab membership looks like export interface ILabMembership { @@ -33,7 +34,7 @@ export interface IUser extends Document { department: string; phone: string; } - status: "ACTIVE" | "INACTIVE" | "SUSPENDED"; + status: Status; createdAt: Date; lastLoginAt: Date; } @@ -42,7 +43,9 @@ export interface IUser extends Document { const userSchema = new Schema({ ucsdId: { type: String, required: true, unique: true }, // ucsd pid - email: { type: String, required: true, unique: true }, // ucsd email + email: { type: String, required: true, unique: true, trim: true, lowercase: true, + match:[/^\S+@ucsd\.edu$/, "Must be a valid UCSD email (@ucsd.edu)"], + }, // ucsd email name: { first: { type: String, required: true }, @@ -95,7 +98,8 @@ const userSchema = new Schema({ { timestamps: true, }); +userSchema.index({"labs.labId": 1}); -//creates and exports the mongoosem odel +//creates and exports the mongoose model export const User = mongoose.models.User || mongoose.model("User", userSchema); \ No newline at end of file