Merge branch 'fix/responsive-layout-overflow' into feat/user-service-ts

This commit is contained in:
Alex Holliday
2026-01-16 19:15:24 +00:00
11 changed files with 152 additions and 111 deletions
@@ -3,6 +3,8 @@
min-height: 100vh;
margin: 0 auto;
padding: 0;
overflow-x: hidden;
width: 100%;
}
/* TODO go for this approach for responsiveness. The aside needs to be taken care of */
@@ -30,4 +32,6 @@
max-width: 1400px;
margin: 0 auto;
flex: 1;
min-width: 0;
overflow-x: hidden;
}
+6 -4
View File
@@ -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",
-105
View File
@@ -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;
+95
View File
@@ -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<User, "id" | "teamId" | "createdAt" | "updatedAt"> & {
teamId?: Types.ObjectId;
profileImage?: Required<UserProfileImage>;
};
interface UserDocument extends UserDocumentBase {
_id: Types.ObjectId;
teamId?: Types.ObjectId;
createdAt: Date;
updatedAt: Date;
}
const profileImageSchema = new Schema<Required<UserProfileImage>>(
{
data: { type: Buffer },
contentType: { type: String },
},
{ _id: false }
);
const UserSchema = new Schema<UserDocument>(
{
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<UserDocument>("User", UserSchema);
export type { UserDocument };
export { UserModel };
export default UserModel;
+3
View File
@@ -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";
+3
View File
@@ -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";
@@ -0,0 +1,9 @@
import type { User } from "@/types/index.js";
export interface IUsersRepository {
// create
// fetch
// update
// delete
// other
}
@@ -0,0 +1,4 @@
import { IUsersRepository } from "@/repositories/index.js";
class MongoUserRepository implements IUsersRepository {}
export default MongoUserRepository;
+3 -2
View File
@@ -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<User>, 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 {
+1
View File
@@ -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";
+24
View File
@@ -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;
}