Revert "Merge pull request #2973 from bluewave-labs/feat/v2/auth"
This reverts commit72882fded9, reversing changes made tobfb299cfca.
This commit is contained in:
1 parent
003d419a64
commit
990aed9f2e
25 files changed
+69
-894
No files matched your search
@@ -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;
|
||||
};
|
||||
@@ -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());
|
||||
};
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
Executable
+35
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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";
|
||||
@@ -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);
|
||||
-1
@@ -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 } });
|
||||
@@ -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 };
|
||||
@@ -0,0 +1,7 @@
|
||||
import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js";
|
||||
|
||||
const runMigrations = async () => {
|
||||
await migrateStatusWindowThreshold();
|
||||
};
|
||||
|
||||
export { runMigrations };
|
||||
@@ -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 };
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 };
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
Reference in new issue
Block a user