diff --git a/client/src/components/auth-verifier/index.tsx b/client/src/components/auth-verifier/index.tsx index d0dd2e9d6..fe1a316c5 100644 --- a/client/src/components/auth-verifier/index.tsx +++ b/client/src/components/auth-verifier/index.tsx @@ -32,7 +32,7 @@ export const AuthVerifier = ({ children }: { children: ReactNode }) => { } const verify = async () => { try { - const res = await get>("/auth/me"); + const res = await get>("/me"); const user: IUser = res.data.data; dispatch(setAuthenticated(true)); dispatch(setUser(user)); diff --git a/client/src/pages/billing/Billing.tsx b/client/src/pages/billing/Billing.tsx index 6406df917..c333567c9 100644 --- a/client/src/pages/billing/Billing.tsx +++ b/client/src/pages/billing/Billing.tsx @@ -21,7 +21,7 @@ const BillingPage = () => { {} ); const { response: meResponse } = useGet>( - "/auth/me", + "/me", {}, { refreshInterval: 10000 } ); diff --git a/client/src/pages/diagnostic/DiagnosticsAccessTab.tsx b/client/src/pages/diagnostic/DiagnosticsAccessTab.tsx index 1bc176f67..91c4f2057 100644 --- a/client/src/pages/diagnostic/DiagnosticsAccessTab.tsx +++ b/client/src/pages/diagnostic/DiagnosticsAccessTab.tsx @@ -2,12 +2,12 @@ import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import { useTheme } from "@mui/material/styles"; import Divider from "@mui/material/Divider"; -import Box from "@mui/material/Box"; -import { StatBox, Table } from "@/components/design-elements"; +import { Table } from "@/components/design-elements"; import type { Header } from "@/components/design-elements/Table"; import { useGet } from "@/hooks/UseApi"; import { useAppSelector } from "@/hooks/AppHooks"; import type { Entitlements } from "@/types/entitlements"; +import { config } from "@/config/index"; type PermissionsResponse = { message: string; @@ -32,18 +32,18 @@ export default function DiagnosticsAccessTab() { const { response: permsRes } = useGet( "/me/permissions", {}, - { refreshInterval: 10000 } + { refreshInterval: config.GLOBAL_REFRESH } ); - const { response: entsRes } = useGet<{ entitlements: Entitlements }>( + const { response: entsRes } = useGet<{ message: string; data: Entitlements }>( "/me/entitlements", {}, - { refreshInterval: 30000 } + { refreshInterval: config.GLOBAL_REFRESH } ); const orgPerms = permsRes?.data?.org || []; const teamPerms = permsRes?.data?.team || []; - const entitlementsObj = entsRes?.entitlements as Entitlements | undefined; + const entitlementsObj = entsRes?.data as Entitlements | undefined; const entitlements: EntitlementRow[] = entitlementsObj ? (Object.entries(entitlementsObj) as [keyof Entitlements, any][]).map( ([key, value]) => ({ id: String(key), key: String(key), value }) diff --git a/server/src/controllers/MeController.ts b/server/src/controllers/MeController.ts new file mode 100644 index 000000000..2b113f8b4 --- /dev/null +++ b/server/src/controllers/MeController.ts @@ -0,0 +1,62 @@ +import { Request, Response, NextFunction } from "express"; +import type { IMeService } from "@/services/business/MeService.js"; +import ApiError from "@/utils/ApiError.js"; + +export interface IMeController { + me: (req: Request, res: Response, next: NextFunction) => Promise; + getEntitlements: ( + req: Request, + res: Response, + next: NextFunction + ) => Promise; + getPermissions: ( + req: Request, + res: Response, + next: NextFunction + ) => Promise; +} + +class MeController implements IMeController { + private meService: IMeService; + + constructor(meService: IMeService) { + this.meService = meService; + } + + me = async (req: Request, res: Response, next: NextFunction) => { + try { + const user = req.user; + if (!user) { + throw new ApiError("Unauthorized", 401); + } + const returnableUser = await this.meService.me(user.sub); + res + .status(200) + .json({ message: "User retrieved successfully", data: returnableUser }); + } catch (error) { + next(error); + } + }; + + getEntitlements = async (req: Request, res: Response, next: NextFunction) => { + try { + const orgId = req.user?.orgId; + if (!orgId) throw new ApiError("Missing org context", 400); + const entitlements = await this.meService.getEntitlements(orgId); + res.status(200).json({ message: "OK", data: entitlements }); + } catch (error) { + next(error); + } + }; + + getPermissions = async (req: Request, res: Response, next: NextFunction) => { + try { + const data = await this.meService.getPermissions((req as any).user); + res.status(200).json({ message: "OK", data }); + } catch (error) { + next(error); + } + }; +} + +export default MeController; diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts index d7e6da080..0180b1360 100644 --- a/server/src/controllers/index.ts +++ b/server/src/controllers/index.ts @@ -16,3 +16,5 @@ export { default as IncidentsController } from "./IncidentsController.js"; export { default as SettingsController } from "./SettingsController.js"; export { default as BillingController } from "./BillingController.js"; export { default as StripeController } from "./StripeController.js"; +export { default as MeController } from "./MeController.js"; +export type { IMeController } from "./MeController.js"; diff --git a/server/src/index.ts b/server/src/index.ts index a2ffc167c..b01cb4035 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -36,6 +36,8 @@ const createApp = async () => { }) ); const routes = initRoutes(controllers, app); + // expose services for route-level access (e.g., in me routes) + app.set("services", services); // Swagger UI at /api-docs try { // Load OpenAPI doc from project root within server folder diff --git a/server/src/init/controllers.ts b/server/src/init/controllers.ts index 91fe2af6e..1d2f4f305 100644 --- a/server/src/init/controllers.ts +++ b/server/src/init/controllers.ts @@ -18,6 +18,7 @@ import { StripeController, BillingController, } from "@/controllers/index.js"; +import MeController from "@/controllers/MeController.js"; export const initControllers = (services: any) => { const controllers: Record = {}; @@ -80,6 +81,7 @@ export const initControllers = (services: any) => { controllers.billingController = new BillingController( services.billingService ); + controllers.meController = new MeController(services.meService); return controllers; }; diff --git a/server/src/init/routes.ts b/server/src/init/routes.ts index 5b419c6a0..59e1131b6 100644 --- a/server/src/init/routes.ts +++ b/server/src/init/routes.ts @@ -49,7 +49,7 @@ export const initRoutes = (controllers: any, app: Express) => { const profileRoutes = new ProfileRoutes(controllers.profileController); const incidentsRoutes = new IncidentsRoutes(controllers.incidentsController); const settingsRoutes = new SettingsRoutes(controllers.settingsController); - const meRoutes = new MeRoutes(); + const meRoutes = new MeRoutes(controllers.meController); const stripeRoutes = new StripeRoutes(controllers.stripeController); const billingRoutes = new BillingRoutes(controllers.billingController); diff --git a/server/src/init/services.ts b/server/src/init/services.ts index 8db628453..880caf24c 100644 --- a/server/src/init/services.ts +++ b/server/src/init/services.ts @@ -32,6 +32,7 @@ import { BillingService, } from "@/services/index.js"; import { EntitlementsFactory } from "@/services/system/EntitlementsService.js"; +import { MeService } from "@/services/index.js"; export const initServices = async () => { const checkService = new CheckService(); @@ -67,6 +68,7 @@ export const initServices = async () => { const jobQueue = await JobQueue.create(jobGenerator); const entitlementsProvider = EntitlementsFactory.create(); const authService = new AuthService(jobQueue, entitlementsProvider); + const meService = new MeService(entitlementsProvider); const monitorService = new MonitorService(jobQueue); const queueService = new QueueService(jobQueue); const teamService = new TeamService(jobQueue); @@ -92,6 +94,7 @@ export const initServices = async () => { jobGenerator, jobQueue, authService, + meService, monitorService, queueService, teamService, diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 29835ed97..02edc6106 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -35,7 +35,7 @@ class AuthRoutes { this.controller.login ); this.router.post("/logout", this.controller.logout); - this.router.get("/me", verifyToken, this.controller.me); + // this.router.get("/me", verifyToken, this.controller.me); this.router.post("/cleanup", this.controller.cleanup); this.router.post("/cleanup-monitors", this.controller.cleanMonitors); }; diff --git a/server/src/routes/me.ts b/server/src/routes/me.ts index bd1a2c01b..9f383649b 100644 --- a/server/src/routes/me.ts +++ b/server/src/routes/me.ts @@ -1,39 +1,32 @@ import { Router } from "express"; import { verifyToken } from "@/middleware/VerifyToken.js"; import { addUserContext } from "@/middleware/AddUserContext.js"; +import { MeController } from "@/controllers/index.js"; class MeRoutes { private router; - constructor() { + private controller: MeController; + constructor(meController: MeController) { this.router = Router(); + this.controller = meController; this.initRoutes(); } initRoutes = () => { + this.router.get("/", verifyToken, this.controller.me); this.router.get( "/entitlements", verifyToken, addUserContext, - (req, res) => { - const entitlements = req.entitlements || {}; - return res.json({ entitlements }); - } + this.controller.getEntitlements ); - this.router.get("/permissions", verifyToken, addUserContext, (req, res) => { - const orgPerms = req.user?.roles?.orgRole?.permissions || []; - const teamPerms = req.user?.roles?.teamRole?.permissions || []; - const currentTeamId = req.user?.currentTeamId; - return res.json({ - message: "OK", - data: { - org: orgPerms, - team: Array.isArray(teamPerms) - ? teamPerms.map((p: string) => ({ teamId: currentTeamId, permission: p })) - : [], - }, - }); - }); + this.router.get( + "/permissions", + verifyToken, + addUserContext, + this.controller.getPermissions + ); }; getRouter() { diff --git a/server/src/services/business/AuthService.ts b/server/src/services/business/AuthService.ts index 1764515ee..2e85a57b8 100644 --- a/server/src/services/business/AuthService.ts +++ b/server/src/services/business/AuthService.ts @@ -42,7 +42,6 @@ export type LoginData = { export type AuthResult = Partial; export interface IAuthService { - me(userId: string): Promise; register(signupData: RegisterData): Promise<{ tokenizedUser: ITokenizedUser; returnableUser: IUserReturnable; @@ -69,80 +68,15 @@ class AuthService implements IAuthService { public SERVICE_NAME: string; private jobQueue: IJobQueue; private entitlementsProvider: IEntitlementsProvider; - constructor(jobQueue: IJobQueue, entitlementsProvider: IEntitlementsProvider) { + constructor( + jobQueue: IJobQueue, + entitlementsProvider: IEntitlementsProvider + ) { this.SERVICE_NAME = SERVICE_NAME; this.jobQueue = jobQueue; this.entitlementsProvider = entitlementsProvider; } - me = async (userId: string) => { - // Need to get teamIds - const user = await User.findById(userId); - if (!user) { - throw new ApiError("User not found", 404); - } - - // Find OrgMembership - const orgMembership = await OrgMembership.findOne({ - userId: user._id, - }).lean(); - if (!orgMembership) { - throw new ApiError("User is not part of any organization"); - } - - // Find org and roles - const org = await Org.findById(orgMembership.orgId).lean(); - - if (!org) { - throw new ApiError("Organization not found"); - } - - const orgRoles = await Role.findById(orgMembership.roleId).lean(); - - // Get teams - const teamMembershipWithRoles = await TeamMembership.find({ - userId: user._id, - }) - .populate<{ roleId: IRole }>("roleId") - .lean(); - - const teamIds = teamMembershipWithRoles.map((tm) => tm.teamId.toString()); - - const teams = await this.getTeams(teamIds); - const teamMap = new Map(teams.map((t) => [t._id.toString(), t])); - const returnableTeams = teamMembershipWithRoles.map((tm) => { - const team = teamMap.get(tm.teamId.toString()); - if (!team) { - throw new ApiError("Team not found for membership"); - } - return { - id: team._id.toString(), - name: team.name, - permissions: tm.roleId?.permissions ?? [], - }; - }); - - const entitlements: Entitlements = await this.entitlementsProvider.getForOrg( - org._id.toString() - ); - - const returnableUser: IUserReturnable = { - id: user._id.toString(), - email: user.email, - firstName: user.firstName, - lastName: user.lastName, - org: { - name: org.name, - planKey: org.planKey, - permissions: orgRoles?.permissions || [], - }, - teams: returnableTeams, - entitlements, - }; - - return returnableUser; - }; - async register(signupData: RegisterData) { const passwordHash = await hashPassword(signupData.password); @@ -263,10 +197,8 @@ class AuthService implements IAuthService { permissions: roles[2]?.permissions || [], }; - const provider = EntitlementsFactory.create(); - const entitlements: Entitlements = await provider.getForOrg( - org._id.toString() - ); + const entitlements: Entitlements = + await this.entitlementsProvider.getForOrg(org._id.toString()); const returnableUser: IUserReturnable = { id: user._id.toString(), @@ -405,10 +337,8 @@ class AuthService implements IAuthService { const orgRole = await Role.findById(orgMembership.roleId); const orgPermissions = orgRole?.permissions || []; - const provider = EntitlementsFactory.create(); - const entitlements: Entitlements = await provider.getForOrg( - org._id.toString() - ); + const entitlements: Entitlements = + await this.entitlementsProvider.getForOrg(org._id.toString()); const returnableUser: IUserReturnable = { id: user._id.toString(), email: user.email, @@ -516,9 +446,8 @@ class AuthService implements IAuthService { orgId: orgMembership.orgId.toString(), }; - const entitlements: Entitlements = await this.entitlementsProvider.getForOrg( - org._id.toString() - ); + const entitlements: Entitlements = + await this.entitlementsProvider.getForOrg(org._id.toString()); const returnableUser: IUserReturnable = { id: user._id.toString(), diff --git a/server/src/services/business/MeService.ts b/server/src/services/business/MeService.ts new file mode 100644 index 000000000..6a64bd7f1 --- /dev/null +++ b/server/src/services/business/MeService.ts @@ -0,0 +1,122 @@ +import { + User, + Org, + OrgMembership, + Team, + ITeam, + IUserReturnable, + TeamMembership, + Role, + IRole, +} from "@/db/models/index.js"; +import ApiError from "@/utils/ApiError.js"; +import type { IEntitlementsProvider } from "@/services/system/EntitlementsService.js"; +import type { Entitlements } from "@/types/entitlements.js"; + +export interface IMeService { + me(userId: string): Promise; + getEntitlements(orgId: string): Promise; + getPermissions(user: any): Promise<{ + org: string[]; + team: { teamId: string; permission: string }[]; + }>; +} + +const SERVICE_NAME = "MeService"; + +class MeService implements IMeService { + public SERVICE_NAME = SERVICE_NAME; + private entitlementsProvider: IEntitlementsProvider; + + constructor(entitlementsProvider: IEntitlementsProvider) { + this.entitlementsProvider = entitlementsProvider; + } + + me = async (userId: string): Promise => { + // Need to get teamIds + const user = await User.findById(userId); + if (!user) { + throw new ApiError("User not found", 404); + } + + // Find OrgMembership + const orgMembership = await OrgMembership.findOne({ + userId: user._id, + }).lean(); + if (!orgMembership) { + throw new ApiError("User is not part of any organization"); + } + + // Find org and roles + const org = await Org.findById(orgMembership.orgId).lean(); + + if (!org) { + throw new ApiError("Organization not found"); + } + + const orgRoles = await Role.findById(orgMembership.roleId).lean(); + + // Get teams + const teamMembershipWithRoles = await TeamMembership.find({ + userId: user._id, + }) + .populate<{ roleId: IRole }>("roleId") + .lean(); + + const teamIds = teamMembershipWithRoles.map((tm) => tm.teamId.toString()); + + const teams = await Team.find({ _id: { $in: teamIds } }).select( + "_id, name" + ); + + const teamMap = new Map(teams.map((t) => [t._id.toString(), t])); + const returnableTeams = teamMembershipWithRoles.map((tm) => { + const team = teamMap.get(tm.teamId.toString()); + if (!team) { + throw new ApiError("Team not found for membership"); + } + return { + id: team._id.toString(), + name: team.name, + permissions: tm.roleId?.permissions ?? [], + }; + }); + + const entitlements: Entitlements = + await this.entitlementsProvider.getForOrg(org._id.toString()); + + const returnableUser: IUserReturnable = { + id: user._id.toString(), + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + org: { + name: org.name, + planKey: org.planKey, + permissions: orgRoles?.permissions || [], + }, + teams: returnableTeams, + entitlements, + }; + + return returnableUser; + }; + + getEntitlements = async (orgId: string): Promise => { + return this.entitlementsProvider.getForOrg(orgId); + }; + + getPermissions = async (user: any) => { + const orgPerms: string[] = user?.roles?.orgRole?.permissions || []; + const teamPermsArr: string[] = user?.roles?.teamRole?.permissions || []; + const currentTeamId: string = user?.currentTeamId || ""; + return { + org: orgPerms, + team: Array.isArray(teamPermsArr) + ? teamPermsArr.map((p) => ({ teamId: currentTeamId, permission: p })) + : [], + }; + }; +} + +export default MeService; diff --git a/server/src/services/index.ts b/server/src/services/index.ts index d6812d452..5863ebe72 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -14,6 +14,7 @@ export { default as StatusPageService } from "@/services/business/StatusPageServ export { default as DiagnosticService } from "@/services/business/DiagnosticService.js"; export { default as RecoveryService } from "@/services/business/RecoveryService.js"; export { default as IncidentService } from "@/services/business/IncidentService.js"; +export { default as MeService } from "@/services/business/MeService.js"; export { default as NetworkService } from "@/services/infrastructure/NetworkService.js"; export { default as NotificationService } from "@/services/infrastructure/NotificationService.js";