diff --git a/lib/rbac.ts b/lib/rbac.ts new file mode 100644 index 0000000..b35973a --- /dev/null +++ b/lib/rbac.ts @@ -0,0 +1,55 @@ +import { connectToDatabase } from "@/lib/mongoose"; +import { auth } from "@/auth"; +import { User, Role, IUser } 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, reason: "Unauthenticated" }; + } + + await connectToDatabase(); + const user = await User.findOne({ email: session.user.email }).lean(); + + if (!user){ + return { allowed: false, user: null, reason: "User not found" }; + } + + 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 bbc79c3..a312097 100644 --- a/models/User.ts +++ b/models/User.ts @@ -1,9 +1,105 @@ -// Example - -export type User = { - id: number; - name: string; - email: string; - createdAt: string; - updatedAt: string; -}; +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 { + labId: Types.ObjectId; + role: Role; +} + +// 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; + }; + role: Role; + labs: ILabMembership[]; + notificationPreferences: { + email: boolean; + sms: boolean; + inApp: boolean; + }; + safety: { + trainingCompleted: string[]; + clearanceLevel: string; + lastReviewedAt: Date; + }; + profile: { + title: string; + department: string; + phone: string; + } + status: Status; + createdAt: Date; + lastLoginAt: Date; +} + +// mongoose schema to valid the data +const userSchema = new Schema({ + + ucsdId: { type: String, required: true, unique: true }, // ucsd pid + 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 }, + last: { type: String, required: true }, + }, + 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: Schema.Types.ObjectId, ref: "Lab", required: true }, + role: { + type: String, + enum: ["PI", "LAB_MANAGER", "RESEARCHER", "VIEWER"], + required: true, + }, + }, + ], + notificationPreferences: { // default notification preferences + email: { type: Boolean, default: true }, + inApp: { type: Boolean, default: true }, + sms: { type: Boolean, default: false }, + }, + + // checks for if you completed training and if you are cleared + safety: { + trainingCompleted: [{ type: String }], + clearanceLevel: { 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, +}); +userSchema.index({"labs.labId": 1}); + + +//creates and exports the mongoose model +export const User = mongoose.models.User || mongoose.model("User", userSchema); \ No newline at end of file