From 990aed9f2e01c459135d6de835b084d452fe4abe Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 23 Sep 2025 14:05:23 -0700 Subject: [PATCH] Revert "Merge pull request #2973 from bluewave-labs/feat/v2/auth" This reverts commit 72882fded9b57b1d0f76a521e12afea9f63135ce, reversing changes made to bfb299cfcaa4dcfaff68daed9f4629bc8fcce554. --- server/nodemon.json | 4 +- server/package-lock.json | 117 --------------- server/package.json | 4 +- server/src/config/controllers.js | 6 - server/src/config/routes.js | 10 -- server/src/config/services.js | 15 +- server/src/controllers/v2/AuthController.ts | 142 ------------------ server/src/db/models/InviteToken.js | 35 +++++ .../src/db/models/{auth/User.ts => User.js} | 80 ++-------- server/src/db/models/auth/Invite.ts | 68 --------- server/src/db/models/auth/Role.ts | 45 ------ server/src/db/models/index.ts | 9 -- server/src/db/models/migration/Migration.ts | 34 ----- ...s => 0001_migrateStatusWindowThreshold.js} | 1 - .../db/mongo/migration/0002_migrateUsers.ts | 56 ------- server/src/db/mongo/migration/index.js | 7 + server/src/db/mongo/migration/index.ts | 25 --- server/src/db/mongo/modules/inviteModule.js | 12 +- server/src/middleware/v2/VerifyToken.ts | 21 --- server/src/middleware/verifyTeamAccess.js | 2 - server/src/routes/v2/auth.ts | 31 ---- server/src/service/v2/business/AuthService.ts | 141 ----------------- .../src/service/v2/business/InviteService.ts | 61 -------- server/src/utils/ApiError.ts | 15 -- server/src/utils/JWTUtils.ts | 22 --- 25 files changed, 69 insertions(+), 894 deletions(-) delete mode 100644 server/src/controllers/v2/AuthController.ts create mode 100755 server/src/db/models/InviteToken.js rename server/src/db/models/{auth/User.ts => User.js} (55%) delete mode 100755 server/src/db/models/auth/Invite.ts delete mode 100644 server/src/db/models/auth/Role.ts delete mode 100644 server/src/db/models/index.ts delete mode 100644 server/src/db/models/migration/Migration.ts rename server/src/db/mongo/migration/{0001_migrateStatusWindowThreshold.ts => 0001_migrateStatusWindowThreshold.js} (90%) delete mode 100644 server/src/db/mongo/migration/0002_migrateUsers.ts create mode 100644 server/src/db/mongo/migration/index.js delete mode 100644 server/src/db/mongo/migration/index.ts delete mode 100644 server/src/middleware/v2/VerifyToken.ts delete mode 100644 server/src/routes/v2/auth.ts delete mode 100644 server/src/service/v2/business/AuthService.ts delete mode 100644 server/src/service/v2/business/InviteService.ts delete mode 100644 server/src/utils/ApiError.ts delete mode 100644 server/src/utils/JWTUtils.ts diff --git a/server/nodemon.json b/server/nodemon.json index 7371b4904..88d6ac686 100755 --- a/server/nodemon.json +++ b/server/nodemon.json @@ -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" } diff --git a/server/package-lock.json b/server/package-lock.json index e21b28382..06c1c30b3 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -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", diff --git a/server/package.json b/server/package.json index 5c655af9d..637c785cc 100755 --- a/server/package.json +++ b/server/package.json @@ -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", diff --git a/server/src/config/controllers.js b/server/src/config/controllers.js index 250d04c4b..5c54579eb 100644 --- a/server/src/config/controllers.js +++ b/server/src/config/controllers.js @@ -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; }; diff --git a/server/src/config/routes.js b/server/src/config/routes.js index 5ef622f37..2ec93a2dc 100644 --- a/server/src/config/routes.js +++ b/server/src/config/routes.js @@ -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()); }; diff --git a/server/src/config/services.js b/server/src/config/services.js index b2b4ac430..08c2525f3 100644 --- a/server/src/config/services.js +++ b/server/src/config/services.js @@ -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) => { diff --git a/server/src/controllers/v2/AuthController.ts b/server/src/controllers/v2/AuthController.ts deleted file mode 100644 index a9d1dd203..000000000 --- a/server/src/controllers/v2/AuthController.ts +++ /dev/null @@ -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; - registerWithInvite(req: Request, res: Response, next: NextFunction): Promise; - login(req: Request, res: Response, next: NextFunction): Promise; - 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; diff --git a/server/src/db/models/InviteToken.js b/server/src/db/models/InviteToken.js new file mode 100755 index 000000000..079600647 --- /dev/null +++ b/server/src/db/models/InviteToken.js @@ -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); diff --git a/server/src/db/models/auth/User.ts b/server/src/db/models/User.js similarity index 55% rename from server/src/db/models/auth/User.ts rename to server/src/db/models/User.js index 82c058ea2..9368d98b1 100755 --- a/server/src/db/models/auth/User.ts +++ b/server/src/db/models/User.js @@ -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; - // V2 - roles: Types.ObjectId[]; - lastLoginAt?: Date; - version: number; -} - -const UserSchema = new Schema( +const UserSchema = mongoose.Schema( { firstName: { type: String, @@ -58,7 +22,7 @@ const UserSchema = new Schema( }, password: { type: String, - required: false, + required: true, }, avatarImage: { type: String, @@ -77,8 +41,8 @@ const UserSchema = new Schema( }, 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( 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("User", UserSchema); +const User = mongoose.model("User", UserSchema); + +export default User; diff --git a/server/src/db/models/auth/Invite.ts b/server/src/db/models/auth/Invite.ts deleted file mode 100755 index 148ff65cb..000000000 --- a/server/src/db/models/auth/Invite.ts +++ /dev/null @@ -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( - { - 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("Invite", InviteSchema); diff --git a/server/src/db/models/auth/Role.ts b/server/src/db/models/auth/Role.ts deleted file mode 100644 index e7ffb2f7d..000000000 --- a/server/src/db/models/auth/Role.ts +++ /dev/null @@ -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( - { - 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("Role", roleSchema); diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts deleted file mode 100644 index 537633bfd..000000000 --- a/server/src/db/models/index.ts +++ /dev/null @@ -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"; diff --git a/server/src/db/models/migration/Migration.ts b/server/src/db/models/migration/Migration.ts deleted file mode 100644 index cf76bb48e..000000000 --- a/server/src/db/models/migration/Migration.ts +++ /dev/null @@ -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( - { - 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("Migration", migrationSchema); diff --git a/server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.ts b/server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.js similarity index 90% rename from server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.ts rename to server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.js index b44487e87..8f6988de4 100644 --- a/server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.ts +++ b/server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.js @@ -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 } }); diff --git a/server/src/db/mongo/migration/0002_migrateUsers.ts b/server/src/db/mongo/migration/0002_migrateUsers.ts deleted file mode 100644 index a7cdb76fc..000000000 --- a/server/src/db/mongo/migration/0002_migrateUsers.ts +++ /dev/null @@ -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 }; diff --git a/server/src/db/mongo/migration/index.js b/server/src/db/mongo/migration/index.js new file mode 100644 index 000000000..4bf56895a --- /dev/null +++ b/server/src/db/mongo/migration/index.js @@ -0,0 +1,7 @@ +import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js"; + +const runMigrations = async () => { + await migrateStatusWindowThreshold(); +}; + +export { runMigrations }; diff --git a/server/src/db/mongo/migration/index.ts b/server/src/db/mongo/migration/index.ts deleted file mode 100644 index 293e0905f..000000000 --- a/server/src/db/mongo/migration/index.ts +++ /dev/null @@ -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 }; diff --git a/server/src/db/mongo/modules/inviteModule.js b/server/src/db/mongo/modules/inviteModule.js index 513524fe5..444e51efd 100755 --- a/server/src/db/mongo/modules/inviteModule.js +++ b/server/src/db/mongo/modules/inviteModule.js @@ -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) { diff --git a/server/src/middleware/v2/VerifyToken.ts b/server/src/middleware/v2/VerifyToken.ts deleted file mode 100644 index 7dabd51cc..000000000 --- a/server/src/middleware/v2/VerifyToken.ts +++ /dev/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 }; diff --git a/server/src/middleware/verifyTeamAccess.js b/server/src/middleware/verifyTeamAccess.js index a548cbf23..60647bdc2 100644 --- a/server/src/middleware/verifyTeamAccess.js +++ b/server/src/middleware/verifyTeamAccess.js @@ -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); diff --git a/server/src/routes/v2/auth.ts b/server/src/routes/v2/auth.ts deleted file mode 100644 index 5a1b8ad63..000000000 --- a/server/src/routes/v2/auth.ts +++ /dev/null @@ -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; diff --git a/server/src/service/v2/business/AuthService.ts b/server/src/service/v2/business/AuthService.ts deleted file mode 100644 index ae22b981d..000000000 --- a/server/src/service/v2/business/AuthService.ts +++ /dev/null @@ -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; - registerWithInvite(signupData: RegisterData): Promise; - login(loginData: LoginData): Promise; -} - -class AuthService implements IAuthService { - constructor() {} - - async register(signupData: RegisterData): Promise { - 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 { - 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 { - 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; diff --git a/server/src/service/v2/business/InviteService.ts b/server/src/service/v2/business/InviteService.ts deleted file mode 100644 index f8bd9e2db..000000000 --- a/server/src/service/v2/business/InviteService.ts +++ /dev/null @@ -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; - get: (tokenHash: string) => Promise; - delete: (id: string) => Promise; -} - -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; diff --git a/server/src/utils/ApiError.ts b/server/src/utils/ApiError.ts deleted file mode 100644 index 313d01c2a..000000000 --- a/server/src/utils/ApiError.ts +++ /dev/null @@ -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; diff --git a/server/src/utils/JWTUtils.ts b/server/src/utils/JWTUtils.ts deleted file mode 100644 index 4eebb78a0..000000000 --- a/server/src/utils/JWTUtils.ts +++ /dev/null @@ -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 };