Merge pull request #3149 from bluewave-labs/feat/user-service-ts
feat/user service ts
This commit is contained in:
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();
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
Executable
+30
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
@@ -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) {
|
||||
|
||||
Vendored
+2
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface RecoveryToken {
|
||||
id: string;
|
||||
email: string;
|
||||
token: string;
|
||||
expiry: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user