From 64dc381e0f527c7ec6f7214983d7c3a1674447c9 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 16 Jan 2026 19:14:03 +0000 Subject: [PATCH] separate user from token --- server/src/controllers/authController.ts | 10 +- server/src/db/models/User.js | 105 ------------------ server/src/db/models/User.ts | 95 ++++++++++++++++ server/src/db/models/index.ts | 3 + server/src/repositories/index.ts | 3 + .../src/repositories/users/IUserRepository.ts | 9 ++ .../repositories/users/MongoUserRepository.ts | 4 + server/src/service/business/userService.ts | 5 +- server/src/types/index.ts | 1 + server/src/types/user.ts | 24 ++++ 10 files changed, 148 insertions(+), 111 deletions(-) delete mode 100755 server/src/db/models/User.js create mode 100644 server/src/db/models/User.ts create mode 100644 server/src/repositories/users/IUserRepository.ts create mode 100644 server/src/repositories/users/MongoUserRepository.ts create mode 100644 server/src/types/user.ts diff --git a/server/src/controllers/authController.ts b/server/src/controllers/authController.ts index d033aa39d..435813a36 100755 --- a/server/src/controllers/authController.ts +++ b/server/src/controllers/authController.ts @@ -32,11 +32,13 @@ class AuthController { registerUser = async (req: Request, res: Response, next: NextFunction) => { try { - if (req.body?.email) { - req.body.email = req.body.email?.toLowerCase(); + const newUser = req.body.user; + const newUserToken = req.body.token; + if (newUser?.email) { + newUser.email = newUser.email.toLowerCase(); } - await registrationBodyValidation.validateAsync(req.body); - const { user, token } = await this.userService.registerUser(req.body, req.file); + await registrationBodyValidation.validateAsync(newUser); + const { user, token } = await this.userService.registerUser(newUser, newUserToken, req.file); res.status(200).json({ success: true, msg: "User registered successfully", diff --git a/server/src/db/models/User.js b/server/src/db/models/User.js deleted file mode 100755 index bf3012b26..000000000 --- a/server/src/db/models/User.js +++ /dev/null @@ -1,105 +0,0 @@ -import mongoose from "mongoose"; -import bcrypt from "bcryptjs"; -import Monitor from "./Monitor.js"; -import Team from "./Team.js"; -import Notification from "./Notification.js"; - -const UserSchema = mongoose.Schema( - { - firstName: { - type: String, - required: true, - }, - lastName: { - type: String, - required: true, - }, - email: { - type: String, - required: true, - unique: true, - }, - password: { - type: String, - required: true, - }, - avatarImage: { - type: String, - }, - profileImage: { - data: Buffer, - contentType: String, - }, - isActive: { - type: Boolean, - default: true, - }, - isVerified: { - type: Boolean, - default: false, - }, - role: { - type: [String], - default: "user", - enum: ["user", "admin", "superadmin", "demo"], - }, - teamId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Team", - immutable: true, - }, - checkTTL: { - type: Number, - }, - }, - { - timestamps: true, - } -); - -UserSchema.pre("save", function (next) { - if (!this.isModified("password")) { - return next(); - } - const salt = bcrypt.genSaltSync(10); - this.password = bcrypt.hashSync(this.password, salt); - next(); -}); - -UserSchema.pre("findOneAndUpdate", function (next) { - const update = this.getUpdate(); - if ("password" in update) { - const salt = bcrypt.genSaltSync(10); - update.password = bcrypt.hashSync(update.password, salt); - } - - next(); -}); - -UserSchema.pre("findOneAndDelete", async function (next) { - try { - const userToDelete = await this.model.findOne(this.getFilter()); - if (!userToDelete) return next(); - if (userToDelete.role.includes("superadmin")) { - await Team.deleteOne({ _id: userToDelete.teamId }); - await Monitor.deleteMany({ userId: userToDelete._id }); - await this.model.deleteMany({ - teamId: userToDelete.teamId, - _id: { $ne: userToDelete._id }, - }); - await Notification.deleteMany({ teamId: userToDelete.teamId }); - } - next(); - } catch (error) { - next(error); - } -}); - -UserSchema.methods.comparePassword = async function (submittedPassword) { - const res = await bcrypt.compare(submittedPassword, this.password); - return res; -}; - -const User = mongoose.model("User", UserSchema); - -export default User; diff --git a/server/src/db/models/User.ts b/server/src/db/models/User.ts new file mode 100644 index 000000000..e00250340 --- /dev/null +++ b/server/src/db/models/User.ts @@ -0,0 +1,95 @@ +import { Schema, model, type Types } from "mongoose"; +import bcrypt from "bcryptjs"; +import type { User, UserProfileImage, UserRole } from "@/types/index.js"; +import { MonitorModel } from "@/db/models/index.js"; +import Team from "./Team.js"; +import Notification from "./Notification.js"; + +type UserDocumentBase = Omit & { + teamId?: Types.ObjectId; + profileImage?: Required; +}; + +interface UserDocument extends UserDocumentBase { + _id: Types.ObjectId; + teamId?: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const profileImageSchema = new Schema>( + { + data: { type: Buffer }, + contentType: { type: String }, + }, + { _id: false } +); + +const UserSchema = new Schema( + { + firstName: { type: String, required: true }, + lastName: { type: String, required: true }, + email: { type: String, required: true, unique: true }, + password: { type: String, required: true }, + avatarImage: { type: String }, + profileImage: { type: profileImageSchema }, + isActive: { type: Boolean, default: true }, + isVerified: { type: Boolean, default: false }, + role: { + type: [String], + enum: ["user", "admin", "superadmin", "demo" satisfies UserRole], + default: ["user"], + }, + teamId: { + type: Schema.Types.ObjectId, + ref: "Team", + immutable: true, + }, + checkTTL: { type: Number }, + }, + { timestamps: true } +); + +UserSchema.pre("save", function (next) { + if (!this.isModified("password")) { + return next(); + } + const salt = bcrypt.genSaltSync(10); + this.password = bcrypt.hashSync(this.password, salt); + next(); +}); + +UserSchema.pre("findOneAndUpdate", function (next) { + const update = this.getUpdate(); + if (update && "password" in update) { + const salt = bcrypt.genSaltSync(10); + (update as any).password = bcrypt.hashSync((update as any).password, salt); + } + next(); +}); + +UserSchema.pre("findOneAndDelete", async function (next) { + try { + const userToDelete = await this.model.findOne(this.getFilter()); + if (!userToDelete) return next(); + if (userToDelete.role.includes("superadmin")) { + await Team.deleteOne({ _id: userToDelete.teamId }); + await MonitorModel.deleteMany({ userId: userToDelete._id }); + await this.model.deleteMany({ teamId: userToDelete.teamId, _id: { $ne: userToDelete._id } }); + await Notification.deleteMany({ teamId: userToDelete.teamId }); + } + next(); + } catch (error) { + next(error as Error); + } +}); + +UserSchema.methods.comparePassword = async function (submittedPassword: string) { + return bcrypt.compare(submittedPassword, this.password); +}; + +const UserModel = model("User", UserSchema); + +export type { UserDocument }; +export { UserModel }; +export default UserModel; diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index b92ae18b7..d91aa0778 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -9,3 +9,6 @@ export { default as MonitorStatsModel } from "@/db/models/MonitorStats.js"; export * from "@/db/models/StatusPage.js"; export { default as StatusPageModel } from "@/db/models/StatusPage.js"; + +export * from "@/db/models/User.js"; +export { default as UserModel } from "@/db/models/User.js"; diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 2c27591f1..92d9b2371 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -9,3 +9,6 @@ export { default as MongoMonitorStatsRepository } from "@/repositories/monitor-s export * from "@/repositories/status-pages/IStatusPagesRepository.js"; export { default as MongoStatusPagesRepository } from "@/repositories/status-pages/MongoStatusPagesRepository.js"; + +export * from "@/repositories/users/IUserRepository.js"; +export { default as MongoUserRepository } from "@/repositories/users/MongoUserRepository.js"; diff --git a/server/src/repositories/users/IUserRepository.ts b/server/src/repositories/users/IUserRepository.ts new file mode 100644 index 000000000..259ba781b --- /dev/null +++ b/server/src/repositories/users/IUserRepository.ts @@ -0,0 +1,9 @@ +import type { User } from "@/types/index.js"; + +export interface IUsersRepository { + // create + // fetch + // update + // delete + // other +} diff --git a/server/src/repositories/users/MongoUserRepository.ts b/server/src/repositories/users/MongoUserRepository.ts new file mode 100644 index 000000000..82c5eb5b6 --- /dev/null +++ b/server/src/repositories/users/MongoUserRepository.ts @@ -0,0 +1,4 @@ +import { IUsersRepository } from "@/repositories/index.js"; +class MongoUserRepository implements IUsersRepository {} + +export default MongoUserRepository; diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index 129ed1e59..e409c9f98 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -1,4 +1,5 @@ import { IMonitorsRepository } from "@/repositories/index.js"; +import type { User } from "@/types/index.js"; const SERVICE_NAME = "userService"; @@ -62,12 +63,12 @@ class UserService { return this.jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL }); }; - registerUser = async (user: any, file: any) => { + registerUser = async (user: Partial, inviteToken: string, file: any) => { // Create a new user // If superAdmin exists, a token should be attached to all further register requests const superAdminExists = await this.db.userModule.checkSuperadmin(); if (superAdminExists) { - const invitedUser = await this.db.inviteModule.getInviteTokenAndDelete(user.inviteToken); + const invitedUser = await this.db.inviteModule.getInviteTokenAndDelete(inviteToken); user.role = invitedUser.role; user.teamId = invitedUser.teamId; } else { diff --git a/server/src/types/index.ts b/server/src/types/index.ts index 4f71788f2..b6495497d 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -3,3 +3,4 @@ export * from "@/types/monitor.js"; export * from "@/types/monitorStats.js"; export * from "@/types/statusPage.js"; export * from "@/types/network.js"; +export * from "@/types/user.js"; diff --git a/server/src/types/user.ts b/server/src/types/user.ts new file mode 100644 index 000000000..41c009791 --- /dev/null +++ b/server/src/types/user.ts @@ -0,0 +1,24 @@ +export const UserRoles = ["user", "admin", "superadmin", "demo"] as const; +export type UserRole = (typeof UserRoles)[number]; + +export interface UserProfileImage { + data?: Buffer; + contentType?: string; +} + +export interface User { + id: string; + firstName: string; + lastName: string; + email: string; + password: string; + avatarImage?: string; + profileImage?: UserProfileImage; + isActive: boolean; + isVerified: boolean; + role: UserRole[]; + teamId: string; + checkTTL?: number; + createdAt: string; + updatedAt: string; +}