refactor /auth/me into /me

This commit is contained in:
Alex Holliday committed 2025-12-14 17:16:21 +00:00
1 parent 4b92b2a8df
commit 1a2d11e6de
14 files changed
+226 -110

No files matched your search

@@ -32,7 +32,7 @@ export const AuthVerifier = ({ children }: { children: ReactNode }) => {
}
const verify = async () => {
try {
const res = await get<ApiResponse<IUser>>("/auth/me");
const res = await get<ApiResponse<IUser>>("/me");
const user: IUser = res.data.data;
dispatch(setAuthenticated(true));
dispatch(setUser(user));
+1 -1
View File
@@ -21,7 +21,7 @@ const BillingPage = () => {
{}
);
const { response: meResponse } = useGet<ApiResponse<IUser>>(
"/auth/me",
"/me",
{},
{ refreshInterval: 10000 }
);
@@ -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<PermissionsResponse>(
"/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 })
+62
View File
@@ -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<void>;
getEntitlements: (
req: Request,
res: Response,
next: NextFunction
) => Promise<void>;
getPermissions: (
req: Request,
res: Response,
next: NextFunction
) => Promise<void>;
}
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;
+2
View File
@@ -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";
+2
View File
@@ -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
+2
View File
@@ -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<string, any> = {};
@@ -80,6 +81,7 @@ export const initControllers = (services: any) => {
controllers.billingController = new BillingController(
services.billingService
);
controllers.meController = new MeController(services.meService);
return controllers;
};
+1 -1
View File
@@ -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);
+3
View File
@@ -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,
+1 -1
View File
@@ -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);
};
+12 -19
View File
@@ -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() {
+10 -81
View File
@@ -42,7 +42,6 @@ export type LoginData = {
export type AuthResult = Partial<ITokenizedUser>;
export interface IAuthService {
me(userId: string): Promise<IUserReturnable>;
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(),
+122
View File
@@ -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<IUserReturnable>;
getEntitlements(orgId: string): Promise<Entitlements>;
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<IUserReturnable> => {
// 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<Entitlements> => {
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;
+1
View File
@@ -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";