Revert "Merge pull request #2973 from bluewave-labs/feat/v2/auth"

This reverts commit 72882fded9, reversing
changes made to bfb299cfca.
This commit is contained in:
Alex Holliday
2025-09-23 14:05:23 -07:00
parent 003d419a64
commit 990aed9f2e
25 changed files with 69 additions and 894 deletions

View File

@@ -1,5 +1,5 @@
{
"ignore": ["src/locales/*", "*.log", "node_modules/*"],
"watch": ["src/**/*.js", "src/**/*.ts", "*.json"],
"ext": "ts,js,json"
"watch": ["src/**/*.js", "*.json"],
"ext": "js,json"
}

117
server/package-lock.json generated
View File

@@ -45,8 +45,6 @@
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/express": "5.0.3",
"@types/jsonwebtoken": "9.0.10",
"c8": "10.1.3",
"chai": "5.2.0",
"eslint": "^9.17.0",
@@ -1680,27 +1678,6 @@
"node": ">=10.13.0"
}
},
"node_modules/@types/body-parser": {
"version": "1.19.6",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
"integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/connect": "*",
"@types/node": "*"
}
},
"node_modules/@types/connect": {
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1708,44 +1685,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/express": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz",
"integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^5.0.0",
"@types/serve-static": "*"
}
},
"node_modules/@types/express-serve-static-core": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz",
"integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*",
"@types/send": "*"
}
},
"node_modules/@types/http-cache-semantics": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
"integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
"license": "MIT"
},
"node_modules/@types/http-errors": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
"integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
@@ -1760,31 +1705,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/jsonwebtoken": {
"version": "9.0.10",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/ms": "*",
"@types/node": "*"
}
},
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.3.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
@@ -1794,49 +1714,12 @@
"undici-types": "~7.10.0"
}
},
"node_modules/@types/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/relateurl": {
"version": "0.2.33",
"resolved": "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.33.tgz",
"integrity": "sha512-bTQCKsVbIdzLqZhLkF5fcJQreE4y1ro4DIyVrlDNSCJRRwHhB8Z+4zXXa8jN6eDvc2HbRsEYgbvrnGvi54EpSw==",
"license": "MIT"
},
"node_modules/@types/send": {
"version": "0.17.5",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz",
"integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/mime": "^1",
"@types/node": "*"
}
},
"node_modules/@types/serve-static": {
"version": "1.15.8",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz",
"integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/http-errors": "*",
"@types/node": "*",
"@types/send": "*"
}
},
"node_modules/@types/triple-beam": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",

View File

@@ -2,7 +2,7 @@
"name": "server",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"main": "index.js",
"type": "module",
"scripts": {
"test": "c8 mocha",
@@ -54,8 +54,6 @@
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/express": "5.0.3",
"@types/jsonwebtoken": "9.0.10",
"c8": "10.1.3",
"chai": "5.2.0",
"eslint": "^9.17.0",

View File

@@ -15,9 +15,6 @@ import StatusPageController from "../controllers/v1/statusPageController.js";
import NotificationController from "../controllers/v1/notificationController.js";
import DiagnosticController from "../controllers/v1/diagnosticController.js";
// V2
import AuthV2Controller from "../controllers/v2/AuthController.js";
export const initializeControllers = (services) => {
const controllers = {};
const commonDependencies = createCommonDependencies(services.db, services.errorService, services.logger, services.stringService);
@@ -65,8 +62,5 @@ export const initializeControllers = (services) => {
diagnosticService: services.diagnosticService,
});
// V2
controllers.authV2Controller = new AuthV2Controller(services.authV2Service, services.inviteV2Service);
return controllers;
};

View File

@@ -13,8 +13,6 @@ import LogRoutes from "../routes/v1/logRoutes.js";
import DiagnosticRoutes from "../routes/v1//diagnosticRoute.js";
import NotificationRoutes from "../routes/v1/notificationRoute.js";
import authV2 from "../routes/v2/auth.js";
export const setupRoutes = (app, controllers) => {
const authRoutes = new AuthRoutes(controllers.authController);
const monitorRoutes = new MonitorRoutes(controllers.monitorController);
@@ -39,12 +37,4 @@ export const setupRoutes = (app, controllers) => {
app.use("/api/v1/status-page", statusPageRoutes.getRouter());
app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter());
app.use("/api/v1/diagnostic", verifyJWT, diagnosticRoutes.getRouter());
// *******************
// V2 Routes
// *******************
const v2AuthRoutes = new authV2(controllers.authV2Controller);
app.use("/api/v2/auth", authApiLimiter, v2AuthRoutes.getRouter());
};

View File

@@ -48,8 +48,8 @@ import { ParseBoolean } from "../utils/utils.js";
// Models
import Check from "../db/models/Check.js";
import Monitor from "../db/models/Monitor.js";
import { User } from "../db/models/auth/User.js";
import { Invite } from "../db/models/auth/Invite.js";
import User from "../db/models/User.js";
import InviteToken from "../db/models/InviteToken.js";
import StatusPage from "../db/models/StatusPage.js";
import Team from "../db/models/Team.js";
import MaintenanceWindow from "../db/models/MaintenanceWindow.js";
@@ -68,10 +68,6 @@ import NotificationModule from "../db/mongo/modules/notificationModule.js";
import RecoveryModule from "../db/mongo/modules/recoveryModule.js";
import SettingsModule from "../db/mongo/modules/settingsModule.js";
// v2
import AuthV2Service from "../service/v2/business/AuthService.js";
import InviteV2Service from "../service/v2/business/InviteService.js";
export const initializeServices = async ({ logger, envSettings, settingsService }) => {
const serviceRegistry = new ServiceRegistry({ logger });
ServiceRegistry.instance = serviceRegistry;
@@ -83,7 +79,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService
// Create DB
const checkModule = new CheckModule({ logger, Check, Monitor, User });
const inviteModule = new InviteModule({ Invite, crypto, stringService });
const inviteModule = new InviteModule({ InviteToken, crypto, stringService });
const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService });
const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService });
const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow });
@@ -212,9 +208,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService
games,
});
const authV2Service = new AuthV2Service();
const inviteV2Service = new InviteV2Service();
const services = {
settingsService,
translationService,
@@ -234,8 +227,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService
monitorService,
errorService,
logger,
authV2Service,
inviteV2Service,
};
Object.values(services).forEach((service) => {

View File

@@ -1,142 +0,0 @@
import { Request, Response, NextFunction } from "express";
import { encode, decode } from "../../utils/JWTUtils.js";
import AuthService from "../../service/v2/business/AuthService.js";
import ApiError from "../../utils/ApiError.js";
import InviteService from "../../service/v2/business/InviteService.js";
import { IInvite } from "../../db/models/index.js";
export interface IAuthController {
register(req: Request, res: Response, next: NextFunction): Promise<void>;
registerWithInvite(req: Request, res: Response, next: NextFunction): Promise<void>;
login(req: Request, res: Response, next: NextFunction): Promise<void>;
logout(req: Request, res: Response): void;
me(req: Request, res: Response, next: NextFunction): void;
}
class AuthController implements IAuthController {
private authService: AuthService;
private inviteService: InviteService;
constructor(authService: AuthService, inviteService: InviteService) {
this.authService = authService;
this.inviteService = inviteService;
}
register = async (req: Request, res: Response, next: NextFunction) => {
try {
const { email, firstName, lastName, password } = req.body;
if (!email || !firstName || !lastName || !password) {
throw new Error("Email, firstName, lastName, and password are required");
}
const result = await this.authService.register({
email,
firstName,
lastName,
password,
});
const token = encode(result);
res.cookie("token", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week
});
res.status(201).json({
message: "User created successfully",
});
} catch (error) {
next(error);
}
};
registerWithInvite = async (req: Request, res: Response, next: NextFunction) => {
try {
const token = req.params.token;
if (!token) {
throw new ApiError("Invite token is required", 400);
}
const invite: IInvite = await this.inviteService.get(token);
const { firstName, lastName, password } = req.body;
const email = invite?.email;
const roles = invite?.roles;
if (!email || !firstName || !lastName || !password || !roles || roles.length === 0) {
throw new Error("Email, firstName, lastName, password, and roles are required");
}
const result = await this.authService.registerWithInvite({
email,
firstName,
lastName,
password,
roles,
});
if (!result) {
throw new Error("Registration failed");
}
await this.inviteService.delete(invite._id.toString());
const jwt = encode(result);
res.cookie("token", jwt, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week
});
res.status(201).json({ message: "User created successfully" });
} catch (error) {
next(error);
}
};
login = async (req: Request, res: Response, next: NextFunction) => {
try {
const { email, password } = req.body;
// Validation
if (!email || !password) {
throw new ApiError("Email and password are required", 400);
}
const result = await this.authService.login({ email, password });
const token = encode(result);
res.cookie("token", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week
});
res.status(200).json({
message: "Login successful",
});
} catch (error) {
next(error);
}
};
logout = (req: Request, res: Response) => {
res.clearCookie("token", {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
});
res.status(200).json({ message: "Logout successful" });
};
me = (req: Request, res: Response, next: NextFunction) => {
return res.status(200).json({ message: "OK" });
};
}
export default AuthController;

View File

@@ -0,0 +1,35 @@
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);

View File

@@ -1,47 +1,11 @@
import mongoose, { Schema, Document, Types } from "mongoose";
import mongoose from "mongoose";
import bcrypt from "bcryptjs";
import logger from "../../../utils/logger.js";
import Monitor from "../Monitor.js";
import Team from "../Team.js";
import Notification from "../Notification.js";
import { subscribe } from "diagnostics_channel";
import logger from "../../utils/logger.js";
import Monitor from "./Monitor.js";
import Team from "./Team.js";
import Notification from "./Notification.js";
export const RoleTypes = ["user", "admin", "superadmin", "demo"] as const;
export type RoleType = (typeof RoleTypes)[number];
export interface ITokenizedUser {
sub: string;
roles: string[];
}
export interface IUser extends Document {
// V1
_id: Types.ObjectId;
firstName: string;
lastName: string;
email: string;
password: string;
avatarImage: string;
profileImage: {
data: Buffer;
contentType: string;
};
isActive: boolean;
isVerified: boolean;
role?: RoleType[];
teamId: Types.ObjectId;
checkTTL: number;
createdAt: Date;
updatedAt: Date;
comparePassword: (submittedPassword: string) => Promise<boolean>;
// V2
roles: Types.ObjectId[];
lastLoginAt?: Date;
version: number;
}
const UserSchema = new Schema<IUser>(
const UserSchema = mongoose.Schema(
{
firstName: {
type: String,
@@ -58,7 +22,7 @@ const UserSchema = new Schema<IUser>(
},
password: {
type: String,
required: false,
required: true,
},
avatarImage: {
type: String,
@@ -77,8 +41,8 @@ const UserSchema = new Schema<IUser>(
},
role: {
type: [String],
default: ["user"],
enum: RoleTypes,
default: "user",
enum: ["user", "admin", "superadmin", "demo"],
},
teamId: {
type: mongoose.Schema.Types.ObjectId,
@@ -88,22 +52,6 @@ const UserSchema = new Schema<IUser>(
checkTTL: {
type: Number,
},
// v2
roles: [
{
type: Schema.Types.ObjectId,
ref: "Role",
default: [],
},
],
lastLoginAt: {
type: Date,
},
version: {
type: Number,
default: 1,
},
},
{
timestamps: true,
@@ -121,7 +69,7 @@ UserSchema.pre("save", function (next) {
UserSchema.pre("findOneAndUpdate", function (next) {
const update = this.getUpdate();
if (update && "password" in update) {
if ("password" in update) {
const salt = bcrypt.genSaltSync(10);
update.password = bcrypt.hashSync(update.password, salt);
}
@@ -144,13 +92,15 @@ UserSchema.pre("findOneAndDelete", async function (next) {
}
next();
} catch (error) {
next(error as Error);
next(error);
}
});
UserSchema.methods.comparePassword = async function (submittedPassword: string) {
UserSchema.methods.comparePassword = async function (submittedPassword) {
const res = await bcrypt.compare(submittedPassword, this.password);
return res;
};
export const User = mongoose.model<IUser>("User", UserSchema);
const User = mongoose.model("User", UserSchema);
export default User;

View File

@@ -1,68 +0,0 @@
import mongoose, { Schema, Document, Types } from "mongoose";
export interface IInvite extends Document {
// v1
_id: Types.ObjectId;
email: string;
teamId: Types.ObjectId;
role: string[];
token: string;
expiry: Date;
createdAt: Date;
updatedAt: Date;
// v2
tokenHash: string;
roles: Types.ObjectId[];
createdBy: Types.ObjectId;
updatedBy: Types.ObjectId;
}
const InviteSchema = new Schema<IInvite>(
{
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true,
},
teamId: {
type: mongoose.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,
},
// v2
roles: [
{
type: Schema.Types.ObjectId,
ref: "Role",
required: true,
},
],
tokenHash: { type: String, required: true, unique: true },
createdBy: { type: Schema.Types.ObjectId, ref: "User", required: true },
updatedBy: { type: Schema.Types.ObjectId, ref: "User", required: true },
},
{
timestamps: true,
}
);
export const Invite = mongoose.model<IInvite>("Invite", InviteSchema);

View File

@@ -1,45 +0,0 @@
import mongoose, { Schema, Document, Types } from "mongoose";
export interface IRole extends Document {
_id: Types.ObjectId;
name: string;
description?: string;
permissions: string[];
isActive: boolean;
createdAt: Date;
updatedAt: Date;
}
const roleSchema = new Schema<IRole>(
{
name: {
type: String,
required: true,
trim: true,
maxlength: 50,
},
description: {
type: String,
trim: true,
maxlength: 200,
},
permissions: [
{
type: String,
required: true,
},
],
isActive: {
type: Boolean,
default: true,
},
},
{
timestamps: true,
}
);
roleSchema.index({ name: 1 }, { unique: true });
export const Role = mongoose.model<IRole>("Role", roleSchema);

View File

@@ -1,9 +0,0 @@
export { User } from "./auth/User.js";
export type { IUser } from "./auth/User.js";
export type { ITokenizedUser } from "./auth/User.js";
export { Role } from "./auth/Role.js";
export type { IRole } from "./auth/Role.js";
export type { IInvite } from "./auth/Invite.js";
export { Invite } from "./auth/Invite.js";
export { Migration } from "./migration/Migration.js";
export type { IMigration } from "./migration/Migration.js";

View File

@@ -1,34 +0,0 @@
import mongoose, { Schema, Document, Types } from "mongoose";
export interface IMigration extends Document {
_id: Types.ObjectId;
name: string;
runAt: Date;
success: boolean;
createdAt: Date;
updatedAt: Date;
}
const migrationSchema = new Schema<IMigration>(
{
name: {
type: String,
required: true,
},
runAt: {
type: Date,
required: false,
},
success: {
type: Boolean,
required: false,
},
},
{
timestamps: true,
}
);
migrationSchema.index({ name: 1 }, { unique: true });
export const Migration = mongoose.model<IMigration>("Migration", migrationSchema);

View File

@@ -1,5 +1,4 @@
import Monitor from "../../models/Monitor.js";
export const MIGRATION_NAME = "0001_migrateStatusWindowThreshold";
async function migrateStatusWindowThreshold() {
try {
const monitors = await Monitor.find({ statusWindowThreshold: { $lt: 1 } });

View File

@@ -1,56 +0,0 @@
import { User, Role } from "../../models/index.js";
import { Types } from "mongoose";
import { DEFAULT_ROLES } from "../../../service/v2/business/AuthService.js";
export const MIGRATION_NAME = "0002_migrateUsers";
async function migrateUsers() {
try {
// Create roles if they don't exist
const roleCount = await Role.countDocuments();
let roles;
if (roleCount === 0) {
const rolePromises = DEFAULT_ROLES.map((roleData) =>
new Role({
...roleData,
}).save()
);
roles = await Promise.all(rolePromises);
} else {
roles = await Role.find();
}
// Migrate users
const users = await User.find({
$or: [{ version: { $exists: false } }, { version: { $lt: 2 } }],
});
for (const user of users) {
const newRoleIds: Types.ObjectId[] = [];
for (const role of user.role || []) {
if (role === "superadmin") {
const superAdminRole = roles.find((role) => role.name === "SuperAdmin");
newRoleIds.push(superAdminRole!._id);
}
if (role === "admin") {
const managerRole = roles.find((role) => role.name === "Manager");
newRoleIds.push(managerRole!._id);
}
if (role === "user") {
const memberRole = roles.find((role) => role.name === "Member");
newRoleIds.push(memberRole!._id);
}
}
const merged = [...new Set([...user.roles, ...newRoleIds])];
user.roles = merged;
user.version = 2;
await user.save();
}
return true;
} catch (err) {
console.error("Migration error:", err);
return false;
}
}
export { migrateUsers };

View File

@@ -0,0 +1,7 @@
import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js";
const runMigrations = async () => {
await migrateStatusWindowThreshold();
};
export { runMigrations };

View File

@@ -1,25 +0,0 @@
import { migrateStatusWindowThreshold, MIGRATION_NAME as MIGRATION_0001 } from "./0001_migrateStatusWindowThreshold.js";
import { migrateUsers, MIGRATION_NAME as MIGRATION_0002 } from "./0002_migrateUsers.js";
import { Migration, IMigration } from "../../models/index.js";
const runMigrations = async () => {
const migrations = [
{ name: MIGRATION_0001, migration: migrateStatusWindowThreshold },
{ name: MIGRATION_0002, migration: migrateUsers },
];
const migrationNames = migrations.map((m) => m.name);
const appliedMigrations = await Migration.find({ name: { $in: migrationNames }, success: true });
const migrationsToRun = migrations.filter((m) => !appliedMigrations.some((am: IMigration) => am.name === m.name));
for (const { name, migration } of migrationsToRun) {
console.log(`Running migration: ${name}`);
const success = await migration();
await Migration.updateOne({ name }, { $set: { runAt: new Date(), success } }, { upsert: true });
console.log(`Migration ${name} completed with status: ${success ? "success" : "failure"}`);
}
};
export { runMigrations };

View File

@@ -1,17 +1,17 @@
const SERVICE_NAME = "inviteModule";
class InviteModule {
constructor({ Invite, crypto, stringService }) {
this.Invite = Invite;
constructor({ InviteToken, crypto, stringService }) {
this.InviteToken = InviteToken;
this.crypto = crypto;
this.stringService = stringService;
}
requestInviteToken = async (userData) => {
try {
await this.Invite.deleteMany({ email: userData.email });
await this.InviteToken.deleteMany({ email: userData.email });
userData.token = this.crypto.randomBytes(32).toString("hex");
let inviteToken = new this.Invite(userData);
let inviteToken = new this.InviteToken(userData);
await inviteToken.save();
return inviteToken;
} catch (error) {
@@ -23,7 +23,7 @@ class InviteModule {
getInviteToken = async (token) => {
try {
const invite = await this.Invite.findOne({
const invite = await this.InviteToken.findOne({
token,
});
if (invite === null) {
@@ -38,7 +38,7 @@ class InviteModule {
};
getInviteTokenAndDelete = async (token) => {
try {
const invite = await this.Invite.findOneAndDelete({
const invite = await this.InviteToken.findOneAndDelete({
token,
});
if (invite === null) {

View File

@@ -1,21 +0,0 @@
import { Request, Response, NextFunction } from "express";
import { decode } from "../../utils/JWTUtils.js";
import ApiError from "../../utils/ApiError.js";
const verifyToken = (req: Request, res: Response, next: NextFunction) => {
const token = req.cookies.token;
if (!token) {
const error = new ApiError("No token provided", 401);
return next(error);
}
try {
const decoded = decode(token);
req.user = decoded;
next();
} catch (error) {
next(error);
}
};
export { verifyToken };

View File

@@ -3,8 +3,6 @@ const SERVICE_NAME = "verifyTeamAccess";
const verifyTeamAccess = (Model, paramName) => {
return async (req, res, next) => {
try {
const user = req.user;
console.log(user);
const documentId = req.params[paramName];
const doc = await Model.findById(documentId);

View File

@@ -1,31 +0,0 @@
import { Router } from "express";
import express from "express";
import AuthController from "../../controllers/v2/AuthController.js";
import { verifyToken } from "../../middleware/v2/VerifyToken.js";
const router = express.Router();
class AuthRoutes {
private controller: AuthController;
private router: Router;
constructor(authController: AuthController) {
this.controller = authController;
this.router = Router();
this.initRoutes();
}
initRoutes = () => {
this.router.post("/register", this.controller.register);
this.router.post("/register/invite/:token", this.controller.registerWithInvite);
this.router.post("/login", this.controller.login);
this.router.post("/logout", this.controller.logout);
this.router.get("/me", verifyToken, this.controller.me);
};
getRouter() {
return this.router;
}
}
export default AuthRoutes;

View File

@@ -1,141 +0,0 @@
import bcrypt from "bcryptjs";
import { User, Role, ITokenizedUser } from "../../../db/models/index.js";
import ApiError from "../../../utils/ApiError.js";
import { Types } from "mongoose";
export const DEFAULT_ROLES = [
{
name: "SuperAdmin",
description: "Super admin with all permissions",
permissions: ["*"],
isSystem: true,
},
{
name: "Admin",
description: "Admin with full permissions",
permissions: ["monitor.*", "users.*"],
isSystem: true,
},
{
name: "Manager",
description: "Can manage users",
permissions: ["users.create", "users.update", "monitors.*"],
isSystem: true,
},
{
name: "Member",
description: "Basic team member",
permissions: ["users.update", "monitors.create", "monitors.view", "monitors.update"],
isSystem: true,
},
];
export type RegisterData = {
email: string;
firstName: string;
lastName: string;
password: string;
roles?: Types.ObjectId[]; // Optional roles for invite-based registration
};
export type LoginData = {
email: string;
password: string;
};
export type AuthResult = ITokenizedUser;
export interface IAuthService {
register(signupData: RegisterData): Promise<ITokenizedUser>;
registerWithInvite(signupData: RegisterData): Promise<ITokenizedUser>;
login(loginData: LoginData): Promise<ITokenizedUser>;
}
class AuthService implements IAuthService {
constructor() {}
async register(signupData: RegisterData): Promise<ITokenizedUser> {
const userCount = await User.countDocuments();
if (userCount > 0) {
throw new Error("Registration is closed. Please request an invite.");
}
const { email, firstName, lastName, password } = signupData;
// Create all default roles
const rolePromises = DEFAULT_ROLES.map((roleData) =>
new Role({
...roleData,
}).save()
);
const roles = await Promise.all(rolePromises);
// Find admin role and assign to first user
const superAdminRole = roles.find((role) => role.name === "SuperAdmin");
const user = new User({
email,
firstName,
lastName,
password,
roles: [superAdminRole!._id],
});
const savedUser = await user.save();
return {
sub: savedUser._id.toString(),
roles: savedUser.roles.map((role) => role.toString()),
};
}
async registerWithInvite(signupData: RegisterData): Promise<ITokenizedUser> {
const { email, firstName, lastName, password, roles } = signupData;
const user = new User({
email,
firstName,
lastName,
password,
roles: roles || [],
});
try {
const savedUser = await user.save();
return {
sub: savedUser._id.toString(),
roles: savedUser.roles.map((role) => role.toString()),
};
} catch (error: any) {
if (error?.code === 11000) {
const dupError = new ApiError("Email already in use", 409);
dupError.stack = error?.stack;
throw dupError;
}
throw error;
}
}
async login(loginData: LoginData): Promise<ITokenizedUser> {
const { email, password } = loginData;
// Find user by email
const user = await User.findOne({ email });
if (!user) {
throw new Error("Invalid email or password");
}
// Check password
const isPasswordValid = await bcrypt.compare(password, user.passwordHash);
if (!isPasswordValid) {
throw new Error("Invalid email or password");
}
return {
sub: user._id.toString(),
roles: user.roles.map((role) => role.toString()),
};
}
}
export default AuthService;

View File

@@ -1,61 +0,0 @@
import crypto from "node:crypto";
import { ITokenizedUser, IInvite, Invite } from "../../../db/models/index.js";
import ApiError from "../../../utils/ApiError.js";
export interface IInviteService {
create: (tokenizedUser: ITokenizedUser, invite: IInvite) => Promise<{ token: string }>;
getAll: () => Promise<IInvite[]>;
get: (tokenHash: string) => Promise<IInvite>;
delete: (id: string) => Promise<boolean>;
}
class InviteService implements IInviteService {
constructor() {}
create = async (tokenizedUser: ITokenizedUser, inviteData: IInvite) => {
const token = crypto.randomBytes(32).toString("hex");
const tokenHash = crypto.createHash("sha256").update(token).digest("hex");
try {
const invite = await Invite.create({
...inviteData,
tokenHash,
createdBy: tokenizedUser.sub,
updatedBy: tokenizedUser.sub,
});
if (!invite) {
throw new ApiError("Failed to create invite", 500);
}
return { token };
} catch (error: any) {
if (error?.code === 11000) {
const dupError = new ApiError("Invite with this email already exists", 409);
dupError.stack = error?.stack;
throw dupError;
}
throw error;
}
};
get = async (token: string) => {
const tokenHash = crypto.createHash("sha256").update(token).digest("hex");
const invite = await Invite.findOne({ tokenHash });
if (!invite) {
throw new ApiError("Invite not found", 404);
}
return invite;
};
getAll = async () => {
return Invite.find();
};
delete = async (id: string) => {
const result = await Invite.deleteOne({ _id: id });
if (!result.deletedCount) {
throw new ApiError("Invite not found", 404);
}
return result.deletedCount === 1;
};
}
export default InviteService;

View File

@@ -1,15 +0,0 @@
class ApiError extends Error {
public status: number;
constructor(message: string, status: number = 500) {
super(message);
this.status = status;
this.name = this.constructor.name;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
export default ApiError;

View File

@@ -1,22 +0,0 @@
import jwt from "jsonwebtoken";
import { AuthResult } from "../service/v2/business/AuthService.js";
const encode = (data: AuthResult): string => {
const secret = process.env.JWT_SECRET;
if (!secret) {
throw new Error("JWT_SECRET is not defined");
}
const token = jwt.sign(data, secret, { expiresIn: "99d" });
return token;
};
const decode = (token: string): AuthResult => {
const secret = process.env.JWT_SECRET;
if (!secret) {
throw new Error("JWT_SECRET is not defined");
}
const decoded = jwt.verify(token, secret) as AuthResult;
return decoded;
};
export { encode, decode };