mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2025-12-30 22:29:37 -06:00
Revert "Merge pull request #2973 from bluewave-labs/feat/v2/auth"
This reverts commit72882fded9, reversing changes made tobfb299cfca.
This commit is contained in:
@@ -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
117
server/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
35
server/src/db/models/InviteToken.js
Executable file
35
server/src/db/models/InviteToken.js
Executable 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);
|
||||
@@ -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,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 };
|
||||
7
server/src/db/mongo/migration/index.js
Normal file
7
server/src/db/mongo/migration/index.js
Normal file
@@ -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