diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 890982f6..7d6f26e0 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -26,8 +26,24 @@ export enum ActionsEnum { getTarget = "getTarget", listTargets = "listTargets", updateTarget = "updateTarget", + createRole = "createRole", + deleteRole = "deleteRole", + getRole = "getRole", + listRoles = "listRoles", + updateRole = "updateRole", deleteUser = "deleteUser", - listUsers = "listUsers" + listUsers = "listUsers", + listSiteRoles = "listSiteRoles", + listUserRoles = "listUserRoles", + listResourceRoles = "listResourceRoles", + addRoleSite = "addRoleSite", + addRoleResource = "addRoleResource", + removeRoleResource = "removeRoleResource", + removeRoleSite = "removeRoleSite", + addRoleAction = "addRoleAction", + removeRoleAction = "removeRoleAction", + listRoleSites = "listRoleSites", + listRoleResources = "listRoleResources", } export async function checkUserActionPermission(actionId: string, req: Request): Promise { diff --git a/server/routers/auth/index.ts b/server/routers/auth/index.ts index 94d0ce62..ed558a33 100644 --- a/server/routers/auth/index.ts +++ b/server/routers/auth/index.ts @@ -9,8 +9,11 @@ export * from "./getUserOrgs"; export * from "./verifySiteAccess"; export * from "./verifyResourceAccess"; export * from "./verifyTargetAccess"; +export * from "./verifyRoleAccess"; +export * from "./verifySuperuser"; export * from "./verifyEmail"; export * from "./requestEmailVerificationCode"; export * from "./changePassword"; export * from "./requestPasswordReset"; export * from "./resetPassword"; +export * from "./verifyUserInRole"; \ No newline at end of file diff --git a/server/routers/auth/verifyRoleAccess.ts b/server/routers/auth/verifyRoleAccess.ts new file mode 100644 index 00000000..70418de2 --- /dev/null +++ b/server/routers/auth/verifyRoleAccess.ts @@ -0,0 +1,50 @@ +import { Request, Response, NextFunction } from 'express'; +import { db } from '@server/db'; +import { roles, userOrgs } from '@server/db/schema'; +import { and, eq } from 'drizzle-orm'; +import createHttpError from 'http-errors'; +import HttpCode from '@server/types/HttpCode'; +import logger from '@server/logger'; + +export async function verifyRoleAccess(req: Request, res: Response, next: NextFunction) { + const userId = req.user?.id; // Assuming you have user information in the request + const roleId = parseInt(req.params.roleId || req.body.roleId || req.query.roleId); + + if (!userId) { + return next(createHttpError(HttpCode.UNAUTHORIZED, 'User not authenticated')); + } + + if (isNaN(roleId)) { + return next(createHttpError(HttpCode.BAD_REQUEST, 'Invalid role ID')); + } + + try { + // Check if the role exists and belongs to the specified organization + const role = await db.select() + .from(roles) + .where(eq(roles.roleId, roleId)) + .limit(1); + + if (role.length === 0) { + return next(createHttpError(HttpCode.NOT_FOUND, `Role with ID ${roleId} not found`)); + } + + // Check if the user has a role in the organization + const userOrgRole = await db.select() + .from(userOrgs) + .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, role[0].orgId!))) + .limit(1); + + if (userOrgRole.length === 0) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have access to this organization')); + } + + req.userOrgRoleId = userOrgRole[0].roleId; + req.userOrgId = userOrgRole[0].orgId; + + return next(); + } catch (error) { + logger.error('Error verifying role access:', error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, 'Error verifying role access')); + } +} \ No newline at end of file diff --git a/server/routers/auth/verifySuperuser.ts b/server/routers/auth/verifySuperuser.ts new file mode 100644 index 00000000..0a4b311e --- /dev/null +++ b/server/routers/auth/verifySuperuser.ts @@ -0,0 +1,48 @@ +import { Request, Response, NextFunction } from 'express'; +import { db } from '@server/db'; +import { roles, userOrgs } from '@server/db/schema'; +import { and, eq } from 'drizzle-orm'; +import createHttpError from 'http-errors'; +import HttpCode from '@server/types/HttpCode'; +import logger from '@server/logger'; + +export async function verifySuperuser(req: Request, res: Response, next: NextFunction) { + const userId = req.user?.id; // Assuming you have user information in the request + const orgId = req.userOrgId; + + if (!userId) { + return next(createHttpError(HttpCode.UNAUTHORIZED, 'User does not have orgId')); + } + + if (!userId) { + return next(createHttpError(HttpCode.UNAUTHORIZED, 'User not authenticated')); + } + + try { + // Check if the user has a role in the organization + const userOrgRole = await db.select() + .from(userOrgs) + .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId!))) + .limit(1); + + if (userOrgRole.length === 0) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have access to this organization')); + } + + // get userOrgRole[0].roleId + // Check if the user's role in the organization is a superuser role + const userRole = await db.select() + .from(roles) + .where(eq(roles.roleId, userOrgRole[0].roleId)) + .limit(1); + + if (userRole.length === 0 || !userRole[0].isSuperuserRole) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have superuser access')); + } + + return next(); + } catch (error) { + logger.error('Error verifying role access:', error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, 'Error verifying role access')); + } +} \ No newline at end of file diff --git a/server/routers/auth/verifyUserInRole.ts b/server/routers/auth/verifyUserInRole.ts new file mode 100644 index 00000000..b38baea5 --- /dev/null +++ b/server/routers/auth/verifyUserInRole.ts @@ -0,0 +1,31 @@ +import { Request, Response, NextFunction } from 'express'; +import { db } from '@server/db'; +import { roles, userOrgs } from '@server/db/schema'; +import { and, eq } from 'drizzle-orm'; +import createHttpError from 'http-errors'; +import HttpCode from '@server/types/HttpCode'; +import logger from '@server/logger'; + +export async function verifyUserInRole(req: Request, res: Response, next: NextFunction) { + try { + const roleId = parseInt(req.params.roleId || req.body.roleId || req.query.roleId); + const userRoleId = req.userOrgRoleId; + + if (isNaN(roleId)) { + return next(createHttpError(HttpCode.BAD_REQUEST, 'Invalid role ID')); + } + + if (!userRoleId) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have access to this organization')); + } + + if (userRoleId !== roleId) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have access to this role')); + } + + return next(); + } catch (error) { + logger.error('Error verifying role access:', error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, 'Error verifying role access')); + } +} \ No newline at end of file diff --git a/server/routers/external.ts b/server/routers/external.ts index 38744805..503a6b66 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -5,6 +5,7 @@ import * as resource from "./resource"; import * as target from "./target"; import * as user from "./user"; import * as auth from "./auth"; +import * as role from "./role"; import HttpCode from "@server/types/HttpCode"; import { rateLimitMiddleware, @@ -17,6 +18,9 @@ import { verifySiteAccess, verifyResourceAccess, verifyTargetAccess, + verifyRoleAccess, + verifySuperuser, + verifyUserInRole } from "./auth"; // Root routes @@ -39,6 +43,7 @@ authenticated.delete("/org/:orgId", verifyOrgAccess, org.deleteOrg); authenticated.put("/org/:orgId/site", verifyOrgAccess, site.createSite); authenticated.get("/org/:orgId/sites", verifyOrgAccess, site.listSites); authenticated.get("/site/:siteId", verifySiteAccess, site.getSite); +authenticated.get("/site/:siteId/roles", verifySiteAccess, site.listSiteRoles); authenticated.post("/site/:siteId", verifySiteAccess, site.updateSite); authenticated.delete("/site/:siteId", verifySiteAccess, site.deleteSite); @@ -53,6 +58,11 @@ authenticated.get( verifyOrgAccess, resource.listResources, ); +authenticated.get( + "/resource/:resourceId/roles", + verifyResourceAccess, + resource.listResourceRoles, +); authenticated.get( "/resource/:resourceId", verifyResourceAccess, @@ -91,9 +101,86 @@ authenticated.delete( target.deleteTarget, ); +authenticated.put( + "/org/:orgId/role", + verifyOrgAccess, + verifySuperuser, + role.createRole, +); +authenticated.get("/org/:orgId/roles", verifyOrgAccess, role.listRoles); +authenticated.get("/role/:roleId", verifyRoleAccess, verifyUserInRole, role.getRole); +authenticated.post( + "/role/:roleId", + verifyRoleAccess, + verifySuperuser, + role.updateRole, +); +authenticated.delete( + "/role/:roleId", + verifyRoleAccess, + verifySuperuser, + role.deleteRole, +); + +authenticated.put( + "/role/:roleId/site", + verifyRoleAccess, + verifyUserInRole, + role.addRoleSite, +); +authenticated.delete( + "/role/:roleId/site", + verifyRoleAccess, + verifyUserInRole, + role.removeRoleSite, +); +authenticated.get( + "/role/:roleId/sites", + verifyRoleAccess, + verifyUserInRole, + role.listRoleSites, +); +authenticated.put( + "/role/:roleId/resource", + verifyRoleAccess, + verifyUserInRole, + role.addRoleResource, +); +authenticated.delete( + "/role/:roleId/resource", + verifyRoleAccess, + verifyUserInRole, + role.removeRoleResource, +); +authenticated.get( + "/role/:roleId/resources", + verifyRoleAccess, + verifyUserInRole, + role.listRoleResources, +); +authenticated.put( + "/role/:roleId/action", + verifyRoleAccess, + verifyUserInRole, + role.addRoleAction, +); +authenticated.delete( + "/role/:roleId/action", + verifyRoleAccess, + verifyUserInRole, + role.removeRoleAction, +); +authenticated.get( + "/role/:roleId/actions", + verifyRoleAccess, + verifyUserInRole, + role.listRoleActions, +); + authenticated.get("/users", user.listUsers); // authenticated.get("/org/:orgId/users", user.???); // TODO: Implement this authenticated.get("/user", user.getUser); +authenticated.get("/user/roles", user.listUserRoles); // authenticated.get("/user/:userId", user.getUser); authenticated.delete("/user/:userId", user.deleteUser); diff --git a/server/routers/org/createOrg.ts b/server/routers/org/createOrg.ts index ae0afcc5..4c79ce89 100644 --- a/server/routers/org/createOrg.ts +++ b/server/routers/org/createOrg.ts @@ -41,7 +41,7 @@ export async function createOrg(req: Request, res: Response, next: NextFunction) // Check if the user has permission to list sites const hasPermission = await checkUserActionPermission(ActionsEnum.createOrg, req); if (!hasPermission) { - return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to list sites')); + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); } const { name, domain } = parsedBody.data; diff --git a/server/routers/org/deleteOrg.ts b/server/routers/org/deleteOrg.ts index e83196df..9cf7ba32 100644 --- a/server/routers/org/deleteOrg.ts +++ b/server/routers/org/deleteOrg.ts @@ -30,7 +30,7 @@ export async function deleteOrg(req: Request, res: Response, next: NextFunction) // Check if the user has permission to list sites const hasPermission = await checkUserActionPermission(ActionsEnum.deleteOrg, req); if (!hasPermission) { - return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to list sites')); + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); } const deletedOrg = await db.delete(orgs) diff --git a/server/routers/org/getOrg.ts b/server/routers/org/getOrg.ts index d9686a9f..71e8917d 100644 --- a/server/routers/org/getOrg.ts +++ b/server/routers/org/getOrg.ts @@ -30,7 +30,7 @@ export async function getOrg(req: Request, res: Response, next: NextFunction): P // Check if the user has permission to list sites const hasPermission = await checkUserActionPermission(ActionsEnum.getOrg, req); if (!hasPermission) { - return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to list sites')); + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); } const org = await db.select() diff --git a/server/routers/org/listOrgs.ts b/server/routers/org/listOrgs.ts index 820ed7ac..d93316e8 100644 --- a/server/routers/org/listOrgs.ts +++ b/server/routers/org/listOrgs.ts @@ -31,7 +31,7 @@ export async function listOrgs(req: Request, res: Response, next: NextFunction): // Check if the user has permission to list sites const hasPermission = await checkUserActionPermission(ActionsEnum.listOrgs, req); if (!hasPermission) { - return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to list sites')); + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); } // Use the userOrgs passed from the middleware diff --git a/server/routers/org/updateOrg.ts b/server/routers/org/updateOrg.ts index 666d1925..078dd8e1 100644 --- a/server/routers/org/updateOrg.ts +++ b/server/routers/org/updateOrg.ts @@ -49,7 +49,7 @@ export async function updateOrg(req: Request, res: Response, next: NextFunction) // Check if the user has permission to list sites const hasPermission = await checkUserActionPermission(ActionsEnum.updateOrg, req); if (!hasPermission) { - return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to list sites')); + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); } const updatedOrg = await db.update(orgs) diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index 5242327f..9a68d9f9 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -1,12 +1,13 @@ import { Request, Response, NextFunction } from 'express'; import { z } from 'zod'; import { db } from '@server/db'; -import { resources } from '@server/db/schema'; +import { resources, roleResources, roles, userResources } from '@server/db/schema'; import response from "@server/utils/response"; import HttpCode from '@server/types/HttpCode'; import createHttpError from 'http-errors'; import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import logger from '@server/logger'; +import { eq, and } from 'drizzle-orm'; const createResourceParamsSchema = z.object({ siteId: z.number().int().positive(), @@ -50,7 +51,11 @@ export async function createResource(req: Request, res: Response, next: NextFunc // Check if the user has permission to list sites const hasPermission = await checkUserActionPermission(ActionsEnum.createResource, req); if (!hasPermission) { - return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to list sites')); + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + if (!req.userOrgRoleId) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have a role')); } // Generate a unique resourceId @@ -65,6 +70,36 @@ export async function createResource(req: Request, res: Response, next: NextFunc subdomain, }).returning(); + + + // find the superuser roleId and also add the resource to the superuser role + const superuserRole = await db.select() + .from(roles) + .where(and(eq(roles.isSuperuserRole, true), eq(roles.orgId, orgId))) + .limit(1); + + if (superuserRole.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Superuser role not found` + ) + ); + } + + await db.insert(roleResources).values({ + roleId: superuserRole[0].roleId, + resourceId: newResource[0].resourceId, + }); + + if (req.userOrgRoleId != superuserRole[0].roleId) { + // make sure the user can access the resource + await db.insert(userResources).values({ + userId: req.user?.id!, + resourceId: newResource[0].resourceId, + }); + } + response(res, { data: newResource[0], success: true, diff --git a/server/routers/resource/deleteResource.ts b/server/routers/resource/deleteResource.ts index 27f72b86..c836d13c 100644 --- a/server/routers/resource/deleteResource.ts +++ b/server/routers/resource/deleteResource.ts @@ -32,7 +32,7 @@ export async function deleteResource(req: Request, res: Response, next: NextFunc // Check if the user has permission to list sites const hasPermission = await checkUserActionPermission(ActionsEnum.deleteResource, req); if (!hasPermission) { - return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to list sites')); + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); } // Delete the resource from the database diff --git a/server/routers/resource/getResource.ts b/server/routers/resource/getResource.ts index de37cd37..7bba2d74 100644 --- a/server/routers/resource/getResource.ts +++ b/server/routers/resource/getResource.ts @@ -32,7 +32,7 @@ export async function getResource(req: Request, res: Response, next: NextFunctio // Check if the user has permission to list sites const hasPermission = await checkUserActionPermission(ActionsEnum.getResource, req); if (!hasPermission) { - return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to list sites')); + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); } // Fetch the resource from the database diff --git a/server/routers/resource/index.ts b/server/routers/resource/index.ts index 42033ea6..46e4a9ff 100644 --- a/server/routers/resource/index.ts +++ b/server/routers/resource/index.ts @@ -2,4 +2,5 @@ export * from "./getResource"; export * from "./createResource"; export * from "./deleteResource"; export * from "./updateResource"; -export * from "./listResources"; \ No newline at end of file +export * from "./listResources"; +export * from "./listResourceRoles"; \ No newline at end of file diff --git a/server/routers/resource/listResourceRoles.ts b/server/routers/resource/listResourceRoles.ts new file mode 100644 index 00000000..56711710 --- /dev/null +++ b/server/routers/resource/listResourceRoles.ts @@ -0,0 +1,58 @@ +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import { db } from '@server/db'; +import { roleResources, roles } from '@server/db/schema'; +import { eq } from 'drizzle-orm'; +import response from "@server/utils/response"; +import HttpCode from '@server/types/HttpCode'; +import createHttpError from 'http-errors'; +import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; +import logger from '@server/logger'; + +const listResourceRolesSchema = z.object({ + resourceId: z.string(), +}); + +export async function listResourceRoles(req: Request, res: Response, next: NextFunction): Promise { + try { + const parsedParams = listResourceRolesSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedParams.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { resourceId } = parsedParams.data; + + // Check if the user has permission to list resource roles + const hasPermission = await checkUserActionPermission(ActionsEnum.listResourceRoles, req); + if (!hasPermission) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + const resourceRolesList = await db + .select({ + roleId: roles.roleId, + name: roles.name, + description: roles.description, + isSuperuserRole: roles.isSuperuserRole, + }) + .from(roleResources) + .innerJoin(roles, eq(roleResources.roleId, roles.roleId)) + .where(eq(roleResources.resourceId, resourceId)); + + return response(res, { + data: resourceRolesList, + success: true, + error: false, + message: "Resource roles retrieved successfully", + status: HttpCode.OK, + }); + } catch (error) { + logger.error(error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); + } +} \ No newline at end of file diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index 533aff4c..594c9665 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -43,7 +43,7 @@ export async function listResources(req: RequestWithOrgAndRole, res: Response, n // Check if the user has permission to list sites const hasPermission = await checkUserActionPermission(ActionsEnum.listResources, req); if (!hasPermission) { - return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to list sites')); + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); } if (orgId && orgId !== req.orgId) { diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index be7e72bd..9fb7f4ae 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -52,7 +52,7 @@ export async function updateResource(req: Request, res: Response, next: NextFunc // Check if the user has permission to list sites const hasPermission = await checkUserActionPermission(ActionsEnum.updateResource, req); if (!hasPermission) { - return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to list sites')); + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); } // Update the resource in the database diff --git a/server/routers/role/addRoleAction.ts b/server/routers/role/addRoleAction.ts new file mode 100644 index 00000000..1faa8175 --- /dev/null +++ b/server/routers/role/addRoleAction.ts @@ -0,0 +1,59 @@ +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import { db } from '@server/db'; +import { roleActions, roles } from '@server/db/schema'; +import response from "@server/utils/response"; +import HttpCode from '@server/types/HttpCode'; +import createHttpError from 'http-errors'; +import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; +import logger from '@server/logger'; + +const addRoleActionSchema = z.object({ + roleId: z.string().transform(Number).pipe(z.number().int().positive()), + actionId: z.string(), +}); + +export async function addRoleAction(req: Request, res: Response, next: NextFunction): Promise { + try { + const parsedBody = addRoleActionSchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedBody.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { roleId, actionId } = parsedBody.data; + + // Check if the user has permission to add role actions + const hasPermission = await checkUserActionPermission(ActionsEnum.addRoleAction, req); + if (!hasPermission) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + // Get the orgId for the role + const role = await db.select({ orgId: roles.orgId }).from(roles).where(eq(roles.roleId, roleId)).limit(1); + if (role.length === 0) { + return next(createHttpError(HttpCode.NOT_FOUND, `Role with ID ${roleId} not found`)); + } + + const newRoleAction = await db.insert(roleActions).values({ + roleId, + actionId, + orgId: role[0].orgId, + }).returning(); + + return response(res, { + data: newRoleAction[0], + success: true, + error: false, + message: "Action added to role successfully", + status: HttpCode.CREATED, + }); + } catch (error) { + logger.error(error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); + } +} \ No newline at end of file diff --git a/server/routers/role/addRoleResource.ts b/server/routers/role/addRoleResource.ts new file mode 100644 index 00000000..bf770f6e --- /dev/null +++ b/server/routers/role/addRoleResource.ts @@ -0,0 +1,52 @@ +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import { db } from '@server/db'; +import { roleResources } from '@server/db/schema'; +import response from "@server/utils/response"; +import HttpCode from '@server/types/HttpCode'; +import createHttpError from 'http-errors'; +import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; +import logger from '@server/logger'; + +const addRoleResourceSchema = z.object({ + roleId: z.string().transform(Number).pipe(z.number().int().positive()), + resourceId: z.string(), +}); + +export async function addRoleResource(req: Request, res: Response, next: NextFunction): Promise { + try { + const parsedBody = addRoleResourceSchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedBody.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { roleId, resourceId } = parsedBody.data; + + // Check if the user has permission to add role resources + const hasPermission = await checkUserActionPermission(ActionsEnum.addRoleResource, req); + if (!hasPermission) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + const newRoleResource = await db.insert(roleResources).values({ + roleId, + resourceId, + }).returning(); + + return response(res, { + data: newRoleResource[0], + success: true, + error: false, + message: "Resource added to role successfully", + status: HttpCode.CREATED, + }); + } catch (error) { + logger.error(error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); + } +} \ No newline at end of file diff --git a/server/routers/role/addRoleSite.ts b/server/routers/role/addRoleSite.ts new file mode 100644 index 00000000..7ac55e56 --- /dev/null +++ b/server/routers/role/addRoleSite.ts @@ -0,0 +1,79 @@ +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import { db } from '@server/db'; +import { resources, roleResources, roleSites } from '@server/db/schema'; +import response from "@server/utils/response"; +import HttpCode from '@server/types/HttpCode'; +import createHttpError from 'http-errors'; +import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; +import logger from '@server/logger'; +import { eq } from 'drizzle-orm'; + +const addRoleSiteParamsSchema = z.object({ + roleId: z.string().transform(Number).pipe(z.number().int().positive()), +}); + +const addRoleSiteSchema = z.object({ + siteId: z.string().transform(Number).pipe(z.number().int().positive()), +}); + +export async function addRoleSite(req: Request, res: Response, next: NextFunction): Promise { + try { + const parsedBody = addRoleSiteSchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedBody.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { siteId } = parsedBody.data; + + const parsedParams = addRoleSiteParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedParams.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { roleId } = parsedParams.data; + + // Check if the user has permission to add role sites + const hasPermission = await checkUserActionPermission(ActionsEnum.addRoleSite, req); + if (!hasPermission) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + const newRoleSite = await db.insert(roleSites).values({ + roleId, + siteId, + }).returning(); + + const siteResources = await db.select() + .from(resources) + .where(eq(resources.siteId, siteId)); + + for (const resource of siteResources) { + await db.insert(roleResources).values({ + roleId, + resourceId: resource.resourceId, + }); + } + + return response(res, { + data: newRoleSite[0], + success: true, + error: false, + message: "Site added to role successfully", + status: HttpCode.CREATED, + }); + } catch (error) { + logger.error(error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); + } +} \ No newline at end of file diff --git a/server/routers/role/createRole.ts b/server/routers/role/createRole.ts new file mode 100644 index 00000000..940f57e7 --- /dev/null +++ b/server/routers/role/createRole.ts @@ -0,0 +1,68 @@ +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import { db } from '@server/db'; +import { roles } from '@server/db/schema'; +import response from "@server/utils/response"; +import HttpCode from '@server/types/HttpCode'; +import createHttpError from 'http-errors'; +import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; +import logger from '@server/logger'; + +const createRoleParamsSchema = z.object({ + orgId: z.number().int().positive() +}); + +const createRoleSchema = z.object({ + name: z.string().min(1).max(255), + description: z.string().optional(), +}); + +export async function createRole(req: Request, res: Response, next: NextFunction): Promise { + try { + const parsedBody = createRoleSchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedBody.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const roleData = parsedBody.data; + + const parsedParams = createRoleParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedParams.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { orgId } = parsedParams.data; + + // Check if the user has permission to create roles + const hasPermission = await checkUserActionPermission(ActionsEnum.createRole, req); + if (!hasPermission) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + const newRole = await db.insert(roles).values({ + ...roleData, + orgId, + }).returning(); + + return response(res, { + data: newRole[0], + success: true, + error: false, + message: "Role created successfully", + status: HttpCode.CREATED, + }); + } catch (error) { + logger.error(error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); + } +} \ No newline at end of file diff --git a/server/routers/role/deleteRole.ts b/server/routers/role/deleteRole.ts new file mode 100644 index 00000000..f5b1225a --- /dev/null +++ b/server/routers/role/deleteRole.ts @@ -0,0 +1,83 @@ +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import { db } from '@server/db'; +import { roles } from '@server/db/schema'; +import { eq } from 'drizzle-orm'; +import response from "@server/utils/response"; +import HttpCode from '@server/types/HttpCode'; +import createHttpError from 'http-errors'; +import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; +import logger from '@server/logger'; + +const deleteRoleSchema = z.object({ + roleId: z.string().transform(Number).pipe(z.number().int().positive()) +}); + +export async function deleteRole(req: Request, res: Response, next: NextFunction): Promise { + try { + const parsedParams = deleteRoleSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedParams.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { roleId } = parsedParams.data; + + // Check if the user has permission to delete roles + const hasPermission = await checkUserActionPermission(ActionsEnum.deleteRole, req); + if (!hasPermission) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + const role = await db.select() + .from(roles) + .where(eq(roles.roleId, roleId)) + .limit(1); + + if (role.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Role with ID ${roleId} not found` + ) + ); + } + + if (role[0].isSuperuserRole) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + `Cannot delete a superuser role` + ) + ); + } + + const deletedRole = await db.delete(roles) + .where(eq(roles.roleId, roleId)) + .returning(); + + if (deletedRole.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Role with ID ${roleId} not found` + ) + ); + } + + return response(res, { + data: null, + success: true, + error: false, + message: "Role deleted successfully", + status: HttpCode.OK, + }); + } catch (error) { + logger.error(error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); + } +} \ No newline at end of file diff --git a/server/routers/role/getRole.ts b/server/routers/role/getRole.ts new file mode 100644 index 00000000..6866448b --- /dev/null +++ b/server/routers/role/getRole.ts @@ -0,0 +1,61 @@ +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import { db } from '@server/db'; +import { roles } from '@server/db/schema'; +import { eq } from 'drizzle-orm'; +import response from "@server/utils/response"; +import HttpCode from '@server/types/HttpCode'; +import createHttpError from 'http-errors'; +import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; +import logger from '@server/logger'; + +const getRoleSchema = z.object({ + roleId: z.string().transform(Number).pipe(z.number().int().positive()) +}); + +export async function getRole(req: Request, res: Response, next: NextFunction): Promise { + try { + const parsedParams = getRoleSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedParams.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { roleId } = parsedParams.data; + + // Check if the user has permission to get roles + const hasPermission = await checkUserActionPermission(ActionsEnum.getRole, req); + if (!hasPermission) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + const role = await db.select() + .from(roles) + .where(eq(roles.roleId, roleId)) + .limit(1); + + if (role.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Role with ID ${roleId} not found` + ) + ); + } + + return response(res, { + data: role[0], + success: true, + error: false, + message: "Role retrieved successfully", + status: HttpCode.OK, + }); + } catch (error) { + logger.error(error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); + } +} \ No newline at end of file diff --git a/server/routers/role/index.ts b/server/routers/role/index.ts new file mode 100644 index 00000000..fff5448d --- /dev/null +++ b/server/routers/role/index.ts @@ -0,0 +1,15 @@ +export * from "./addRoleAction"; +export * from "./addRoleResource"; +export * from "./addRoleSite"; +export * from "./createRole"; +export * from "./deleteRole"; +export * from "./getRole"; +export * from "./index"; +export * from "./listRoleActions"; +export * from "./listRoleResources"; +export * from "./listRoles"; +export * from "./listRoleSites"; +export * from "./removeRoleAction"; +export * from "./removeRoleResource"; +export * from "./removeRoleSite"; +export * from "./updateRole"; \ No newline at end of file diff --git a/server/routers/role/listRoleActions.ts b/server/routers/role/listRoleActions.ts new file mode 100644 index 00000000..6f16b956 --- /dev/null +++ b/server/routers/role/listRoleActions.ts @@ -0,0 +1,59 @@ +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import { db } from '@server/db'; +import { roleActions, actions } from '@server/db/schema'; +import { eq } from 'drizzle-orm'; +import response from "@server/utils/response"; +import HttpCode from '@server/types/HttpCode'; +import createHttpError from 'http-errors'; +import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; +import logger from '@server/logger'; + +const listRoleActionsSchema = z.object({ + roleId: z.string().transform(Number).pipe(z.number().int().positive()), +}); + +export async function listRoleActions(req: Request, res: Response, next: NextFunction): Promise { + try { + const parsedParams = listRoleActionsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedParams.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { roleId } = parsedParams.data; + + // Check if the user has permission to list role actions + const hasPermission = await checkUserActionPermission(ActionsEnum.listRoleActions, req); + if (!hasPermission) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + const roleActionsList = await db + .select({ + actionId: actions.actionId, + name: actions.name, + description: actions.description, + }) + .from(roleActions) + .innerJoin(actions, eq(roleActions.actionId, actions.actionId)) + .where(eq(roleActions.roleId, roleId)); + + // TODO: Do we need to filter out what the user can see? + + return response(res, { + data: roleActionsList, + success: true, + error: false, + message: "Role actions retrieved successfully", + status: HttpCode.OK, + }); + } catch (error) { + logger.error(error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); + } +} \ No newline at end of file diff --git a/server/routers/role/listRoleResources.ts b/server/routers/role/listRoleResources.ts new file mode 100644 index 00000000..0d4644a4 --- /dev/null +++ b/server/routers/role/listRoleResources.ts @@ -0,0 +1,59 @@ +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import { db } from '@server/db'; +import { roleResources, resources } from '@server/db/schema'; +import { eq } from 'drizzle-orm'; +import response from "@server/utils/response"; +import HttpCode from '@server/types/HttpCode'; +import createHttpError from 'http-errors'; +import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; +import logger from '@server/logger'; + +const listRoleResourcesSchema = z.object({ + roleId: z.string().transform(Number).pipe(z.number().int().positive()), +}); + +export async function listRoleResources(req: Request, res: Response, next: NextFunction): Promise { + try { + const parsedParams = listRoleResourcesSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedParams.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { roleId } = parsedParams.data; + + // Check if the user has permission to list role resources + const hasPermission = await checkUserActionPermission(ActionsEnum.listRoleResources, req); + if (!hasPermission) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + const roleResourcesList = await db + .select({ + resourceId: resources.resourceId, + name: resources.name, + subdomain: resources.subdomain, + }) + .from(roleResources) + .innerJoin(resources, eq(roleResources.resourceId, resources.resourceId)) + .where(eq(roleResources.roleId, roleId)); + + // TODO: Do we need to filter out what the user can see? + + return response(res, { + data: roleResourcesList, + success: true, + error: false, + message: "Role resources retrieved successfully", + status: HttpCode.OK, + }); + } catch (error) { + logger.error(error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); + } +} \ No newline at end of file diff --git a/server/routers/role/listRoleSites.ts b/server/routers/role/listRoleSites.ts new file mode 100644 index 00000000..ddb38762 --- /dev/null +++ b/server/routers/role/listRoleSites.ts @@ -0,0 +1,59 @@ +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import { db } from '@server/db'; +import { roleSites, sites } from '@server/db/schema'; +import { eq } from 'drizzle-orm'; +import response from "@server/utils/response"; +import HttpCode from '@server/types/HttpCode'; +import createHttpError from 'http-errors'; +import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; +import logger from '@server/logger'; + +const listRoleSitesSchema = z.object({ + roleId: z.string().transform(Number).pipe(z.number().int().positive()), +}); + +export async function listRoleSites(req: Request, res: Response, next: NextFunction): Promise { + try { + const parsedParams = listRoleSitesSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedParams.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { roleId } = parsedParams.data; + + // Check if the user has permission to list role sites + const hasPermission = await checkUserActionPermission(ActionsEnum.listRoleSites, req); + if (!hasPermission) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + const roleSitesList = await db + .select({ + siteId: sites.siteId, + name: sites.name, + subdomain: sites.subdomain, + }) + .from(roleSites) + .innerJoin(sites, eq(roleSites.siteId, sites.siteId)) + .where(eq(roleSites.roleId, roleId)); + + // TODO: Do we need to filter out what the user can see? + + return response(res, { + data: roleSitesList, + success: true, + error: false, + message: "Role sites retrieved successfully", + status: HttpCode.OK, + }); + } catch (error) { + logger.error(error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); + } +} \ No newline at end of file diff --git a/server/routers/role/listRoles.ts b/server/routers/role/listRoles.ts new file mode 100644 index 00000000..9409eb9f --- /dev/null +++ b/server/routers/role/listRoles.ts @@ -0,0 +1,95 @@ +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import { db } from '@server/db'; +import { roles, orgs } from '@server/db/schema'; +import response from "@server/utils/response"; +import HttpCode from '@server/types/HttpCode'; +import createHttpError from 'http-errors'; +import { sql, eq } from 'drizzle-orm'; +import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; +import logger from '@server/logger'; + +const listRolesParamsSchema = z.object({ + orgId: z.string().optional().transform(Number).pipe(z.number().int().positive()), +}); + +const listRolesSchema = z.object({ + limit: z.string().optional().transform(Number).pipe(z.number().int().positive().default(10)), + offset: z.string().optional().transform(Number).pipe(z.number().int().nonnegative().default(0)), + orgId: z.string().optional().transform(Number).pipe(z.number().int().positive()), +}); + +export async function listRoles(req: Request, res: Response, next: NextFunction): Promise { + try { + const parsedQuery = listRolesSchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedQuery.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { limit, offset } = parsedQuery.data; + + const parsedParams = listRolesParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedParams.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { orgId } = parsedParams.data; + + // Check if the user has permission to list roles + const hasPermission = await checkUserActionPermission(ActionsEnum.listRoles, req); + if (!hasPermission) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + let baseQuery: any = db + .select({ + roleId: roles.roleId, + orgId: roles.orgId, + isSuperuserRole: roles.isSuperuserRole, + name: roles.name, + description: roles.description, + orgName: orgs.name, + }) + .from(roles) + .leftJoin(orgs, eq(roles.orgId, orgs.orgId)); + + let countQuery: any = db.select({ count: sql`cast(count(*) as integer)` }).from(roles); + + if (orgId) { + baseQuery = baseQuery.where(eq(roles.orgId, orgId)); + countQuery = countQuery.where(eq(roles.orgId, orgId)); + } + + const rolesList = await baseQuery.limit(limit).offset(offset); + const totalCountResult = await countQuery; + const totalCount = totalCountResult[0].count; + + return response(res, { + data: { + roles: rolesList, + pagination: { + total: totalCount, + limit, + offset, + }, + }, + success: true, + error: false, + message: "Roles retrieved successfully", + status: HttpCode.OK, + }); + } catch (error) { + logger.error(error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); + } +} \ No newline at end of file diff --git a/server/routers/role/removeRoleAction.ts b/server/routers/role/removeRoleAction.ts new file mode 100644 index 00000000..08c1217d --- /dev/null +++ b/server/routers/role/removeRoleAction.ts @@ -0,0 +1,76 @@ +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import { db } from '@server/db'; +import { roleActions } from '@server/db/schema'; +import { and, eq } from 'drizzle-orm'; +import response from "@server/utils/response"; +import HttpCode from '@server/types/HttpCode'; +import createHttpError from 'http-errors'; +import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; +import logger from '@server/logger'; + +const removeRoleActionParamsSchema = z.object({ + roleId: z.string().transform(Number).pipe(z.number().int().positive()), +}); + +const removeRoleActionSchema = z.object({ + actionId: z.string(), +}); + +export async function removeRoleAction(req: Request, res: Response, next: NextFunction): Promise { + try { + const parsedParams = removeRoleActionSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedParams.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { actionId } = parsedParams.data; + + const parsedBody = removeRoleActionParamsSchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedBody.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { roleId } = parsedBody.data; + + // Check if the user has permission to remove role actions + const hasPermission = await checkUserActionPermission(ActionsEnum.removeRoleAction, req); + if (!hasPermission) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + const deletedRoleAction = await db.delete(roleActions) + .where(and(eq(roleActions.roleId, roleId), eq(roleActions.actionId, actionId))) + .returning(); + + if (deletedRoleAction.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Action with ID ${actionId} not found for role with ID ${roleId}` + ) + ); + } + + return response(res, { + data: null, + success: true, + error: false, + message: "Action removed from role successfully", + status: HttpCode.OK, + }); + } catch (error) { + logger.error(error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); + } +} \ No newline at end of file diff --git a/server/routers/role/removeRoleResource.ts b/server/routers/role/removeRoleResource.ts new file mode 100644 index 00000000..557e6cef --- /dev/null +++ b/server/routers/role/removeRoleResource.ts @@ -0,0 +1,61 @@ +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import { db } from '@server/db'; +import { roleResources } from '@server/db/schema'; +import { and, eq } from 'drizzle-orm'; +import response from "@server/utils/response"; +import HttpCode from '@server/types/HttpCode'; +import createHttpError from 'http-errors'; +import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; +import logger from '@server/logger'; + +const removeRoleResourceSchema = z.object({ + roleId: z.string().transform(Number).pipe(z.number().int().positive()), + resourceId: z.string(), +}); + +export async function removeRoleResource(req: Request, res: Response, next: NextFunction): Promise { + try { + const parsedParams = removeRoleResourceSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedParams.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { roleId, resourceId } = parsedParams.data; + + // Check if the user has permission to remove role resources + const hasPermission = await checkUserActionPermission(ActionsEnum.removeRoleResource, req); + if (!hasPermission) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + const deletedRoleResource = await db.delete(roleResources) + .where(and(eq(roleResources.roleId, roleId), eq(roleResources.resourceId, resourceId))) + .returning(); + + if (deletedRoleResource.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Resource with ID ${resourceId} not found for role with ID ${roleId}` + ) + ); + } + + return response(res, { + data: null, + success: true, + error: false, + message: "Resource removed from role successfully", + status: HttpCode.OK, + }); + } catch (error) { + logger.error(error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); + } +} \ No newline at end of file diff --git a/server/routers/role/removeRoleSite.ts b/server/routers/role/removeRoleSite.ts new file mode 100644 index 00000000..badaac87 --- /dev/null +++ b/server/routers/role/removeRoleSite.ts @@ -0,0 +1,86 @@ +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import { db } from '@server/db'; +import { resources, roleResources, roleSites } from '@server/db/schema'; +import { and, eq } from 'drizzle-orm'; +import response from "@server/utils/response"; +import HttpCode from '@server/types/HttpCode'; +import createHttpError from 'http-errors'; +import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; +import logger from '@server/logger'; + +const removeRoleSiteParamsSchema = z.object({ + roleId: z.string().transform(Number).pipe(z.number().int().positive()), +}); + +const removeRoleSiteSchema = z.object({ + siteId: z.string().transform(Number).pipe(z.number().int().positive()), +}); + +export async function removeRoleSite(req: Request, res: Response, next: NextFunction): Promise { + try { + const parsedParams = removeRoleSiteSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedParams.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { siteId } = parsedParams.data; + + const parsedBody = removeRoleSiteParamsSchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedBody.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { roleId } = parsedBody.data; + + // Check if the user has permission to remove role sites + const hasPermission = await checkUserActionPermission(ActionsEnum.removeRoleSite, req); + if (!hasPermission) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + const deletedRoleSite = await db.delete(roleSites) + .where(and(eq(roleSites.roleId, roleId), eq(roleSites.siteId, siteId))) + .returning(); + + if (deletedRoleSite.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Site with ID ${siteId} not found for role with ID ${roleId}` + ) + ); + } + + const siteResources = await db.select() + .from(resources) + .where(eq(resources.siteId, siteId)); + + for (const resource of siteResources) { + await db.delete(roleResources) + .where(and(eq(roleResources.roleId, roleId), eq(roleResources.resourceId, resource.resourceId))) + .returning(); + } + + return response(res, { + data: null, + success: true, + error: false, + message: "Site removed from role successfully", + status: HttpCode.OK, + }); + } catch (error) { + logger.error(error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); + } +} \ No newline at end of file diff --git a/server/routers/role/updateRole.ts b/server/routers/role/updateRole.ts new file mode 100644 index 00000000..bd8960b5 --- /dev/null +++ b/server/routers/role/updateRole.ts @@ -0,0 +1,102 @@ +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import { db } from '@server/db'; +import { roles } from '@server/db/schema'; +import { eq } from 'drizzle-orm'; +import response from "@server/utils/response"; +import HttpCode from '@server/types/HttpCode'; +import createHttpError from 'http-errors'; +import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; +import logger from '@server/logger'; + +const updateRoleParamsSchema = z.object({ + roleId: z.string().transform(Number).pipe(z.number().int().positive()) +}); + +const updateRoleBodySchema = z.object({ + name: z.string().min(1).max(255).optional(), + description: z.string().optional(), +}).refine(data => Object.keys(data).length > 0, { + message: "At least one field must be provided for update" +}); + +export async function updateRole(req: Request, res: Response, next: NextFunction): Promise { + try { + const parsedParams = updateRoleParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedParams.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const parsedBody = updateRoleBodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedBody.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { roleId } = parsedParams.data; + const updateData = parsedBody.data; + + // Check if the user has permission to update roles + const hasPermission = await checkUserActionPermission(ActionsEnum.updateRole, req); + if (!hasPermission) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + const role = await db.select() + .from(roles) + .where(eq(roles.roleId, roleId)) + .limit(1); + + if (role.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Role with ID ${roleId} not found` + ) + ); + } + + if (role[0].isSuperuserRole) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + `Cannot update a superuser role` + ) + ); + } + + const updatedRole = await db.update(roles) + .set(updateData) + .where(eq(roles.roleId, roleId)) + .returning(); + + if (updatedRole.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Role with ID ${roleId} not found` + ) + ); + } + + return response(res, { + data: updatedRole[0], + success: true, + error: false, + message: "Role updated successfully", + status: HttpCode.OK, + }); + } catch (error) { + logger.error(error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); + } +} \ No newline at end of file diff --git a/server/routers/site/createSite.ts b/server/routers/site/createSite.ts index 38ad97f0..2ae99246 100644 --- a/server/routers/site/createSite.ts +++ b/server/routers/site/createSite.ts @@ -1,13 +1,14 @@ import { Request, Response, NextFunction } from 'express'; import { z } from 'zod'; import { db } from '@server/db'; -import { sites } from '@server/db/schema'; +import { roles, userSites, sites, roleSites } from '@server/db/schema'; import response from "@server/utils/response"; import HttpCode from '@server/types/HttpCode'; import createHttpError from 'http-errors'; import fetch from 'node-fetch'; import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import logger from '@server/logger'; +import { eq, and } from 'drizzle-orm'; const API_BASE_URL = "http://localhost:3000"; @@ -54,7 +55,11 @@ export async function createSite(req: Request, res: Response, next: NextFunction // Check if the user has permission to list sites const hasPermission = await checkUserActionPermission(ActionsEnum.createSite, req); if (!hasPermission) { - return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to list sites')); + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission perform this action')); + } + + if (!req.userOrgRoleId) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have a role')); } // Create new site in the database @@ -65,7 +70,34 @@ export async function createSite(req: Request, res: Response, next: NextFunction pubKey, subnet, }).returning(); + // find the superuser roleId and also add the resource to the superuser role + const superuserRole = await db.select() + .from(roles) + .where(and(eq(roles.isSuperuserRole, true), eq(roles.orgId, orgId))) + .limit(1); + + if (superuserRole.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Superuser role not found` + ) + ); + } + + await db.insert(roleSites).values({ + roleId: superuserRole[0].roleId, + siteId: newSite[0].siteId, + }); + if (req.userOrgRoleId != superuserRole[0].roleId) { + // make sure the user can access the site + db.insert(userSites).values({ + userId: req.user?.id!, + siteId: newSite[0].siteId, + }); + } + return response(res, { data: newSite[0], success: true, diff --git a/server/routers/site/deleteSite.ts b/server/routers/site/deleteSite.ts index bad0d1ce..5a99320f 100644 --- a/server/routers/site/deleteSite.ts +++ b/server/routers/site/deleteSite.ts @@ -35,7 +35,7 @@ export async function deleteSite(req: Request, res: Response, next: NextFunction // Check if the user has permission to list sites const hasPermission = await checkUserActionPermission(ActionsEnum.deleteSite, req); if (!hasPermission) { - return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to list sites')); + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); } // Delete the site from the database diff --git a/server/routers/site/getSite.ts b/server/routers/site/getSite.ts index 2b14a765..cb95bed5 100644 --- a/server/routers/site/getSite.ts +++ b/server/routers/site/getSite.ts @@ -32,7 +32,7 @@ export async function getSite(req: Request, res: Response, next: NextFunction): // Check if the user has permission to list sites const hasPermission = await checkUserActionPermission(ActionsEnum.updateSite, req); if (!hasPermission) { - return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to list sites')); + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); } // Fetch the site from the database diff --git a/server/routers/site/index.ts b/server/routers/site/index.ts index 320c10de..944a72a4 100644 --- a/server/routers/site/index.ts +++ b/server/routers/site/index.ts @@ -2,4 +2,5 @@ export * from "./getSite"; export * from "./createSite"; export * from "./deleteSite"; export * from "./updateSite"; -export * from "./listSites"; \ No newline at end of file +export * from "./listSites"; +export * from "./listSiteRoles"; \ No newline at end of file diff --git a/server/routers/site/listSiteRoles.ts b/server/routers/site/listSiteRoles.ts new file mode 100644 index 00000000..4acf52c0 --- /dev/null +++ b/server/routers/site/listSiteRoles.ts @@ -0,0 +1,58 @@ +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import { db } from '@server/db'; +import { roleSites, roles } from '@server/db/schema'; +import { eq } from 'drizzle-orm'; +import response from "@server/utils/response"; +import HttpCode from '@server/types/HttpCode'; +import createHttpError from 'http-errors'; +import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; +import logger from '@server/logger'; + +const listSiteRolesSchema = z.object({ + siteId: z.string().transform(Number).pipe(z.number().int().positive()), +}); + +export async function listSiteRoles(req: Request, res: Response, next: NextFunction): Promise { + try { + const parsedParams = listSiteRolesSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedParams.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { siteId } = parsedParams.data; + + // Check if the user has permission to list site roles + const hasPermission = await checkUserActionPermission(ActionsEnum.listSiteRoles, req); + if (!hasPermission) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + const siteRolesList = await db + .select({ + roleId: roles.roleId, + name: roles.name, + description: roles.description, + isSuperuserRole: roles.isSuperuserRole, + }) + .from(roleSites) + .innerJoin(roles, eq(roleSites.roleId, roles.roleId)) + .where(eq(roleSites.siteId, siteId)); + + return response(res, { + data: siteRolesList, + success: true, + error: false, + message: "Site roles retrieved successfully", + status: HttpCode.OK, + }); + } catch (error) { + logger.error(error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); + } +} \ No newline at end of file diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index 63a7eeed..29436b51 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -36,7 +36,7 @@ export async function listSites(req: Request, res: Response, next: NextFunction) // Check if the user has permission to list sites const hasPermission = await checkUserActionPermission(ActionsEnum.listSites, req); if (!hasPermission) { - return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to list sites')); + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); } if (orgId && orgId !== req.userOrgId) { diff --git a/server/routers/site/updateSite.ts b/server/routers/site/updateSite.ts index 23ba2b21..e5ecc409 100644 --- a/server/routers/site/updateSite.ts +++ b/server/routers/site/updateSite.ts @@ -57,7 +57,7 @@ export async function updateSite(req: Request, res: Response, next: NextFunction // Check if the user has permission to list sites const hasPermission = await checkUserActionPermission(ActionsEnum.updateSite, req); if (!hasPermission) { - return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to list sites')); + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); } // Update the site in the database diff --git a/server/routers/target/createTarget.ts b/server/routers/target/createTarget.ts index fcfe20db..eb216c95 100644 --- a/server/routers/target/createTarget.ts +++ b/server/routers/target/createTarget.ts @@ -49,7 +49,7 @@ export async function createTarget(req: Request, res: Response, next: NextFuncti // Check if the user has permission to list sites const hasPermission = await checkUserActionPermission(ActionsEnum.createTarget, req); if (!hasPermission) { - return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to list sites')); + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); } const newTarget = await db.insert(targets).values({ diff --git a/server/routers/target/deleteTarget.ts b/server/routers/target/deleteTarget.ts index 5d5132e3..40f9af2b 100644 --- a/server/routers/target/deleteTarget.ts +++ b/server/routers/target/deleteTarget.ts @@ -30,7 +30,7 @@ export async function deleteTarget(req: Request, res: Response, next: NextFuncti // Check if the user has permission to list sites const hasPermission = await checkUserActionPermission(ActionsEnum.deleteTarget, req); if (!hasPermission) { - return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to list sites')); + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); } const deletedTarget = await db.delete(targets) diff --git a/server/routers/target/getTarget.ts b/server/routers/target/getTarget.ts index 49fd0952..a0a1c321 100644 --- a/server/routers/target/getTarget.ts +++ b/server/routers/target/getTarget.ts @@ -30,7 +30,7 @@ export async function getTarget(req: Request, res: Response, next: NextFunction) // Check if the user has permission to list sites const hasPermission = await checkUserActionPermission(ActionsEnum.getTarget, req); if (!hasPermission) { - return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to list sites')); + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); } const target = await db.select() diff --git a/server/routers/target/listTargets.ts b/server/routers/target/listTargets.ts index 375abe8c..6873f0d0 100644 --- a/server/routers/target/listTargets.ts +++ b/server/routers/target/listTargets.ts @@ -47,7 +47,7 @@ export async function listTargets(req: Request, res: Response, next: NextFunctio // Check if the user has permission to list sites const hasPermission = await checkUserActionPermission(ActionsEnum.listTargets, req); if (!hasPermission) { - return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to list sites')); + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); } let baseQuery: any = db diff --git a/server/routers/target/updateTarget.ts b/server/routers/target/updateTarget.ts index 01401c08..d0486995 100644 --- a/server/routers/target/updateTarget.ts +++ b/server/routers/target/updateTarget.ts @@ -51,7 +51,7 @@ export async function updateTarget(req: Request, res: Response, next: NextFuncti // Check if the user has permission to list sites const hasPermission = await checkUserActionPermission(ActionsEnum.updateTarget, req); if (!hasPermission) { - return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to list sites')); + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); } const updatedTarget = await db.update(targets) diff --git a/server/routers/user/deleteUser.ts b/server/routers/user/deleteUser.ts index d74d6424..4ffe3479 100644 --- a/server/routers/user/deleteUser.ts +++ b/server/routers/user/deleteUser.ts @@ -30,7 +30,7 @@ export async function deleteUser(req: Request, res: Response, next: NextFunction // Check if the user has permission to list sites const hasPermission = await checkUserActionPermission(ActionsEnum.deleteUser, req); if (!hasPermission) { - return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to list sites')); + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); } const deletedUser = await db.delete(users) diff --git a/server/routers/user/index.ts b/server/routers/user/index.ts index 42478702..fbc1ecc7 100644 --- a/server/routers/user/index.ts +++ b/server/routers/user/index.ts @@ -1,3 +1,5 @@ export * from "./getUser"; export * from "./deleteUser"; -export * from "./listUsers"; \ No newline at end of file +export * from "./listUsers"; +export * from "./listUserRoles"; +export * from "./setUserRole"; \ No newline at end of file diff --git a/server/routers/user/listUsers.ts b/server/routers/user/listUsers.ts index 3a365788..e7082ea1 100644 --- a/server/routers/user/listUsers.ts +++ b/server/routers/user/listUsers.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from 'express'; import { z } from 'zod'; import { db } from '@server/db'; -import { users } from '@server/db/schema'; +import { roles, userOrgs, users } from '@server/db/schema'; import response from "@server/utils/response"; import HttpCode from '@server/types/HttpCode'; import createHttpError from 'http-errors'; @@ -10,59 +10,67 @@ import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import logger from '@server/logger'; const listUsersSchema = z.object({ - limit: z.string().optional().transform(Number).pipe(z.number().int().positive().default(10)), - offset: z.string().optional().transform(Number).pipe(z.number().int().nonnegative().default(0)), + limit: z.string().optional().transform(Number).pipe(z.number().int().positive().default(10)), + offset: z.string().optional().transform(Number).pipe(z.number().int().nonnegative().default(0)), }); export async function listUsers(req: Request, res: Response, next: NextFunction): Promise { - try { - const parsedQuery = listUsersSchema.safeParse(req.query); - if (!parsedQuery.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - parsedQuery.error.errors.map(e => e.message).join(', ') - ) - ); - } - - const { limit, offset } = parsedQuery.data; - - // Check if the user has permission to list sites - const hasPermission = await checkUserActionPermission(ActionsEnum.listUsers, req); - if (!hasPermission) { - return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to list sites')); - } - - const usersList = await db.select() - .from(users) - .limit(limit) - .offset(offset); - - const totalCountResult = await db - .select({ count: sql`cast(count(*) as integer)` }) - .from(users); - const totalCount = totalCountResult[0].count; - - // Remove passwordHash from each user object - const usersWithoutPassword = usersList.map(({ passwordHash, ...userWithoutPassword }) => userWithoutPassword); - - return response(res, { - data: { - users: usersWithoutPassword, - pagination: { - total: totalCount, - limit, - offset, - }, - }, - success: true, - error: false, - message: "Users retrieved successfully", - status: HttpCode.OK, - }); - } catch (error) { - logger.error(error); - return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); + try { + const parsedQuery = listUsersSchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedQuery.error.errors.map(e => e.message).join(', ') + ) + ); } + const { limit, offset } = parsedQuery.data; + + // Check if the user has permission to list users + const hasPermission = await checkUserActionPermission(ActionsEnum.listUsers, req); + if (!hasPermission) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + // Query to join users, userOrgs, and roles tables + const usersWithRoles = await db + .select({ + id: users.id, + email: users.email, + emailVerified: users.emailVerified, + dateCreated: users.dateCreated, + orgId: userOrgs.orgId, + roleId: userOrgs.roleId, + roleName: roles.name, + }) + .from(users) + .leftJoin(userOrgs, sql`${users.id} = ${userOrgs.userId}`) + .leftJoin(roles, sql`${userOrgs.roleId} = ${roles.roleId}`) + .limit(limit) + .offset(offset); + + // Count total users + const [{ count }] = await db + .select({ count: sql`count(*)` }) + .from(users); + + return response(res, { + data: { + users: usersWithRoles, + pagination: { + total: count, + limit, + offset, + }, + }, + success: true, + error: false, + message: "Users retrieved successfully", + status: HttpCode.OK, + }); + } catch (error) { + logger.error(error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); + } } \ No newline at end of file diff --git a/server/routers/user/setUserRole.ts b/server/routers/user/setUserRole.ts new file mode 100644 index 00000000..3bae1924 --- /dev/null +++ b/server/routers/user/setUserRole.ts @@ -0,0 +1,64 @@ +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import { db } from '@server/db'; +import { userOrgs, roles } from '@server/db/schema'; +import { eq, and } from 'drizzle-orm'; +import response from "@server/utils/response"; +import HttpCode from '@server/types/HttpCode'; +import createHttpError from 'http-errors'; +import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; +import logger from '@server/logger'; + +const addUserRoleSchema = z.object({ + userId: z.string(), + roleId: z.number().int().positive(), + orgId: z.number().int().positive(), +}); + +export async function addUserRole(req: Request, res: Response, next: NextFunction): Promise { + try { + const parsedBody = addUserRoleSchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedBody.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { userId, roleId, orgId } = parsedBody.data; + + // Check if the user has permission to add user roles + const hasPermission = await checkUserActionPermission(ActionsEnum.addUserRole, req); + if (!hasPermission) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + // Check if the role exists and belongs to the specified org + const roleExists = await db.select() + .from(roles) + .where(and(eq(roles.roleId, roleId), eq(roles.orgId, orgId))) + .limit(1); + + if (roleExists.length === 0) { + return next(createHttpError(HttpCode.NOT_FOUND, 'Role not found or does not belong to the specified organization')); + } + + const newUserRole = await db.update(userOrgs) + .set({ roleId }) + .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))) + .returning(); + + return response(res, { + data: newUserRole[0], + success: true, + error: false, + message: "Role added to user successfully", + status: HttpCode.CREATED, + }); + } catch (error) { + logger.error(error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); + } +} \ No newline at end of file