Merge pull request #3149 from bluewave-labs/feat/user-service-ts

feat/user service ts
This commit is contained in:
Alexander Holliday authored and GitHub committed 2026-01-17 11:09:29 -08:00
commit 288ac08cd6
42 files changed
+851 -366

No files matched your search

@@ -32,7 +32,6 @@ const ActionsMenu = ({
event.preventDefault();
event.stopPropagation();
let monitor = { id: actions.id };
console.log(monitor);
await deleteMonitor({ monitor });
updateRowCallback();
};
+16 -13
View File
@@ -10,21 +10,24 @@ const initialState = {
msg: null,
};
export const register = createAsyncThunk("auth/register", async (form, thunkApi) => {
try {
const res = await networkService.registerUser(form);
return res.data;
} catch (error) {
if (error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
export const register = createAsyncThunk(
"auth/register",
async ({ user, token }, thunkApi) => {
try {
const res = await networkService.registerUser(user, token);
return res.data;
} catch (error) {
if (error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
});
);
export const login = createAsyncThunk("auth/login", async (form, thunkApi) => {
try {
-1
View File
@@ -344,7 +344,6 @@ export const useDeleteMonitor = () => {
const deleteMonitor = async ({ monitor, redirect }) => {
try {
setIsLoading(true);
console.log(monitor);
await networkService.deleteMonitorById({ monitorId: monitor.id });
createToast({ body: "Monitor deleted successfully!" });
if (redirect) {
@@ -104,7 +104,7 @@ const TeamPanel = () => {
team = team.map((member) => ({
...member,
id: member._id,
id: member.id,
role: member.role.map((role) => ROLE_MAP[role]).join(","),
}));
setData(team);
@@ -268,6 +268,7 @@ const TeamPanel = () => {
cursor: isSuperAdmin ? "pointer" : "default",
},
onRowClick: (row) => {
console.log(row);
if (isSuperAdmin) {
navigate(`/account/team/${row.id}`);
}
+1 -1
View File
@@ -176,7 +176,7 @@ const Register = ({ superAdminExists }) => {
delete toSubmit.confirm;
const action = await dispatch(register(toSubmit));
const action = await dispatch(register({ user: toSubmit, token }));
if (action.payload.success) {
navigate("/uptime");
createToast({
@@ -143,7 +143,6 @@ const UptimeMonitors = () => {
type: ["http", "ping", "docker", "port", "game"],
});
console.log(monitors);
useEffect(() => {
if (isSearching) {
setPage(undefined);
+2 -11
View File
@@ -347,17 +347,8 @@ class NetworkService {
return this.axiosInstance.get(`/monitors/certificate/${config.monitorId}`);
}
/**
* ************************************
* Registers a new user
* ************************************
*
* @async
* @param {Object} form - The form data for the new user to be registered.
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
*/
async registerUser(form) {
return this.axiosInstance.post(`/auth/register`, form);
async registerUser(user, token) {
return this.axiosInstance.post(`/auth/register`, { user, token });
}
/**
+25 -5
View File
@@ -37,9 +37,6 @@ import crypto from "crypto";
import { games, GameDig } from "gamedig";
import jmespath from "jmespath";
import { fileURLToPath } from "url";
import { ObjectId } from "mongodb";
// DB Modules
import { NormalizeData, NormalizeDataUptimeDetails } from "../utils/dataUtils.js";
import { GenerateAvatarImage } from "../utils/imageProcessing.js";
@@ -48,7 +45,7 @@ import { ParseBoolean } from "../utils/utils.js";
// Models
import Monitor from "../db/models/Monitor.js";
import User from "../db/models/User.js";
import InviteToken from "../db/models/InviteToken.js";
import InviteToken from "../db/models/Invite.js";
import StatusPage from "../db/models/StatusPage.js";
import Team from "../db/models/Team.js";
import MaintenanceWindow from "../db/models/MaintenanceWindow.js";
@@ -74,10 +71,18 @@ import {
MongoChecksRepository,
MongoMonitorStatsRepository,
MongoStatusPagesRepository,
MongoUsersRepository,
MongoInvitesRepository,
MongoRecoveryTokensRepository,
MongoSettingsRepository,
IMonitorsRepository,
IChecksRepository,
IMonitorStatsRepository,
IStatusPagesRepository,
IUsersRepository,
IInvitesRepository,
IRecoveryTokensRepository,
ISettingsRepository,
} from "@/repositories/index.js";
export type InitializedSerivces = {
@@ -107,6 +112,10 @@ export type InitializedSerivces = {
checksRepository: IChecksRepository;
monitorStatsRepository: IMonitorStatsRepository;
statusPagesRepository: IStatusPagesRepository;
usersRepository: IUsersRepository;
invitesRepository: IInvitesRepository;
recoveryTokensRepository: IRecoveryTokensRepository;
settingsRepository: ISettingsRepository;
};
export const initializeServices = async ({
@@ -158,6 +167,10 @@ export const initializeServices = async ({
const checksRepository = new MongoChecksRepository(logger);
const monitorStatsRepository = new MongoMonitorStatsRepository();
const statusPagesRepository = new MongoStatusPagesRepository();
const usersRepository = new MongoUsersRepository();
const invitesRepository = new MongoInvitesRepository();
const recoveryTokensRepository = new MongoRecoveryTokensRepository();
const settingsRepository = new MongoSettingsRepository();
const networkService = new NetworkService({
axios,
@@ -231,7 +244,6 @@ export const initializeServices = async ({
// Business services
const userService = new UserService({
crypto,
db,
emailService,
settingsService,
logger,
@@ -240,6 +252,10 @@ export const initializeServices = async ({
errorService,
jobQueue: superSimpleQueue,
monitorsRepository,
usersRepository,
invitesRepository,
recoveryTokensRepository,
settingsRepository,
});
const diagnosticService = new DiagnosticService();
@@ -298,6 +314,10 @@ export const initializeServices = async ({
checksRepository,
monitorStatsRepository,
statusPagesRepository,
usersRepository,
invitesRepository,
recoveryTokensRepository,
settingsRepository,
};
Object.values(services).forEach((service) => {
+11 -7
View File
@@ -32,11 +32,14 @@ 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",
@@ -189,7 +192,8 @@ class AuthController {
editUserById = async (req: Request, res: Response, next: NextFunction) => {
try {
const roles = req?.user?.role;
if (!roles.includes("superadmin")) {
if (!roles || !roles.includes("superadmin")) {
throw new AppError({ message: "Unauthorized", status: 403 });
}
@@ -198,7 +202,7 @@ class AuthController {
await editUserByIdParamValidation.validateAsync(req.params);
// If this is superadmin self edit, allow "superadmin" role
if (userId === req.user._id) {
if (userId === req.user?.id) {
await editSuperadminUserByIdBodyValidation.validateAsync(req.body);
} else {
await editUserByIdBodyValidation.validateAsync(req.body);
@@ -214,7 +218,7 @@ class AuthController {
editUserPasswordById = async (req: Request, res: Response, next: NextFunction) => {
try {
const roles = req?.user?.role;
if (!roles.includes("superadmin")) {
if (!roles || !roles.includes("superadmin")) {
throw new AppError({ message: "Unauthorized", status: 403 });
}
+1 -1
View File
@@ -69,7 +69,7 @@ class IncidentController {
try {
const resolvedIncident = await this.incidentService.resolveIncidentManually({
incidentId: req?.params?.incidentId,
userId: req?.user?._id,
userId: req?.user?.id,
teamId: req?.user?.teamId,
comment: req?.body?.comment,
});
@@ -46,7 +46,7 @@ class MaintenanceWindowController {
try {
await getMaintenanceWindowByIdParamValidation.validateAsync(req.params);
const teamId = req.user.teamId;
const teamId = req.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
@@ -130,7 +130,7 @@ class MaintenanceWindowController {
await editMaintenanceWindowByIdParamValidation.validateAsync(req.params);
await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body);
const teamId = req.user.teamId;
const teamId = req.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
+4 -4
View File
@@ -164,7 +164,7 @@ class MonitorController {
try {
await createMonitorBodyValidation.validateAsync(req.body);
const userId = requireString(req?.user?._id, "User ID");
const userId = requireString(req?.user?.id, "User ID");
const teamId = requireTeamId(req?.user?.teamId);
const monitor = await this.monitorService.createMonitor(teamId, userId, req.body);
@@ -193,7 +193,7 @@ class MonitorController {
throw new AppError({ message: "File is empty", status: 400 });
}
const userId = requireString(req?.user?._id, "User ID");
const userId = requireString(req?.user?.id, "User ID");
const teamId = requireTeamId(req?.user?.teamId);
const fileData = req?.file?.buffer?.toString("utf-8");
@@ -286,9 +286,9 @@ class MonitorController {
addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => {
try {
const _id = requireString(req?.user?._id, "User ID");
const id = requireString(req?.user?.id, "User ID");
const teamId = requireTeamId(req?.user?.teamId);
const demoMonitors = await this.monitorService.addDemoMonitors({ userId: _id, teamId });
const demoMonitors = await this.monitorService.addDemoMonitors({ userId: id, teamId });
return res.status(200).json({
success: true,
@@ -53,7 +53,7 @@ class NotificationController {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const userId = req?.user?._id;
const userId = req?.user?.id;
if (!userId) {
throw new AppError({ message: "User ID is required", status: 400 });
}
@@ -21,11 +21,11 @@ class StatusPageController {
await createStatusPageBodyValidation.validateAsync(req.body);
await imageValidation.validateAsync(req.file);
const { _id, teamId } = req.user;
const { id, teamId } = req.user ?? {};
const statusPage = await this.db.statusPageModule.createStatusPage({
statusPageData: req.body,
image: req.file,
userId: _id,
userId: id,
teamId,
});
return res.status(200).json({
@@ -87,7 +87,7 @@ class StatusPageController {
};
getStatusPagesByTeamId = async (req: Request, res: Response, next: NextFunction) => {
try {
const teamId = req.user.teamId;
const teamId = req.user?.teamId;
const statusPages = await this.db.statusPageModule.getStatusPagesByTeamId(teamId);
return res.status(200).json({
-84
View File
@@ -1,84 +0,0 @@
import mongoose from "mongoose";
const AppSettingsSchema = mongoose.Schema(
{
checkTTL: {
type: Number,
default: 30,
},
language: {
type: String,
default: "gb",
},
pagespeedApiKey: {
type: String,
},
systemEmailHost: {
type: String,
},
systemEmailPort: {
type: Number,
},
systemEmailAddress: {
type: String,
},
systemEmailPassword: {
type: String,
},
systemEmailUser: {
type: String,
},
systemEmailConnectionHost: {
type: String,
default: "localhost",
},
systemEmailTLSServername: {
type: String,
},
systemEmailSecure: {
type: Boolean,
default: false,
},
systemEmailPool: {
type: Boolean,
default: false,
},
systemEmailIgnoreTLS: {
type: Boolean,
default: false,
},
systemEmailRequireTLS: {
type: Boolean,
default: false,
},
systemEmailRejectUnauthorized: {
type: Boolean,
default: true,
},
showURL: {
type: Boolean,
default: false,
},
singleton: {
type: Boolean,
required: true,
unique: true,
default: true,
},
version: {
type: Number,
default: 1,
},
globalThresholds: {
cpu: { type: Number },
memory: { type: Number },
disk: { type: Number },
temperature: { type: Number },
},
},
{
timestamps: true,
}
);
export default mongoose.model("AppSettings", AppSettingsSchema);
+50
View File
@@ -0,0 +1,50 @@
import { Schema, model, type Types } from "mongoose";
import type { Settings, SettingsThresholds } from "@/types/settings.js";
interface AppSettingsDocument extends Omit<Settings, "id" | "createdAt" | "updatedAt"> {
_id: Types.ObjectId;
createdAt: Date;
updatedAt: Date;
}
const thresholdsSchema = new Schema<SettingsThresholds>(
{
cpu: { type: Number },
memory: { type: Number },
disk: { type: Number },
temperature: { type: Number },
},
{ _id: false }
);
const AppSettingsSchema = new Schema<AppSettingsDocument>(
{
checkTTL: { type: Number, default: 30 },
language: { type: String, default: "gb" },
jwtSecret: { type: String },
pagespeedApiKey: { type: String },
systemEmailHost: { type: String },
systemEmailPort: { type: Number },
systemEmailAddress: { type: String },
systemEmailPassword: { type: String },
systemEmailUser: { type: String },
systemEmailConnectionHost: { type: String, default: "localhost" },
systemEmailTLSServername: { type: String },
systemEmailSecure: { type: Boolean, default: false },
systemEmailPool: { type: Boolean, default: false },
systemEmailIgnoreTLS: { type: Boolean, default: false },
systemEmailRequireTLS: { type: Boolean, default: false },
systemEmailRejectUnauthorized: { type: Boolean, default: true },
showURL: { type: Boolean, default: false },
singleton: { type: Boolean, required: true, unique: true, default: true },
version: { type: Number, default: 1 },
globalThresholds: { type: thresholdsSchema },
},
{ timestamps: true }
);
const AppSettingsModel = model<AppSettingsDocument>("AppSettings", AppSettingsSchema);
export type { AppSettingsDocument };
export { AppSettingsModel };
export default AppSettingsModel;
+30
View File
@@ -0,0 +1,30 @@
import { Schema, model, type Types } from "mongoose";
import type { Invite } from "@/types/invite.js";
type InviteDocumentBase = Omit<Invite, "id" | "teamId" | "createdAt" | "updatedAt" | "expiry"> & {
teamId: Types.ObjectId;
expiry: Date;
};
interface InviteDocument extends InviteDocumentBase {
_id: Types.ObjectId;
createdAt: Date;
updatedAt: Date;
}
const InviteSchema = new Schema<InviteDocument>(
{
email: { type: String, required: true, unique: true },
teamId: { type: Schema.Types.ObjectId, ref: "Team", immutable: true, required: true },
role: { type: [String], required: true, default: ["user"] },
token: { type: String, required: true },
expiry: { type: Date, default: Date.now, expires: 3600 },
},
{ timestamps: true }
);
const InviteModel = model<InviteDocument>("Invite", InviteSchema);
export type { InviteDocument };
export { InviteModel };
export default InviteModel;
-35
View File
@@ -1,35 +0,0 @@
import mongoose from "mongoose";
const InviteTokenSchema = mongoose.Schema(
{
email: {
type: String,
required: true,
unique: true,
},
teamId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Team",
immutable: true,
required: true,
},
role: {
type: Array,
required: true,
default: ["user"],
},
token: {
type: String,
required: true,
},
expiry: {
type: Date,
default: Date.now,
expires: 3600,
},
},
{
timestamps: true,
}
);
export default mongoose.model("InviteToken", InviteTokenSchema);
-25
View File
@@ -1,25 +0,0 @@
import mongoose from "mongoose";
const RecoveryTokenSchema = mongoose.Schema(
{
email: {
type: String,
required: true,
unique: true,
},
token: {
type: String,
required: true,
},
expiry: {
type: Date,
default: Date.now,
expires: 600,
},
},
{
timestamps: true,
}
);
export default mongoose.model("RecoveryToken", RecoveryTokenSchema);
+27
View File
@@ -0,0 +1,27 @@
import { Schema, model, type Types } from "mongoose";
import type { RecoveryToken as RecoveryTokenEntity } from "@/types/recoveryToken.js";
type RecoveryTokenDocumentBase = Omit<RecoveryTokenEntity, "id" | "createdAt" | "updatedAt" | "expiry"> & {
expiry: Date;
};
interface RecoveryTokenDocument extends RecoveryTokenDocumentBase {
_id: Types.ObjectId;
createdAt: Date;
updatedAt: Date;
}
const RecoveryTokenSchema = new Schema<RecoveryTokenDocument>(
{
email: { type: String, required: true, unique: true },
token: { type: String, required: true },
expiry: { type: Date, default: Date.now, expires: 600 },
},
{ timestamps: true }
);
const RecoveryTokenModel = model<RecoveryTokenDocument>("RecoveryToken", RecoveryTokenSchema);
export type { RecoveryTokenDocument };
export { RecoveryTokenModel };
export default RecoveryTokenModel;
-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;
+12
View File
@@ -9,3 +9,15 @@ 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";
export * from "@/db/models/Invite.js";
export { default as InviteModel } from "@/db/models/Invite.js";
export * from "@/db/models/AppSettings.js";
export { default as AppSettingsModel } from "@/db/models/AppSettings.js";
export * from "@/db/models/RecoveryToken.js";
export { default as RecoveryTokenModel } from "@/db/models/RecoveryToken.js";
@@ -1,3 +1,5 @@
import { NextFunction, Request, Response } from "express";
import jwt from "jsonwebtoken";
import ServiceRegistry from "../../service/system/serviceRegistry.js";
import SettingsService from "../../service/system/settingsService.js";
@@ -5,20 +7,12 @@ import StringService from "../../service/system/stringService.js";
const SERVICE_NAME = "verifyJWT";
const TOKEN_PREFIX = "Bearer ";
/**
* Verifies the JWT token
* @function
* @param {express.Request} req
* @param {express.Response} res
* @param {express.NextFunction} next
* @returns {express.Response}
*/
const verifyJWT = (req, res, next) => {
const verifyJWT = (req: Request, res: Response, next: NextFunction) => {
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
const token = req.headers["authorization"];
// Make sure a token is provided
if (!token) {
const error = new Error(stringService.noAuthToken);
const error: any = new Error(stringService.noAuthToken);
error.status = 401;
error.service = SERVICE_NAME;
next(error);
@@ -26,7 +20,7 @@ const verifyJWT = (req, res, next) => {
}
// Make sure it is properly formatted
if (!token.startsWith(TOKEN_PREFIX)) {
const error = new Error(stringService.invalidAuthToken); // Instantiate a new Error object for improperly formatted token
const error: any = new Error(stringService.invalidAuthToken); // Instantiate a new Error object for improperly formatted token
error.status = 401;
error.service = SERVICE_NAME;
error.method = "verifyJWT";
@@ -37,7 +31,7 @@ const verifyJWT = (req, res, next) => {
const parsedToken = token.slice(TOKEN_PREFIX.length, token.length);
// Verify the token's authenticity
const { jwtSecret } = ServiceRegistry.get(SettingsService.SERVICE_NAME).getSettings();
jwt.verify(parsedToken, jwtSecret, (err, decoded) => {
jwt.verify(parsedToken, jwtSecret, (err: any, decoded: any) => {
if (err) {
const errorMessage = err.name === "TokenExpiredError" ? stringService.expiredAuthToken : stringService.invalidAuthToken;
err.details = { msg: errorMessage };
+12
View File
@@ -9,3 +9,15 @@ 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/IUsersRepository.js";
export { default as MongoUsersRepository } from "@/repositories/users/MongoUsersRepository.js";
export * from "@/repositories/invites/IInvitesRepository.js";
export { default as MongoInvitesRepository } from "@/repositories/invites/MongoInviteRepository.js";
export * from "@/repositories/recovery-tokens/IRecoveryTokensRepository.js";
export { default as MongoRecoveryTokensRepository } from "@/repositories/recovery-tokens/MongoRecoveryTokensRepository.js";
export * from "@/repositories/settings/ISettingsRepository.js";
export { default as MongoSettingsRepository } from "@/repositories/settings/MongoSettingsRepository.js";
@@ -0,0 +1,11 @@
import type { Invite } from "@/types/index.js";
export interface IInvitesRepository {
// create
// fetch
findByTokenAndDelete(token: string): Promise<Invite>;
// update
// delete
// other
}
@@ -0,0 +1,45 @@
import { IInvitesRepository } from "@/repositories/index.js";
import type { Invite } from "@/types/index.js";
import { type InviteDocument, InviteModel } from "@/db/models/index.js";
import { AppError } from "@/utils/AppError.js";
import mongoose from "mongoose";
class MongoInvitesRepository implements IInvitesRepository {
private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => {
if (!value) {
return "";
}
return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value);
};
private toDateString = (value?: Date | string | null): string => {
if (!value) {
return new Date(0).toISOString();
}
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
};
protected toEntity = (doc: InviteDocument): Invite => {
return {
id: this.toStringId(doc._id),
email: doc.email,
teamId: this.toStringId(doc.teamId),
role: doc.role ?? [],
token: doc.token,
expiry: this.toDateString(doc.expiry),
createdAt: this.toDateString(doc.createdAt),
updatedAt: this.toDateString(doc.updatedAt),
};
};
findByTokenAndDelete = async (token: string) => {
const invite = await InviteModel.findOneAndDelete({
token,
});
if (invite === null) {
throw new AppError({ message: "Invite not found", status: 404 });
}
return this.toEntity(invite);
};
}
export default MongoInvitesRepository;
@@ -0,0 +1,12 @@
import type { RecoveryToken } from "@/types/recoveryToken.js";
export interface IRecoveryTokensRepository {
// create
create(email: string): Promise<RecoveryToken>;
// fetch
findByToken(token: string): Promise<RecoveryToken>;
// update
// delete
deleteManyByEmail(email: string): Promise<number>;
// other
}
@@ -0,0 +1,61 @@
import type { RecoveryToken } from "@/types/index.js";
import type { IRecoveryTokensRepository } from "./IRecoveryTokensRepository.js";
import type { RecoveryTokenDocument } from "@/db/models/RecoveryToken.js";
import { RecoveryTokenModel } from "@/db/models/RecoveryToken.js";
import mongoose from "mongoose";
import crypto from "crypto";
import { AppError } from "@/utils/AppError.js";
const SERVICE_NAME = "MongoRecoveryTokensRepository";
class MongoRecoveryTokensRepository implements IRecoveryTokensRepository {
static SERVICE_NAME = SERVICE_NAME;
private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => {
if (!value) {
return "";
}
return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value);
};
private toDateString = (value?: Date | string | null): string => {
if (!value) {
return new Date(0).toISOString();
}
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
};
protected toEntity = (doc: RecoveryTokenDocument): RecoveryToken => {
return {
id: this.toStringId(doc._id),
email: doc.email,
token: doc.token,
expiry: this.toDateString(doc.expiry),
createdAt: this.toDateString(doc.createdAt),
updatedAt: this.toDateString(doc.updatedAt),
};
};
create = async (email: string): Promise<RecoveryToken> => {
const token = await RecoveryTokenModel.create({
email,
token: crypto.randomBytes(32).toString("hex"),
});
return this.toEntity(token);
};
findByToken = async (token: string): Promise<RecoveryToken> => {
const recoveryToken = await RecoveryTokenModel.findOne({ token });
if (!recoveryToken) {
throw new AppError({ message: "Recovery token not found", service: SERVICE_NAME, status: 404 });
}
return this.toEntity(recoveryToken);
};
deleteManyByEmail = async (email: string) => {
const result = await RecoveryTokenModel.deleteMany({
email,
});
return Promise.resolve(result.deletedCount || 0);
};
}
export default MongoRecoveryTokensRepository;
@@ -0,0 +1,8 @@
import type { Settings } from "@/types/index.js";
export interface ISettingsRepository {
// create
// fetch
// update
update(settings: Partial<Settings>): Promise<Settings>;
// delete
}
@@ -0,0 +1,76 @@
import mongoose from "mongoose";
import { ISettingsRepository } from "@/repositories/settings/ISettingsRepository.js";
import type { Settings } from "@/types/index.js";
import { AppSettingsModel, type AppSettingsDocument } from "@/db/models/AppSettings.js";
class MongoSettingsRepository implements ISettingsRepository {
private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => {
if (!value) {
return "";
}
return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value);
};
private toDateString = (value?: Date | string | null): string => {
if (!value) {
return new Date(0).toISOString();
}
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
};
protected toEntity = (doc: AppSettingsDocument): Settings => {
return {
id: this.toStringId(doc._id),
checkTTL: doc.checkTTL,
language: doc.language,
jwtSecret: doc.jwtSecret ?? undefined,
pagespeedApiKey: doc.pagespeedApiKey ?? undefined,
systemEmailHost: doc.systemEmailHost ?? undefined,
systemEmailPort: doc.systemEmailPort ?? undefined,
systemEmailAddress: doc.systemEmailAddress ?? undefined,
systemEmailPassword: doc.systemEmailPassword ?? undefined,
systemEmailUser: doc.systemEmailUser ?? undefined,
systemEmailConnectionHost: doc.systemEmailConnectionHost ?? undefined,
systemEmailTLSServername: doc.systemEmailTLSServername ?? undefined,
systemEmailSecure: doc.systemEmailSecure ?? false,
systemEmailPool: doc.systemEmailPool ?? false,
systemEmailIgnoreTLS: doc.systemEmailIgnoreTLS ?? false,
systemEmailRequireTLS: doc.systemEmailRequireTLS ?? false,
systemEmailRejectUnauthorized: doc.systemEmailRejectUnauthorized ?? true,
showURL: doc.showURL ?? false,
singleton: doc.singleton,
version: doc.version ?? 1,
globalThresholds: doc.globalThresholds ?? undefined,
createdAt: this.toDateString(doc.createdAt),
updatedAt: this.toDateString(doc.updatedAt),
};
};
update = async (settings: Partial<Settings>) => {
const update: Record<string, any> = { $set: { ...settings } };
if (settings.pagespeedApiKey === "") {
update.$unset = { pagespeedApiKey: "" };
delete update.$set.pagespeedApiKey;
}
if (settings.systemEmailPassword === "") {
update.$unset = { systemEmailPassword: "" };
delete update.$set.systemEmailPassword;
}
await AppSettingsModel.findOneAndUpdate({}, update, {
upsert: true,
});
const updatedSettings = await AppSettingsModel.findOneAndUpdate({}, update, {
upsert: true,
new: true,
projection: "-__v -_id -createdAt -updatedAt -singleton",
});
return this.toEntity(updatedSettings);
};
}
export default MongoSettingsRepository;
@@ -0,0 +1,15 @@
import type { User } from "@/types/index.js";
export interface IUsersRepository {
// create
create(user: Partial<User>, imageFile?: Express.Multer.File | null): Promise<User>;
// fetch
findByEmail(email: string): Promise<User>;
findById(id: string): Promise<User>;
findAll(): Promise<User[]>;
// update
updateById(id: string, patch: Partial<User>, file?: Express.Multer.File | null): Promise<User>;
// delete
deleteById(id: string): Promise<User>;
// other
findSuperAdmin(): Promise<boolean>;
}
@@ -0,0 +1,154 @@
import mongoose from "mongoose";
import { IUsersRepository } from "@/repositories/index.js";
import { UserModel, type UserDocument } from "@/db/models/index.js";
import type { User, UserProfileImage } from "@/types/index.js";
import { GenerateAvatarImage } from "@/utils/imageProcessing.js";
import { ParseBoolean } from "@/utils/utils.js";
import { AppError } from "@/utils/AppError.js";
const SERVICE_NAME = "MongoUsersRepository";
class MongoUsersRepository implements IUsersRepository {
static SERVICE_NAME = SERVICE_NAME;
private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => {
if (!value) {
return "";
}
return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value);
};
private toDateString = (value?: Date | string | null): string => {
if (!value) {
return new Date(0).toISOString();
}
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
};
private mapProfileImage = (image?: (UserProfileImage & { data?: Buffer }) | null) => {
if (!image) {
return undefined;
}
return {
data: image.data,
contentType: image.contentType,
};
};
protected toEntity = (doc: UserDocument): User => {
return {
id: this.toStringId(doc._id),
firstName: doc.firstName,
lastName: doc.lastName,
email: doc.email,
password: doc.password,
avatarImage: doc.avatarImage ?? undefined,
profileImage: this.mapProfileImage(doc.profileImage),
isActive: doc.isActive ?? false,
isVerified: doc.isVerified ?? false,
role: doc.role ?? [],
teamId: this.toStringId(doc.teamId),
checkTTL: doc.checkTTL ?? undefined,
createdAt: this.toDateString(doc.createdAt),
updatedAt: this.toDateString(doc.updatedAt),
};
};
create = async (user: Partial<User>, imageFile: Express.Multer.File | null) => {
if (imageFile) {
// 1. Save the full size image
user.profileImage = {
data: imageFile.buffer,
contentType: imageFile.mimetype,
};
// 2. Get the avatar sized image
const avatar = await GenerateAvatarImage(imageFile);
user.avatarImage = avatar;
}
const newUser = new UserModel(user);
await newUser.save();
const sanitizedUser = await UserModel.findOne({ _id: newUser._id }).select("-password").select("-profileImage");
if (!sanitizedUser) {
throw new AppError({ message: "Failed to create user", service: SERVICE_NAME, status: 500 });
}
return this.toEntity(sanitizedUser);
};
findByEmail = async (email: string) => {
const user = await UserModel.findOne({ email: email }).select("-profileImage");
if (!user) {
throw new AppError({ message: "User not found", service: SERVICE_NAME, status: 404 });
}
return this.toEntity(user);
};
findById = async (id: string) => {
const user = await UserModel.findById(id).select("-password").select("-profileImage");
if (!user) {
throw new Error("User not found");
}
return this.toEntity(user);
};
findAll = async () => {
const users = await UserModel.find().select("-password").select("-profileImage");
return this.mapDocuments(users);
};
updateById = async (id: string, patch: Partial<User & { deleteProfileImage?: boolean }>, file?: Express.Multer.File | null): Promise<User> => {
const candidateUser = { ...patch };
if (ParseBoolean(candidateUser.deleteProfileImage) === true) {
candidateUser.profileImage = undefined;
candidateUser.avatarImage = undefined;
} else if (file) {
// 1. Save the full size image
candidateUser.profileImage = {
data: file.buffer,
contentType: file.mimetype,
};
// 2. Get the avatar sized image
const avatar = await GenerateAvatarImage(file);
candidateUser.avatarImage = avatar;
}
const updatedUser = await UserModel.findOneAndUpdate(
{ _id: id },
candidateUser,
{ new: true } // Returns updated user instead of pre-update user
)
.select("-password")
.select("-profileImage");
if (!updatedUser) {
throw new AppError({ message: "User not found", service: SERVICE_NAME, status: 404 });
}
return this.toEntity(updatedUser);
};
deleteById = async (id: string) => {
const deletedUser = await UserModel.findByIdAndDelete(id);
if (!deletedUser) {
throw new AppError({ message: "User not found", service: SERVICE_NAME, status: 404 });
}
return this.toEntity(deletedUser);
};
findSuperAdmin = async () => {
const superAdmin = await UserModel.findOne({ role: "superadmin" });
if (superAdmin !== null) {
return true;
}
return false;
};
private mapDocuments = (documents: UserDocument[]): User[] => {
if (!documents?.length) {
return [];
}
return documents.map((doc) => this.toEntity(doc));
};
}
export default MongoUsersRepository;
+86 -45
View File
@@ -1,11 +1,14 @@
import { IMonitorsRepository } from "@/repositories/index.js";
import { IInvitesRepository, IMonitorsRepository, IRecoveryTokensRepository, IUsersRepository, ISettingsRepository } from "@/repositories/index.js";
import Team from "@/db/models/Team.js";
import type { User } from "@/types/index.js";
import bcrypt from "bcryptjs";
import { AppError } from "@/utils/AppError.js";
const SERVICE_NAME = "userService";
class UserService {
static SERVICE_NAME = SERVICE_NAME;
private db: any;
private emailService: any;
private settingsService: any;
private logger: any;
@@ -15,10 +18,13 @@ class UserService {
private jobQueue: any;
private crypto: any;
private monitorsRepository: IMonitorsRepository;
private usersRepository: IUsersRepository;
private invitesRepository: IInvitesRepository;
private recoveryTokensRepository: IRecoveryTokensRepository;
private settingsRepository: ISettingsRepository;
constructor({
crypto,
db,
emailService,
settingsService,
logger,
@@ -27,9 +33,12 @@ class UserService {
errorService,
jobQueue,
monitorsRepository,
usersRepository,
invitesRepository,
recoveryTokensRepository,
settingsRepository,
}: {
crypto: any;
db: any;
emailService: any;
settingsService: any;
logger: any;
@@ -38,8 +47,11 @@ class UserService {
errorService: any;
jobQueue: any;
monitorsRepository: IMonitorsRepository;
usersRepository: IUsersRepository;
invitesRepository: IInvitesRepository;
recoveryTokensRepository: IRecoveryTokensRepository;
settingsRepository: ISettingsRepository;
}) {
this.db = db;
this.emailService = emailService;
this.settingsService = settingsService;
this.logger = logger;
@@ -49,6 +61,10 @@ class UserService {
this.jobQueue = jobQueue;
this.crypto = crypto;
this.monitorsRepository = monitorsRepository;
this.usersRepository = usersRepository;
this.invitesRepository = invitesRepository;
this.recoveryTokensRepository = recoveryTokensRepository;
this.settingsRepository = settingsRepository;
}
get serviceName() {
@@ -62,36 +78,40 @@ 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();
const superAdminExists = await this.usersRepository.findSuperAdmin();
if (superAdminExists) {
const invitedUser = await this.db.inviteModule.getInviteTokenAndDelete(user.inviteToken);
user.role = invitedUser.role;
user.teamId = invitedUser.teamId;
const invite = await this.invitesRepository.findByTokenAndDelete(inviteToken);
user.role = invite.role;
user.teamId = invite.teamId;
} else {
// This is the first account, create JWT secret to use if one is not supplied by env
const jwtSecret = this.crypto.randomBytes(64).toString("hex");
await this.db.settingsModule.updateAppSettings({ jwtSecret });
await this.settingsRepository.update({ jwtSecret });
// Create a new team
const team = new Team({
email: user.email,
});
user.teamId = team._id;
}
const newUser = await this.db.userModule.insertUser({ ...user }, file);
const newUser = await this.usersRepository.create({ ...user }, file);
this.logger.debug({
message: "New user created",
service: SERVICE_NAME,
method: "registerUser",
details: newUser._id,
details: newUser.id,
});
const userForToken = { ...newUser._doc };
delete userForToken.profileImage;
delete userForToken.avatarImage;
delete newUser.profileImage;
delete newUser.avatarImage;
const appSettings = await this.settingsService.getSettings();
const token = this.issueToken(userForToken, appSettings);
const token = this.issueToken(newUser, appSettings);
try {
const html = await this.emailService.buildEmail("welcomeEmailTemplate", {
@@ -119,17 +139,18 @@ class UserService {
loginUser = async (email: string, password: string) => {
// Check if user exists
const user = await this.db.userModule.getUserByEmail(email);
const user = await this.usersRepository.findByEmail(email);
// Compare password
const match = await user.comparePassword(password);
const match = await bcrypt.compare(password, user.password);
if (match !== true) {
throw this.errorService.createAuthenticationError(this.stringService.authIncorrectPassword);
}
// Remove password from user object. Should this be abstracted to DB layer?
const userWithoutPassword = { ...user._doc };
delete userWithoutPassword.password;
delete userWithoutPassword.avatarImage;
const userWithoutPassword = { ...user };
userWithoutPassword.password = "";
userWithoutPassword.avatarImage = "";
// Happy path, return token
const appSettings = await this.settingsService.getSettings();
@@ -139,16 +160,16 @@ class UserService {
return { user: userWithoutPassword, token };
};
editUser = async (updates: any, file: any, currentUser: any) => {
editUser = async (updates: Partial<User & { newPassword?: string }>, file: any, currentUser: any) => {
// Change Password check
if (updates?.password && updates?.newPassword) {
// Get user's email
// Add user email to body for DB operation
updates.email = currentUser.email;
// Get user
const user = await this.db.userModule.getUserByEmail(currentUser.email);
const user = await this.usersRepository.findByEmail(currentUser.email);
// Compare passwords
const match = await user.comparePassword(updates?.password);
const match = await bcrypt.compare(updates?.password, user.password);
// If not a match, throw a 403
// 403 instead of 401 to avoid triggering axios interceptor
if (!match) {
@@ -158,18 +179,19 @@ class UserService {
updates.password = updates.newPassword;
}
const updatedUser = await this.db.userModule.updateUser({ userId: currentUser?._id, user: updates, file: file });
return updatedUser;
return await this.usersRepository.updateById(currentUser.id, updates, file);
};
checkSuperadminExists = async () => {
const superAdminExists = await this.db.userModule.checkSuperadmin();
return superAdminExists;
return await this.usersRepository.findSuperAdmin();
};
requestRecovery = async (email: string) => {
const user = await this.db.userModule.getUserByEmail(email);
const recoveryToken = await this.db.recoveryModule.requestRecoveryToken(email);
const user = await this.usersRepository.findByEmail(email);
// Delete existing tokens
await this.recoveryTokensRepository.deleteManyByEmail(email);
const recoveryToken = await this.recoveryTokensRepository.create(email);
const name = user.firstName;
const { clientHost } = this.settingsService.getSettings();
const url = `${clientHost}/set-new-password/${recoveryToken.token}`;
@@ -184,24 +206,39 @@ class UserService {
};
validateRecovery = async (recoveryToken: string) => {
await this.db.recoveryModule.validateRecoveryToken(recoveryToken);
// Throws if token not found, validating
await this.recoveryTokensRepository.findByToken(recoveryToken);
};
resetPassword = async (password: string, recoveryToken: string) => {
const user = await this.db.recoveryModule.resetPassword(password, recoveryToken);
const appSettings = await this.settingsService.getSettings();
const token = this.issueToken(user._doc, appSettings);
return { user, token };
const existingToken = await this.recoveryTokensRepository.findByToken(recoveryToken);
const existingUser = await this.usersRepository.findByEmail(existingToken.email);
const match = await bcrypt.compare(password, existingUser.password);
if (match === true) {
throw new AppError({ message: "New password cannot be same as old password", service: SERVICE_NAME, status: 400 });
}
existingUser.password = password;
await this.usersRepository.updateById(existingUser.id, existingUser, null);
await this.recoveryTokensRepository.deleteManyByEmail(existingUser.email);
existingUser.password = "";
existingUser.profileImage = undefined;
const token = this.issueToken(existingUser, await this.settingsService.getSettings());
return { user: existingUser, token };
};
deleteUser = async (user: any) => {
deleteUser = async (user: User) => {
const email = user?.email;
if (!email) {
throw this.errorService.createBadRequestError("No email in request");
}
const teamId = user?.teamId;
const userId = user?._id;
const userId = user?.id;
if (!teamId) {
throw this.errorService.createBadRequestError("No team ID in request");
@@ -230,24 +267,28 @@ class UserService {
));
}
// 6. Delete the user by id
await this.db.userModule.deleteUser(userId);
await this.usersRepository.deleteById(userId);
};
getAllUsers = async () => {
const users = await this.db.userModule.getAllUsers();
return users;
return await this.usersRepository.findAll();
};
getUserById = async (roles: any, userId: any) => {
const user = await this.db.userModule.getUserById(roles, userId);
if (!roles.includes("superadmin")) {
throw new AppError({ message: "User is not a superadmin", service: SERVICE_NAME, status: 403 });
}
const user = await this.usersRepository.findById(userId);
return user;
};
editUserById = async (userId: any, user: any) => {
await this.db.userModule.editUserById(userId, user);
editUserById = async (userId: any, patch: Partial<User>) => {
await this.usersRepository.updateById(userId, patch, null);
};
setPasswordByUserId = async (userId: any, password: string) => {
const updatedUser = await this.db.userModule.updateUser({ userId: userId, user: { password: password }, file: null });
const updatedUser = await this.usersRepository.updateById(userId, { password }, null);
return updatedUser;
};
}
@@ -46,7 +46,7 @@ class EmailService {
*/
this.loadTemplate = (templateName) => {
try {
const templatePath = this.path.join(__dirname, `../../../templates/${templateName}.mjml`);
const templatePath = this.path.join(__dirname, `../../templates/${templateName}.mjml`);
const templateContent = this.fs.readFileSync(templatePath, "utf8");
return this.compile(templateContent);
} catch (error) {
+2 -2
View File
@@ -1,10 +1,10 @@
import { ITokenizedUser } from "../db/models/index.ts";
import type { User } from "@/types/index.js";
declare global {
namespace Express {
interface Request {
file?: Multer.File;
user?: ITokenizedUser;
user?: User | undefined;
resource?: any;
}
}
+4
View File
@@ -3,3 +3,7 @@ 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";
export * from "@/types/invite.js";
export * from "@/types/recoveryToken.js";
export * from "@/types/settings.js";
+12
View File
@@ -0,0 +1,12 @@
import type { UserRole } from "@/types/user.js";
export interface Invite {
id: string;
email: string;
teamId: string;
role: UserRole[];
token: string;
expiry: string;
createdAt: string;
updatedAt: string;
}
+8
View File
@@ -0,0 +1,8 @@
export interface RecoveryToken {
id: string;
email: string;
token: string;
expiry: string;
createdAt: string;
updatedAt: string;
}
+32
View File
@@ -0,0 +1,32 @@
export interface SettingsThresholds {
cpu?: number;
memory?: number;
disk?: number;
temperature?: number;
}
export interface Settings {
id: string;
checkTTL: number;
language: string;
jwtSecret?: string;
pagespeedApiKey?: string;
systemEmailHost?: string;
systemEmailPort?: number;
systemEmailAddress?: string;
systemEmailPassword?: string;
systemEmailUser?: string;
systemEmailConnectionHost?: string;
systemEmailTLSServername?: string;
systemEmailSecure: boolean;
systemEmailPool: boolean;
systemEmailIgnoreTLS: boolean;
systemEmailRequireTLS: boolean;
systemEmailRejectUnauthorized: boolean;
showURL: boolean;
singleton: boolean;
version: number;
globalThresholds?: SettingsThresholds;
createdAt: string;
updatedAt: string;
}
+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;
}
-5
View File
@@ -1,8 +1,3 @@
/**
* Converts a request body parameter to a boolean.
* @param {string | boolean} value
* @returns {boolean}
*/
const ParseBoolean = (value) => {
if (value === true || value === "true") {
return true;