diff --git a/server/src/controllers/AuthController.ts b/server/src/controllers/AuthController.ts index 04ebab1de..ee1768ecf 100644 --- a/server/src/controllers/AuthController.ts +++ b/server/src/controllers/AuthController.ts @@ -132,22 +132,6 @@ class AuthController { }); res.status(200).json({ message: "Logout successful" }); }; - - cleanup = async (req: Request, res: Response) => { - try { - await this.authService.cleanup(); - res.status(200).json({ message: "Cleanup successful" }); - } catch (error) {} - }; - - cleanMonitors = async (req: Request, res: Response) => { - try { - await this.authService.cleanMonitors(); - res.status(200).json({ message: "Monitors cleanup successful" }); - } catch (error) { - res.status(500).json({ message: "Internal server error" }); - } - }; } export default AuthController; diff --git a/server/src/controllers/RecoveryController.ts b/server/src/controllers/RecoveryController.ts index 9430cf27f..28254ccf8 100644 --- a/server/src/controllers/RecoveryController.ts +++ b/server/src/controllers/RecoveryController.ts @@ -73,7 +73,10 @@ class RecoveryController implements IRecoveryController { if (!recoveryToken) { throw new ApiError("Invalid recovery token", 400); } - await this.authService.changePassword(recoveryToken.userId, password); + await this.authService.changePassword( + recoveryToken.userId.toString(), + password + ); await this.recoveryService.delete(recoveryToken._id); const user = await User.findById(recoveryToken.userId); diff --git a/server/src/db/models/auth/Role.ts b/server/src/db/models/auth/Role.ts index b8d6cdd4a..49e0c1768 100644 --- a/server/src/db/models/auth/Role.ts +++ b/server/src/db/models/auth/Role.ts @@ -3,6 +3,18 @@ import mongoose, { Schema, Document, Types } from "mongoose"; export const ScopeTypes = ["organization", "team"] as const; export type ScopeType = (typeof ScopeTypes)[number]; +type IdLike = string | Types.ObjectId; + +export type RoleLike = { + _id: IdLike; + organizationId: IdLike; + name: string; + scope: ScopeType; + permissions: string[]; + createdAt: Date; + updatedAt: Date; +}; + export interface IRole extends Document { _id: Types.ObjectId; organizationId: Types.ObjectId; @@ -15,7 +27,7 @@ export interface IRole extends Document { const roleSchema = new Schema( { - organizationId: { type: Schema.Types.ObjectId, ref: "Org" }, + organizationId: { type: Schema.Types.ObjectId, ref: "Org", required: true }, name: { type: String, required: true }, scope: { type: String, enum: ScopeTypes, required: true }, permissions: { type: [String], default: [] }, diff --git a/server/src/db/models/auth/User.ts b/server/src/db/models/auth/User.ts index 30b1abaec..cf5b4b51e 100644 --- a/server/src/db/models/auth/User.ts +++ b/server/src/db/models/auth/User.ts @@ -2,43 +2,6 @@ import mongoose, { Schema, Document, Types } from "mongoose"; import { IRole, ITeam } from "@/db/models/index.js"; import { Entitlements } from "@/types/entitlements.js"; -export interface ITokenizedUser { - sub: string; - email: string; - orgId: string; -} -export interface IUserContext { - sub: string; - email: string; - orgId: string; - teamIds?: string[]; - teams?: ITeam[]; - currentTeamId?: string; - roles?: { - orgRole?: IRole; - teamRole: IRole; - }; - entitlements: Entitlements; -} - -export interface IUserReturnable { - id: string; - email: string; - firstName: string; - lastName: string; - org: { - name: string; - planKey: string; - permissions: string[]; - }; - teams: { - id: string; - name: string; - permissions: string[]; - }[]; - entitlements: Entitlements; -} - export interface IUser extends Document { _id: Types.ObjectId; firstName: string; diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index 9445bcfcc..29f78dd11 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -7,13 +7,7 @@ export { MonitorStats } from "./monitors/MonitorStats.js"; export type { IMonitorStats } from "./monitors/MonitorStats.js"; export { User } from "./auth/User.js"; -export type { - IUser, - ITokenizedUser, - IUserReturnable, - IUserContext, - IUserProfile, -} from "./auth/User.js"; +export type { IUser, IUserProfile } from "./auth/User.js"; export { NotificationChannel, @@ -25,7 +19,7 @@ export { Invite } from "./auth/Invite.js"; export type { IInvite } from "./auth/Invite.js"; export { Role } from "./auth/Role.js"; -export type { IRole } from "./auth/Role.js"; +export type { IRole, RoleLike } from "./auth/Role.js"; export { Maintenance } from "./maintenance/Maintenance.js"; export type { IMaintenance } from "./maintenance/Maintenance.js"; diff --git a/server/src/init/services.ts b/server/src/init/services.ts index 0f9961fcb..18608adc6 100644 --- a/server/src/init/services.ts +++ b/server/src/init/services.ts @@ -44,6 +44,7 @@ import { MongoInviteRepository, MongoTeamMembershipRepository, MongoOrgRepository, + MongoOrgMembershipRepository, } from "@/repositories/index.js"; export const initServices = async () => { @@ -56,7 +57,8 @@ export const initServices = async () => { const teamRepository = new MongoTeamRepository(); const inviteRepository = new MongoInviteRepository(); const teamMembershipRepository = new MongoTeamMembershipRepository(); - const mongoOrgRepository = new MongoOrgRepository(); + const orgRepository = new MongoOrgRepository(); + const orgMembershipRepository = new MongoOrgMembershipRepository(); const checkService = new CheckService(checksRepository, monitorRepository); const inviteService = new InviteService( @@ -112,7 +114,9 @@ export const initServices = async () => { entitlementsProvider, monitorRepository, userRepository, - mongoOrgRepository, + roleRepository, + orgRepository, + orgMembershipRepository, teamRepository, teamMembershipRepository ); diff --git a/server/src/repositories/org-membership/IOrgMembershopRepository.ts b/server/src/repositories/org-membership/IOrgMembershopRepository.ts index df2b9ef3d..a3c46ff82 100644 --- a/server/src/repositories/org-membership/IOrgMembershopRepository.ts +++ b/server/src/repositories/org-membership/IOrgMembershopRepository.ts @@ -1,7 +1,13 @@ +import { OrgMembership } from "@/types/domain/index.js"; + export interface IOrgMembershipRepository { // create + create(orgMembershipData: Partial): Promise; // single fetch + findByUserId(userId: string): Promise; // collection fetch // update + update(orgMembership: Partial): Promise; // delete + deleteById(membershipId: string): Promise; } diff --git a/server/src/repositories/org-membership/MongoOrgMembershipRepository.ts b/server/src/repositories/org-membership/MongoOrgMembershipRepository.ts index dc149ab15..e61721128 100644 --- a/server/src/repositories/org-membership/MongoOrgMembershipRepository.ts +++ b/server/src/repositories/org-membership/MongoOrgMembershipRepository.ts @@ -14,6 +14,39 @@ class MongoOrgMembershipRepository implements IOrgMembershipRepository { updatedAt: doc.updatedAt, }; }; + + create = async (orgMembershipData: Partial) => { + const membership = await OrgMembership.create(orgMembershipData); + return this.toEntity(membership); + }; + + findByUserId = async (userId: string) => { + const orgMembership = await OrgMembership.findOne({ + userId, + }); + if (!orgMembership) return null; + return this.toEntity(orgMembership); + }; + + update = async (orgMembership: Partial) => { + const updated = await OrgMembership.findOneAndUpdate( + { _id: orgMembership.id }, + { + $set: { + ...orgMembership, + updatedAt: new Date(), + }, + }, + { new: true } + ); + if (!updated) return null; + return this.toEntity(updated); + }; + + deleteById = async (membershipId: string) => { + const result = await OrgMembership.deleteOne({ _id: membershipId }); + return result.deletedCount === 1; + }; } export default MongoOrgMembershipRepository; diff --git a/server/src/repositories/org/IOrgRepository.ts b/server/src/repositories/org/IOrgRepository.ts index 15bfa80ac..22da0b7bd 100644 --- a/server/src/repositories/org/IOrgRepository.ts +++ b/server/src/repositories/org/IOrgRepository.ts @@ -4,7 +4,10 @@ export interface IOrgRepository { // create create(orgData: Partial): Promise; // single fetch + findById(orgId: string): Promise; // collection fetch // update + updateById(orgId: string, orgData: Partial): Promise; // delete + deleteById(orgId: string): Promise; } diff --git a/server/src/repositories/org/MongoOrgRepository.ts b/server/src/repositories/org/MongoOrgRepository.ts index 46e374545..00472a3e8 100644 --- a/server/src/repositories/org/MongoOrgRepository.ts +++ b/server/src/repositories/org/MongoOrgRepository.ts @@ -29,6 +29,27 @@ class MongoOrgRepository implements IOrgRepository { const org = await Org.create(orgData); return this.toEntity(org); }; + + findById = async (orgId: string) => { + const org = await Org.findById(orgId); + if (!org) return null; + return this.toEntity(org); + }; + + updateById = async (orgId: string, orgData: Partial) => { + const updated = await Org.findOneAndUpdate( + { _id: orgId }, + { $set: orgData }, + { new: true } + ); + if (!updated) return null; + return this.toEntity(updated); + }; + + deleteById = async (orgId: string) => { + const result = await Org.deleteOne({ _id: orgId }); + return result.deletedCount === 1; + }; } export default MongoOrgRepository; diff --git a/server/src/repositories/role/IRoleRepository.ts b/server/src/repositories/role/IRoleRepository.ts index 9d7060c65..40f4ac8fa 100644 --- a/server/src/repositories/role/IRoleRepository.ts +++ b/server/src/repositories/role/IRoleRepository.ts @@ -2,10 +2,11 @@ import { Role } from "@/types/domain/index.js"; export interface IRoleRepository { // create - createMany(rolesData: Role[]): Promise; + createMany(rolesData: Array>): Promise; // single fetch findById(roleId: string, orgId: string): Promise; // collection fetch // update // delete + deleteMany(roleIds: string[]): Promise; } diff --git a/server/src/repositories/role/MongoRoleRepository.ts b/server/src/repositories/role/MongoRoleRepository.ts index 95dc1be4e..6882e8e7a 100644 --- a/server/src/repositories/role/MongoRoleRepository.ts +++ b/server/src/repositories/role/MongoRoleRepository.ts @@ -1,11 +1,10 @@ -import { Types } from "mongoose"; import type { IRoleRepository } from "./IRoleRepository.js"; import { Role } from "@/db/models/index.js"; -import type { IRole } from "@/db/models/index.js"; +import type { RoleLike, IRole } from "@/db/models/index.js"; import type { Role as RoleEntity } from "@/types/domain/index.js"; class MongoRoleRepository implements IRoleRepository { - private toEntity = (doc: IRole): RoleEntity => { + private toEntity = (doc: RoleLike): RoleEntity => { return { id: doc._id.toString(), organizationId: doc.organizationId.toString(), @@ -18,24 +17,18 @@ class MongoRoleRepository implements IRoleRepository { }; createMany = async (rolesData: RoleEntity[]): Promise => { - const docs = rolesData.map((role) => ({ - ...role, - organizationId: role.organizationId - ? new Types.ObjectId(role.organizationId) - : undefined, - })); - - const insertedRoles = await Role.insertMany(docs); + const insertedRoles = await Role.insertMany(rolesData); return insertedRoles.map((r) => { - return this.toEntity({ - _id: r._id, - organizationId: r.organizationId || new Types.ObjectId(), + const e = { + _id: r._id.toString(), + organizationId: r.organizationId.toString(), name: r.name, scope: r.scope, permissions: r.permissions, createdAt: r.createdAt, updatedAt: r.updatedAt, - }); + }; + return this.toEntity(e); }); }; @@ -52,6 +45,11 @@ class MongoRoleRepository implements IRoleRepository { } return this.toEntity(role); }; + + deleteMany = async (roleIds: string[]) => { + const result = await Role.deleteMany({ _id: { $in: roleIds } }); + return result.deletedCount ?? 0; + }; } export default MongoRoleRepository; diff --git a/server/src/repositories/team-membership/ITeamMembershipRepository.ts b/server/src/repositories/team-membership/ITeamMembershipRepository.ts index 9dff65003..1badeb997 100644 --- a/server/src/repositories/team-membership/ITeamMembershipRepository.ts +++ b/server/src/repositories/team-membership/ITeamMembershipRepository.ts @@ -1,10 +1,17 @@ -import { TeamMembership } from "@/types/domain/index.js"; +import { + TeamMembership, + TeamMembershipWithDetails, +} from "@/types/domain/index.js"; export interface ITeamMembershipRepository { // create create(membership: Partial): Promise; // single fetch findByUserId(userId: string, teamId: string): Promise; + findByUserIdWithRole( + userId: string + ): Promise; // collection fetch // update // delete + deleteById(membershipId: string): Promise; } diff --git a/server/src/repositories/team-membership/MongoTeamMembershipRepository.ts b/server/src/repositories/team-membership/MongoTeamMembershipRepository.ts index 0521326e1..98dea1674 100644 --- a/server/src/repositories/team-membership/MongoTeamMembershipRepository.ts +++ b/server/src/repositories/team-membership/MongoTeamMembershipRepository.ts @@ -1,6 +1,15 @@ import { type ITeamMembership, TeamMembership } from "@/db/models/index.js"; import type { ITeamMembershipRepository } from "@/repositories/index.js"; -import { TeamMembership as TeamMembershipEntity } from "@/types/domain/index.js"; +import { + TeamMembership as TeamMembershipEntity, + TeamMembershipWithDetails, +} from "@/types/domain/index.js"; +import type { IRole } from "@/db/models/index.js"; + +type TeamMembershipPopulated = Omit & { + roleId: IRole; +}; + class MongoTeamMembershipRepository implements ITeamMembershipRepository { private toEntity = (doc: ITeamMembership): TeamMembershipEntity => { return { @@ -13,6 +22,28 @@ class MongoTeamMembershipRepository implements ITeamMembershipRepository { updatedAt: doc.updatedAt, }; }; + + private toEntityWithDetails = ( + doc: TeamMembershipPopulated + ): TeamMembershipWithDetails => { + return { + id: doc._id.toString(), + orgId: doc.orgId.toString(), + teamId: doc.teamId.toString(), + userId: doc.userId.toString(), + roleId: doc.roleId.toString(), + role: { + id: doc.roleId._id.toString(), + organizationId: doc.roleId.organizationId.toString(), + name: doc.roleId.name, + permissions: doc.roleId.permissions, + createdAt: doc.roleId.createdAt, + updatedAt: doc.roleId.updatedAt, + }, + createdAt: doc.createdAt, + updatedAt: doc.updatedAt, + }; + }; create = async (membership: Partial) => { const created = await TeamMembership.create(membership); return this.toEntity(created); @@ -26,6 +57,20 @@ class MongoTeamMembershipRepository implements ITeamMembershipRepository { if (!membership) return null; return this.toEntity(membership); }; + + findByUserIdWithRole = async (userId: string) => { + const teamMembershipsWithRole = await TeamMembership.find({ + userId: userId, + }) + .populate<{ roleId: IRole }>("roleId") + .lean(); + return teamMembershipsWithRole.map(this.toEntityWithDetails); + }; + + deleteById = async (membershipId: string) => { + const result = await TeamMembership.deleteOne({ _id: membershipId }); + return result.deletedCount === 1; + }; } export default MongoTeamMembershipRepository; diff --git a/server/src/repositories/team/ITeamRepository.ts b/server/src/repositories/team/ITeamRepository.ts index f23f388c0..e69dd8013 100644 --- a/server/src/repositories/team/ITeamRepository.ts +++ b/server/src/repositories/team/ITeamRepository.ts @@ -6,6 +6,8 @@ export interface ITeamRepository { // single fetch findById(teamId: string, orgId: string): Promise; // collection fetch + findManyById(teamIds: string[]): Promise; // update // delete + deleteById(teamId: string): Promise; } diff --git a/server/src/repositories/team/MongoTeamRepository.ts b/server/src/repositories/team/MongoTeamRepository.ts index 233ddb29d..220812d14 100644 --- a/server/src/repositories/team/MongoTeamRepository.ts +++ b/server/src/repositories/team/MongoTeamRepository.ts @@ -1,6 +1,7 @@ import type { ITeamRepository } from "@/repositories/index.js"; import { type ITeam, Team } from "@/db/models/index.js"; import { Team as TeamEntity } from "@/types/domain/index.js"; + class MongoTeamRepository implements ITeamRepository { private toEntity = (doc: ITeam): TeamEntity => { return { @@ -29,6 +30,16 @@ class MongoTeamRepository implements ITeamRepository { } return this.toEntity(team); }; + + findManyById = async (teamIds: string[]) => { + const teams = await Team.find({ _id: { $in: teamIds } }); + return teams.map(this.toEntity); + }; + + deleteById = async (teamId: string) => { + const result = await Team.deleteOne({ _id: teamId }); + return result.deletedCount === 1; + }; } export default MongoTeamRepository; diff --git a/server/src/repositories/user/IUserRepository.ts b/server/src/repositories/user/IUserRepository.ts index 37011492f..f99595118 100644 --- a/server/src/repositories/user/IUserRepository.ts +++ b/server/src/repositories/user/IUserRepository.ts @@ -6,5 +6,10 @@ export interface IUserRepository { findByEmail: (email: string) => Promise; // collection fetch // update + updateById: ( + userId: string, + updateData: Partial + ) => Promise; // delete + deleteById(userId: string): Promise; } diff --git a/server/src/repositories/user/MongoUserRepository.ts b/server/src/repositories/user/MongoUserRepository.ts index 585635af5..8fe5b0deb 100644 --- a/server/src/repositories/user/MongoUserRepository.ts +++ b/server/src/repositories/user/MongoUserRepository.ts @@ -27,6 +27,21 @@ class MongoUserRepository implements IUserRepository { if (!user) return null; return this.toEntity(user); }; + + updateById = async (userId: string, updateData: Partial) => { + const result = await User.findOneAndUpdate( + { _id: userId }, + { $set: updateData }, + { new: true } + ); + if (!result) return null; + return this.toEntity(result); + }; + + deleteById = async (userId: string) => { + const result = await User.deleteOne({ _id: userId }); + return result.deletedCount === 1; + }; } export default MongoUserRepository; diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 02edc6106..fde1d1899 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -1,7 +1,6 @@ import { Router } from "express"; import { AuthController } from "@/controllers/index.js"; -import { verifyToken } from "@/middleware/VerifyToken.js"; import { validateBody } from "@/middleware/validation.js"; import { registerSchema, @@ -36,8 +35,6 @@ class AuthRoutes { ); this.router.post("/logout", this.controller.logout); // this.router.get("/me", verifyToken, this.controller.me); - this.router.post("/cleanup", this.controller.cleanup); - this.router.post("/cleanup-monitors", this.controller.cleanMonitors); }; getRouter() { diff --git a/server/src/services/business/AuthService.ts b/server/src/services/business/AuthService.ts index 13a90b90c..883f0dc65 100644 --- a/server/src/services/business/AuthService.ts +++ b/server/src/services/business/AuthService.ts @@ -1,20 +1,4 @@ import bcrypt from "bcryptjs"; -import { - User, - Org, - OrgMembership, - Team, - ITeam, - IUserReturnable, - TeamMembership, - Role, - IRole, - IUser, - ITokenizedUser, - Check, - NotificationChannel, -} from "@/db/models/index.js"; -import { Types } from "mongoose"; import ApiError from "@/utils/ApiError.js"; import { IJobQueue } from "../infrastructure/JobQueue.js"; import { hashPassword } from "@/utils/JWTUtils.js"; @@ -26,11 +10,18 @@ import type { IMonitorRepository, IUserRepository, IOrgRepository, + IRoleRepository, ITeamRepository, + IOrgMembershipRepository, ITeamMembershipRepository, } from "@/repositories/index.js"; -import type { Invite } from "@/types/domain/index.js"; -import mongoose from "mongoose"; +import type { + User, + UserReturnable, + TokenizedUser, + Team, + Invite, +} from "@/types/domain/index.js"; const SERVICE_NAME = "AuthService"; @@ -46,29 +37,26 @@ export type LoginData = { password: string; }; -export type AuthResult = Partial; +export type AuthResult = Partial; export interface IAuthService { register(signupData: RegisterData): Promise<{ - tokenizedUser: ITokenizedUser; - returnableUser: IUserReturnable; + tokenizedUser: TokenizedUser; + returnableUser: UserReturnable; }>; registerWithInvite( invite: Invite, signupData: RegisterData ): Promise<{ - tokenizedUser: ITokenizedUser; - returnableUser: IUserReturnable; + tokenizedUser: TokenizedUser; + returnableUser: UserReturnable; }>; login(loginData: LoginData): Promise<{ - tokenizedUser: ITokenizedUser; - returnableUser: IUserReturnable; + tokenizedUser: TokenizedUser; + returnableUser: UserReturnable; }>; - getTeamIds(userId: string): Promise; - getTeams(teamIds: string[]): Promise; - cleanup(): Promise; - cleanMonitors(): Promise; - changePassword(userId: Types.ObjectId, newPassword: string): Promise; + getTeams(teamIds: string[]): Promise; + changePassword(userId: string, newPassword: string): Promise; } class AuthService implements IAuthService { @@ -77,7 +65,9 @@ class AuthService implements IAuthService { private entitlementsProvider: IEntitlementsProvider; private monitorRepository: IMonitorRepository; private userRepository: IUserRepository; + private roleRepository: IRoleRepository; private orgRepository: IOrgRepository; + private orgMembershipRepository: IOrgMembershipRepository; private teamRepository: ITeamRepository; private teamMembershipRepository: ITeamMembershipRepository; @@ -86,7 +76,9 @@ class AuthService implements IAuthService { entitlementsProvider: IEntitlementsProvider, monitorRepository: IMonitorRepository, userRepository: IUserRepository, + roleRepository: IRoleRepository, orgRepository: IOrgRepository, + orgMembershipRepository: IOrgMembershipRepository, teamRepository: ITeamRepository, teamMembershipRepository: ITeamMembershipRepository ) { @@ -96,8 +88,10 @@ class AuthService implements IAuthService { this.monitorRepository = monitorRepository; this.userRepository = userRepository; this.orgRepository = orgRepository; + this.roleRepository = roleRepository; this.teamRepository = teamRepository; this.teamMembershipRepository = teamMembershipRepository; + this.orgMembershipRepository = orgMembershipRepository; } async register(signupData: RegisterData) { @@ -113,7 +107,7 @@ class AuthService implements IAuthService { }; try { - const newUserData: Partial = { + const newUserData: Partial = { firstName: signupData.firstName, lastName: signupData.lastName, email: signupData.email, @@ -132,7 +126,7 @@ class AuthService implements IAuthService { created.org = org.id; - const roles = await Role.insertMany([ + const roles = await this.roleRepository.createMany([ { organizationId: org.id, name: "Org Admin", @@ -184,14 +178,14 @@ class AuthService implements IAuthService { }, ]); - created.roles = roles.map((r) => r._id); + created.roles = roles.map((r) => r.id); - const membership = await OrgMembership.create({ + const membership = await this.orgMembershipRepository.create({ userId: user.id, orgId: org.id, - roleId: roles[0]?._id, + roleId: roles[0]?.id, }); - created.orgMembership = membership._id; + created.orgMembership = membership.id; const team = await this.teamRepository.create({ name: "Default Team", @@ -205,11 +199,11 @@ class AuthService implements IAuthService { orgId: org.id, userId: user.id, teamId: team.id, - roleId: roles[2]?._id.toString(), + roleId: roles[2]?.id.toString(), }); created.teamMembership = teamMembership.id; - const tokenizedUser: ITokenizedUser = { + const tokenizedUser: TokenizedUser = { sub: user.id, email: user.email, orgId: org.id, @@ -224,7 +218,7 @@ class AuthService implements IAuthService { const entitlements: Entitlements = await this.entitlementsProvider.getForOrg(org.id); - const returnableUser: IUserReturnable = { + const returnableUser: UserReturnable = { id: user.id, email: user.email, firstName: user.firstName, @@ -240,12 +234,12 @@ class AuthService implements IAuthService { return { tokenizedUser, returnableUser }; } catch (error) { - await TeamMembership.deleteOne({ _id: created.teamMembership }); - await OrgMembership.deleteOne({ _id: created.orgMembership }); - await Team.deleteOne({ _id: created.team }); - await Role.deleteMany({ _id: { $in: created.roles } }); - if (created.org) await Org.findByIdAndDelete(created.org); - if (created.user) await User.findByIdAndDelete(created.user); + await this.teamMembershipRepository.deleteById(created.teamMembership); + await this.orgMembershipRepository.deleteById(created.orgMembership); + await this.teamRepository.deleteById(created.team); + await this.roleRepository.deleteMany(created.roles); + if (created.org) await this.orgRepository.deleteById(created.org); + if (created.user) await this.userRepository.deleteById(created.user); throw error; } } @@ -271,100 +265,102 @@ class AuthService implements IAuthService { throw new ApiError("No organization ID", 400); } // Check for org - const org = await Org.findById(orgId); + const org = await this.orgRepository.findById(orgId); if (!org) { throw new ApiError("Organization not found", 404); } // Get or Create user - let user = await User.findOne({ email }); + let user = await this.userRepository.findByEmail(email); if (!user) { const passwordHash = await hashPassword(signupData.password); - user = await User.create({ + user = await this.userRepository.create({ email, firstName: signupData.firstName, lastName: signupData.lastName, passwordHash, }); - created.user = user._id; + created.user = user.id; } - const team = await Team.findById(teamId); + const team = await this.teamRepository.findById(teamId, org.id); if (!team) { throw new ApiError("Team not found", 404); } // Get or create orgMembership - let orgMembership = await OrgMembership.findOne({ - userId: user._id, - orgId, - }); + let orgMembership = await this.orgMembershipRepository.findByUserId( + user.id + ); + if (orgMembership && !orgMembership.roleId && orgRoleId) { - orgMembership.roleId = new mongoose.Types.ObjectId(orgRoleId); - await orgMembership.save(); + orgMembership.roleId = orgRoleId; + await this.orgMembershipRepository.update(orgMembership); } if (!orgMembership) { - orgMembership = await OrgMembership.create({ + orgMembership = await this.orgMembershipRepository.create({ orgId, - userId: user._id, + userId: user.id, ...(orgRoleId ? { roleId: orgRoleId } : {}), }); - created.orgMembership = orgMembership._id; + created.orgMembership = orgMembership.id; } // Create TeamMembership - let teamMembership = await TeamMembership.findOne({ - userId: user._id, - teamId, - }); + let teamMembership = await this.teamMembershipRepository.findByUserId( + user.id, + teamId + ); if (teamMembership) { throw new ApiError("User is already a member of the team", 400); } - teamMembership = await TeamMembership.create({ + teamMembership = await this.teamMembershipRepository.create({ orgId, teamId, - userId: user._id, + userId: user.id, roleId: teamRoleId, }); - created.teamMembership = teamMembership._id; + created.teamMembership = teamMembership.id; // Get data for return - const teamMembershipWithRoles = await TeamMembership.find({ - userId: user._id, - }) - .populate<{ roleId: IRole }>("roleId") - .lean(); - - const teamIds = teamMembershipWithRoles.map((tm) => tm.teamId.toString()); + const teamMembershipsWithRole = + await this.teamMembershipRepository.findByUserIdWithRole(user.id); + if (!teamMembershipsWithRole) { + throw new ApiError("Team memberships not found for user", 404); + } + const teamIds = teamMembershipsWithRole.map((tm) => tm.teamId); const teams = await this.getTeams(teamIds); - const teamMap = new Map(teams.map((t) => [t._id.toString(), t])); + const teamMap = new Map(teams.map((t) => [t.id, t])); - const tokenizedUser: ITokenizedUser = { - sub: user._id.toString(), + const tokenizedUser: TokenizedUser = { + sub: user.id, email: user.email, - orgId: orgId.toString(), + orgId: orgId, }; - const returnableTeams = teamMembershipWithRoles.map((tm) => { - const team = teamMap.get(tm.teamId.toString()); + const returnableTeams = teamMembershipsWithRole.map((tm) => { + const team = teamMap.get(tm.teamId); if (!team) { throw new ApiError("Team not found for membership"); } return { - id: team._id.toString(), + id: team.id, name: team.name, - permissions: tm.roleId?.permissions ?? [], + permissions: tm.role?.permissions ?? [], }; }); - const orgRole = await Role.findById(orgMembership.roleId); + const orgRole = await this.roleRepository.findById( + orgMembership.roleId || "", + org.id + ); const orgPermissions = orgRole?.permissions || []; const entitlements: Entitlements = - await this.entitlementsProvider.getForOrg(org._id.toString()); - const returnableUser: IUserReturnable = { - id: user._id.toString(), + await this.entitlementsProvider.getForOrg(org.id); + const returnableUser: UserReturnable = { + id: user.id, email: user.email, firstName: user.firstName, lastName: user.lastName, @@ -383,26 +379,26 @@ class AuthService implements IAuthService { }; } catch (error) { if (created.orgMembership) { - await OrgMembership.deleteOne({ _id: created.orgMembership }); + await this.orgMembershipRepository.deleteById(created.orgMembership); } if (created.teamMembership) { - await TeamMembership.deleteOne({ _id: created.teamMembership }); + await this.teamMembershipRepository.deleteById(created.teamMembership); } if (created.user) { - await User.findByIdAndDelete(created.user); + await this.userRepository.deleteById(created.user); } throw error; } }; async login(loginData: LoginData): Promise<{ - tokenizedUser: ITokenizedUser; - returnableUser: IUserReturnable; + tokenizedUser: TokenizedUser; + returnableUser: UserReturnable; }> { const { email, password } = loginData; // Find user by email - const user = await User.findOne({ email }); + const user = await this.userRepository.findByEmail(email); if (!user) { throw new ApiError("Invalid email or password"); @@ -415,15 +411,16 @@ class AuthService implements IAuthService { } // Find OrgMembership - const orgMembership = await OrgMembership.findOne({ - userId: user._id, - }).lean(); + const orgMembership = await this.orgMembershipRepository.findByUserId( + user.id + ); + if (!orgMembership) { throw new ApiError("User is not part of any organization"); } // Find org and roles - let org = await Org.findById(orgMembership.orgId).lean(); + let org = await this.orgRepository.findById(orgMembership.orgId); if (!org) { throw new ApiError("Organization not found"); @@ -431,50 +428,53 @@ class AuthService implements IAuthService { // Update org entitlements const planKey = org.entitlements.plan; - org = await Org.findOneAndUpdate( - { _id: org._id }, - { $set: { entitlements: Plans[planKey] } }, - { new: true } - ); + org = await this.orgRepository.updateById(org.id, { + entitlements: Plans[planKey], + }); if (!org) { throw new ApiError("Organization entitlement error"); } - const orgRoles = await Role.findById(orgMembership.roleId).lean(); + const orgRoles = await this.roleRepository.findById( + orgMembership.roleId || "", + org.id + ); // Get teams - const teamMemberships = await TeamMembership.find({ userId: user._id }) - .populate<{ roleId: IRole }>("roleId") - .lean(); + const teamMemberships = + await this.teamMembershipRepository.findByUserIdWithRole(user.id); + if (!teamMemberships) { + throw new ApiError("Team memberships not found for user", 404); + } - const teamIds = teamMemberships.map((tm) => tm.teamId.toString()); + const teamIds = teamMemberships.map((tm) => tm.teamId); const teams = await this.getTeams(teamIds); - const teamMap = new Map(teams.map((t) => [t._id.toString(), t])); + const teamMap = new Map(teams.map((t) => [t.id, t])); const returnableTeams = teamMemberships.map((tm) => { - const team = teamMap.get(tm.teamId.toString()); + const team = teamMap.get(tm.teamId); if (!team) { throw new ApiError("Team not found for membership"); } return { - id: team._id.toString(), + id: team.id, name: team.name, - permissions: tm.roleId?.permissions ?? [], + permissions: tm.role?.permissions ?? [], }; }); - const tokenizedUser: ITokenizedUser = { - sub: user._id.toString(), + const tokenizedUser: TokenizedUser = { + sub: user.id, email: user.email, - orgId: orgMembership.orgId.toString(), + orgId: orgMembership.orgId, }; const entitlements: Entitlements = - await this.entitlementsProvider.getForOrg(org._id.toString()); + await this.entitlementsProvider.getForOrg(org.id); - const returnableUser: IUserReturnable = { - id: user._id.toString(), + const returnableUser: UserReturnable = { + id: user.id, email: user.email, firstName: user.firstName, lastName: user.lastName, @@ -493,46 +493,20 @@ class AuthService implements IAuthService { }; } - async getTeamIds(userId: string): Promise { - const teamMemberships = await TeamMembership.find({ userId }) - .select("teamId") - .lean(); - const teamIds = teamMemberships.map((t) => t.teamId.toString()); - return teamIds; - } - - async getTeams(teamIds: string[]): Promise { - const teams = await Team.find({ _id: { $in: teamIds } }).select( - "_id, name" - ); - return teams; - } - - async cleanup() { - await User.deleteMany({}); - await Role.deleteMany({}); - await this.monitorRepository.deleteAll(); - await Check.deleteMany({}); - await NotificationChannel.deleteMany({}); - await this.jobQueue.flush(); - } - - async cleanMonitors() { - this.monitorRepository.deleteAll(); - await Check.deleteMany({}); + async getTeams(teamIds: string[]): Promise { + return await this.teamRepository.findManyById(teamIds); } changePassword = async ( - userId: Types.ObjectId, + userId: string, newPassword: string ): Promise => { try { const passwordHash = await hashPassword(newPassword); - const result = await User.updateOne( - { _id: userId }, - { $set: { passwordHash } } - ); - return result.modifiedCount > 0; + const result = await this.userRepository.updateById(userId, { + passwordHash, + }); + return Boolean(result); } catch (error) { throw error; } diff --git a/server/src/services/business/CheckService.ts b/server/src/services/business/CheckService.ts index d7cda9f2e..64692bef1 100644 --- a/server/src/services/business/CheckService.ts +++ b/server/src/services/business/CheckService.ts @@ -10,10 +10,7 @@ import ApiError from "@/utils/ApiError.js"; import { getChildLogger } from "@/logger/Logger.js"; import { getStartDate } from "@/utils/TimeUtils.js"; -import type { - CheckEntity, - ICheckLighthouseFields, -} from "@/types/domain/index.js"; +import type { CheckEntity } from "@/types/domain/index.js"; import type { IChecksRepository, IMonitorRepository, diff --git a/server/src/services/business/MaintenanceService.ts b/server/src/services/business/MaintenanceService.ts index 9bb901750..a04882936 100644 --- a/server/src/services/business/MaintenanceService.ts +++ b/server/src/services/business/MaintenanceService.ts @@ -1,11 +1,6 @@ -import { - Monitor, - IUserContext, - IMaintenance, - Maintenance, -} from "@/db/models/index.js"; +import { Monitor, IMaintenance, Maintenance } from "@/db/models/index.js"; import ApiError from "@/utils/ApiError.js"; -import { MaintenanceRepeats } from "@/types/domain/index.js"; +import { MaintenanceRepeats, UserContext } from "@/types/domain/index.js"; const SERVICE_NAME = "MaintenanceService"; const DAY_MS = 24 * 60 * 60 * 1000; @@ -13,19 +8,19 @@ const WEEK_MS = 7 * DAY_MS; export interface IMaintenanceService { create: ( - userContext: IUserContext, + userContext: UserContext, maintenance: IMaintenance ) => Promise; getAll: (teamId: string) => Promise; get: (teamId: string, id: string) => Promise; toggleActive: ( teamId: string, - userContext: IUserContext, + userContext: UserContext, id: string ) => Promise; update: ( teamId: string, - userContext: IUserContext, + userContext: UserContext, id: string, updateData: Partial ) => Promise; @@ -47,7 +42,7 @@ class MaintenanceService implements IMaintenanceService { this.lastRefresh = 0; } - create = async (userContext: IUserContext, maintenanceData: IMaintenance) => { + create = async (userContext: UserContext, maintenanceData: IMaintenance) => { // Make sure monitors belong to current team const monitorIds = maintenanceData.monitors || []; const count = await Monitor.countDocuments({ @@ -86,7 +81,7 @@ class MaintenanceService implements IMaintenanceService { toggleActive = async ( teamId: string, - userContext: IUserContext, + userContext: UserContext, id: string ) => { const updatedMaintenance = await Maintenance.findOneAndUpdate( @@ -110,7 +105,7 @@ class MaintenanceService implements IMaintenanceService { update = async ( teamId: string, - userContext: IUserContext, + userContext: UserContext, id: string, updateData: Partial ) => { diff --git a/server/src/services/business/MeService.ts b/server/src/services/business/MeService.ts index 6a64bd7f1..fcf440d59 100644 --- a/server/src/services/business/MeService.ts +++ b/server/src/services/business/MeService.ts @@ -3,8 +3,6 @@ import { Org, OrgMembership, Team, - ITeam, - IUserReturnable, TeamMembership, Role, IRole, @@ -12,9 +10,10 @@ import { import ApiError from "@/utils/ApiError.js"; import type { IEntitlementsProvider } from "@/services/system/EntitlementsService.js"; import type { Entitlements } from "@/types/entitlements.js"; +import type { UserReturnable } from "@/types/domain/index.js"; export interface IMeService { - me(userId: string): Promise; + me(userId: string): Promise; getEntitlements(orgId: string): Promise; getPermissions(user: any): Promise<{ org: string[]; @@ -32,7 +31,7 @@ class MeService implements IMeService { this.entitlementsProvider = entitlementsProvider; } - me = async (userId: string): Promise => { + me = async (userId: string): Promise => { // Need to get teamIds const user = await User.findById(userId); if (!user) { @@ -85,7 +84,7 @@ class MeService implements IMeService { const entitlements: Entitlements = await this.entitlementsProvider.getForOrg(org._id.toString()); - const returnableUser: IUserReturnable = { + const returnableUser: UserReturnable = { id: user._id.toString(), email: user.email, firstName: user.firstName, diff --git a/server/src/services/business/NotificationChannelService.ts b/server/src/services/business/NotificationChannelService.ts index 702083f31..eeb774607 100644 --- a/server/src/services/business/NotificationChannelService.ts +++ b/server/src/services/business/NotificationChannelService.ts @@ -1,30 +1,30 @@ import mongoose from "mongoose"; import { - IUserContext, INotificationChannel, NotificationChannel, Monitor, } from "@/db/models/index.js"; import ApiError from "@/utils/ApiError.js"; +import type { UserContext } from "@/types/domain/index.js"; const SERVICE_NAME = "NotificationChannelService"; export interface INotificationChannelService { create: ( - tokenizedUser: IUserContext, + tokenizedUser: UserContext, notificationChannel: INotificationChannel ) => Promise; getAll: (teamId: string) => Promise; get: (teamId: string, id: string) => Promise; toggleActive: ( teamId: string, - tokenizedUser: IUserContext, + tokenizedUser: UserContext, id: string ) => Promise; update: ( teamId: string, - tokenizedUser: IUserContext, + tokenizedUser: UserContext, id: string, updateData: Partial ) => Promise; @@ -39,7 +39,7 @@ class NotificationChannelService implements INotificationChannelService { } create = async ( - userContext: IUserContext, + userContext: UserContext, notificationChannelData: INotificationChannel ) => { const data: INotificationChannel = { @@ -68,7 +68,7 @@ class NotificationChannelService implements INotificationChannelService { toggleActive = async ( teamId: string, - userContext: IUserContext, + userContext: UserContext, id: string ) => { const updatedChannel = await NotificationChannel.findOneAndUpdate( @@ -92,7 +92,7 @@ class NotificationChannelService implements INotificationChannelService { update = async ( teamId: string, - userContext: IUserContext, + userContext: UserContext, id: string, updateData: Partial ) => { diff --git a/server/src/services/business/StatusPageService.ts b/server/src/services/business/StatusPageService.ts index aaa591d87..ec0a8209f 100644 --- a/server/src/services/business/StatusPageService.ts +++ b/server/src/services/business/StatusPageService.ts @@ -1,21 +1,13 @@ import mongoose from "mongoose"; -import { - IUserContext, - IStatusPage, - StatusPage, - Monitor, - Check, - IMonitor, -} from "@/db/models/index.js"; +import { IStatusPage, StatusPage, Monitor, Check } from "@/db/models/index.js"; import ApiError from "@/utils/ApiError.js"; -import { count } from "node:console"; - +import type { UserContext } from "@/types/domain/index.js"; const SERVICE_NAME = "StatusPageService"; export interface IStatusPageService { create: ( - tokenizedUser: IUserContext, + tokenizedUser: UserContext, statusPage: IStatusPage ) => Promise; getAll: ( @@ -32,7 +24,7 @@ export interface IStatusPageService { ) => Promise<{ statusPage: IStatusPage; checksMap: Record }>; update: ( teamId: string, - tokenizedUser: IUserContext, + tokenizedUser: UserContext, id: string, updateData: Partial ) => Promise; @@ -46,7 +38,7 @@ class StatusPageService implements IStatusPageService { this.SERVICE_NAME = SERVICE_NAME; } - create = async (userContext: IUserContext, statusPageData: IStatusPage) => { + create = async (userContext: UserContext, statusPageData: IStatusPage) => { const monitorIds = statusPageData.monitors || []; const count = await Monitor.countDocuments({ _id: { $in: monitorIds }, @@ -153,7 +145,7 @@ class StatusPageService implements IStatusPageService { update = async ( teamId: string, - userContext: IUserContext, + userContext: UserContext, id: string, updateData: Partial ) => { diff --git a/server/src/services/business/TeamService.ts b/server/src/services/business/TeamService.ts index 6f09e5240..1e1f2d81f 100644 --- a/server/src/services/business/TeamService.ts +++ b/server/src/services/business/TeamService.ts @@ -2,18 +2,14 @@ import mongoose from "mongoose"; import { Monitor, Role, - IRole, ITeam, Team, TeamMembership, - OrgMembership, - IUserContext, } from "@/db/models/index.js"; import type { IMonitorRepository } from "@/repositories/index.js"; import type { IJobQueue } from "../infrastructure/JobQueue.js"; import ApiError from "@/utils/ApiError.js"; -import { PERMISSIONS } from "@/types/permissions.js"; - +import type { UserContext } from "@/types/domain/index.js"; const SERVICE_NAME = "TeamService"; export interface ITeamService { create: ( @@ -26,7 +22,7 @@ export interface ITeamService { getJoined: (teamId: string, orgId: string) => Promise>; get: (teamId: string, orgId: string) => Promise; update: ( - user: IUserContext, + user: UserContext, teamId: string, teamData: Partial ) => Promise; @@ -115,7 +111,7 @@ class TeamService implements ITeamService { }; update = async ( - user: IUserContext, + user: UserContext, teamId: string, teamData: Partial ) => { diff --git a/server/src/types/domain/teamMembership.ts b/server/src/types/domain/teamMembership.ts index 571b41aaf..3676eff31 100644 --- a/server/src/types/domain/teamMembership.ts +++ b/server/src/types/domain/teamMembership.ts @@ -7,3 +7,14 @@ export interface TeamMembership { createdAt: Date; updatedAt: Date; } + +export interface TeamMembershipWithDetails extends TeamMembership { + role: { + id: string; + organizationId: string; + name: string; + permissions: string[]; + createdAt: Date; + updatedAt: Date; + }; +} diff --git a/server/src/types/domain/user.ts b/server/src/types/domain/user.ts index 8a82cb9b6..6c1a4f61a 100644 --- a/server/src/types/domain/user.ts +++ b/server/src/types/domain/user.ts @@ -1,3 +1,5 @@ +import { Team, Role } from "@/types/domain/index.js"; +import type { Entitlements } from "@/types/entitlements.js"; export interface User { id: string; firstName: string; @@ -8,3 +10,40 @@ export interface User { createdAt: Date; updatedAt: Date; } + +export interface TokenizedUser { + sub: string; + email: string; + orgId: string; +} +export interface UserContext { + sub: string; + email: string; + orgId: string; + teamIds?: string[]; + teams?: Team[]; + currentTeamId?: string; + roles?: { + orgRole?: Role; + teamRole: Role; + }; + entitlements: Entitlements; +} + +export interface UserReturnable { + id: string; + email: string; + firstName: string; + lastName: string; + org: { + name: string; + planKey: string; + permissions: string[]; + }; + teams: { + id: string; + name: string; + permissions: string[]; + }[]; + entitlements: Entitlements; +} diff --git a/server/src/utils/JWTUtils.ts b/server/src/utils/JWTUtils.ts index bc9cc1965..c27d6a18a 100644 --- a/server/src/utils/JWTUtils.ts +++ b/server/src/utils/JWTUtils.ts @@ -1,8 +1,7 @@ import jwt from "jsonwebtoken"; import { AuthResult } from "@/services/business/AuthService.js"; -import { IUserContext } from "@/db/models/index.js"; import bcrypt from "bcryptjs"; - +import type { UserContext } from "@/types/domain/index.js"; export const encode = (data: AuthResult): string => { const secret = process.env.JWT_SECRET; if (!secret) { @@ -12,12 +11,12 @@ export const encode = (data: AuthResult): string => { return token; }; -export const decode = (token: string): IUserContext => { +export const decode = (token: string): UserContext => { const secret = process.env.JWT_SECRET; if (!secret) { throw new Error("JWT_SECRET is not defined"); } - const decoded = jwt.verify(token, secret) as IUserContext; + const decoded = jwt.verify(token, secret) as UserContext; return decoded; };