diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 890982f6..0fc38564 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -26,8 +26,33 @@ export enum ActionsEnum { getTarget = "getTarget", listTargets = "listTargets", updateTarget = "updateTarget", - deleteUser = "deleteUser", - listUsers = "listUsers" + createRole = "createRole", + deleteRole = "deleteRole", + getRole = "getRole", + listRoles = "listRoles", + updateRole = "updateRole", + addUser = "addUser", + removeUser = "removeUser", + listUsers = "listUsers", + listSiteRoles = "listSiteRoles", + listUserRoles = "listUserRoles", + listResourceRoles = "listResourceRoles", + addRoleSite = "addRoleSite", + addRoleResource = "addRoleResource", + removeRoleResource = "removeRoleResource", + removeRoleSite = "removeRoleSite", + addRoleAction = "addRoleAction", + removeRoleAction = "removeRoleAction", + listRoleSites = "listRoleSites", + listRoleResources = "listRoleResources", + listRoleActions = "listRoleActions", + addUserRole = "addUserRole", + addUserResource = "addUserResource", + addUserSite = "addUserSite", + addUserAction = "addUserAction", + removeUserAction = "removeUserAction", + removeUserResource = "removeUserResource", + removeUserSite = "removeUserSite", } 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..7d7b36eb 100644 --- a/server/routers/auth/index.ts +++ b/server/routers/auth/index.ts @@ -9,8 +9,12 @@ export * from "./getUserOrgs"; export * from "./verifySiteAccess"; export * from "./verifyResourceAccess"; export * from "./verifyTargetAccess"; +export * from "./verifyRoleAccess"; +export * from "./verifyUserAccess"; +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/verifyResourceAccess.ts b/server/routers/auth/verifyResourceAccess.ts index 16f6b97b..85b133eb 100644 --- a/server/routers/auth/verifyResourceAccess.ts +++ b/server/routers/auth/verifyResourceAccess.ts @@ -7,7 +7,7 @@ import HttpCode from '@server/types/HttpCode'; export async function verifyResourceAccess(req: Request, res: Response, next: NextFunction) { const userId = req.user!.id; // Assuming you have user information in the request - const resourceId = req.params.resourceId; + const resourceId = req.params.resourceId || req.body.resourceId || req.query.resourceId; if (!userId) { return next(createHttpError(HttpCode.UNAUTHORIZED, 'User not authenticated')); 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/verifySiteAccess.ts b/server/routers/auth/verifySiteAccess.ts index c2b3f083..7f53ef65 100644 --- a/server/routers/auth/verifySiteAccess.ts +++ b/server/routers/auth/verifySiteAccess.ts @@ -7,7 +7,7 @@ import HttpCode from '@server/types/HttpCode'; export async function verifySiteAccess(req: Request, res: Response, next: NextFunction) { const userId = req.user!.id; // Assuming you have user information in the request - const siteId = parseInt(req.params.siteId); + const siteId = parseInt(req.params.siteId || req.body.siteId || req.query.siteId); if (!userId) { return next(createHttpError(HttpCode.UNAUTHORIZED, 'User not authenticated')); 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/verifyUserAccess.ts b/server/routers/auth/verifyUserAccess.ts new file mode 100644 index 00000000..67e6c936 --- /dev/null +++ b/server/routers/auth/verifyUserAccess.ts @@ -0,0 +1,37 @@ +import { Request, Response, NextFunction } from 'express'; +import { db } from '@server/db'; +import { sites, userOrgs, userSites, roleSites, roles } from '@server/db/schema'; +import { and, eq, or } from 'drizzle-orm'; +import createHttpError from 'http-errors'; +import HttpCode from '@server/types/HttpCode'; + +export async function verifyUserAccess(req: Request, res: Response, next: NextFunction) { + const userId = req.user!.id; // Assuming you have user information in the request + const reqUserId = req.params.userId || req.body.userId || req.query.userId; + + if (!userId) { + return next(createHttpError(HttpCode.UNAUTHORIZED, 'User not authenticated')); + } + + if (!reqUserId) { + return next(createHttpError(HttpCode.BAD_REQUEST, 'Invalid user ID')); + } + + try { + + const userOrg = await db.select() + .from(userOrgs) + .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, req.userOrgId!))) + .limit(1); + + if (userOrg.length === 0) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have access to this user')); + } + + // If we reach here, the user doesn't have access to the site + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have access to this site')); + + } catch (error) { + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, 'Error verifying site 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 4989fa3f..b49edd2d 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,10 @@ import { verifySiteAccess, verifyResourceAccess, verifyTargetAccess, + verifyRoleAccess, + verifySuperuser, + verifyUserInRole, + verifyUserAccess, } from "./auth"; // Root routes @@ -39,6 +44,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 +59,11 @@ authenticated.get( verifyOrgAccess, resource.listResources, ); +authenticated.get( + "/resource/:resourceId/roles", + verifyResourceAccess, + resource.listResourceRoles, +); authenticated.get( "/resource/:resourceId", verifyResourceAccess, @@ -96,6 +107,142 @@ authenticated.get("/users", user.listUsers); unauthenticated.get("/user", verifySessionMiddleware, user.getUser); // authenticated.get("/user/:userId", user.getUser); authenticated.delete("/user/:userId", user.deleteUser); +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, + verifySuperuser, + role.removeRoleAction, +); +authenticated.get( + "/role/:roleId/actions", + verifyRoleAccess, + verifyUserInRole, + verifySuperuser, + role.listRoleActions, +); + +authenticated.get("/user", user.getUser); +authenticated.get("/org/:orgId/users", verifyOrgAccess, user.listUsers); +authenticated.delete( + "/org/:orgId/user/:userId", + verifyOrgAccess, + verifyUserAccess, + user.removeUserOrg, +); +authenticated.put( + "/org/:orgId/user/:userId", + verifyOrgAccess, + verifyUserAccess, + user.addUserOrg, +); + +authenticated.put( + "/user/:userId/site", + verifySiteAccess, + verifyUserAccess, + role.addRoleSite, +); +authenticated.delete( + "/user/:userId/site", + verifySiteAccess, + verifyUserAccess, + role.removeRoleSite, +); +authenticated.put( + "/user/:userId/resource", + verifyResourceAccess, + verifyUserAccess, + role.addRoleResource, +); +authenticated.delete( + "/user/:userId/resource", + verifyResourceAccess, + verifyUserAccess, + role.removeRoleResource, +); +authenticated.put( + "/org/:orgId/user/:userId/action", + verifyOrgAccess, + verifyUserAccess, + verifySuperuser, + role.addRoleAction, +); +authenticated.delete( + "/org/:orgId/user/:userId/action", + verifyOrgAccess, + verifyUserAccess, + verifySuperuser, + role.removeRoleAction, +); // Auth routes export const authRouter = Router(); 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/user/deleteUser.ts b/server/routers/role/getRole.ts similarity index 61% rename from server/routers/user/deleteUser.ts rename to server/routers/role/getRole.ts index d74d6424..6866448b 100644 --- a/server/routers/user/deleteUser.ts +++ b/server/routers/role/getRole.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 } from '@server/db/schema'; import { eq } from 'drizzle-orm'; import response from "@server/utils/response"; import HttpCode from '@server/types/HttpCode'; @@ -9,13 +9,13 @@ import createHttpError from 'http-errors'; import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import logger from '@server/logger'; -const deleteUserSchema = z.object({ - userId: z.string().uuid() +const getRoleSchema = z.object({ + roleId: z.string().transform(Number).pipe(z.number().int().positive()) }); -export async function deleteUser(req: Request, res: Response, next: NextFunction): Promise { +export async function getRole(req: Request, res: Response, next: NextFunction): Promise { try { - const parsedParams = deleteUserSchema.safeParse(req.params); + const parsedParams = getRoleSchema.safeParse(req.params); if (!parsedParams.success) { return next( createHttpError( @@ -25,36 +25,37 @@ export async function deleteUser(req: Request, res: Response, next: NextFunction ); } - const { userId } = parsedParams.data; + const { roleId } = parsedParams.data; - // Check if the user has permission to list sites - const hasPermission = await checkUserActionPermission(ActionsEnum.deleteUser, req); + // 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 list sites')); + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); } - const deletedUser = await db.delete(users) - .where(eq(users.id, userId)) - .returning(); + const role = await db.select() + .from(roles) + .where(eq(roles.roleId, roleId)) + .limit(1); - if (deletedUser.length === 0) { + if (role.length === 0) { return next( createHttpError( HttpCode.NOT_FOUND, - `User with ID ${userId} not found` + `Role with ID ${roleId} not found` ) ); } return response(res, { - data: null, + data: role[0], success: true, error: false, - message: "User deleted successfully", + 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/addUserAction.ts b/server/routers/user/addUserAction.ts new file mode 100644 index 00000000..d84f717c --- /dev/null +++ b/server/routers/user/addUserAction.ts @@ -0,0 +1,61 @@ +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import { db } from '@server/db'; +import { userActions, users } 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 addUserActionSchema = z.object({ + userId: z.string(), + actionId: z.string(), + orgId: z.string().transform(Number).pipe(z.number().int().positive()), +}); + +export async function addUserAction(req: Request, res: Response, next: NextFunction): Promise { + try { + const parsedBody = addUserActionSchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedBody.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { userId, actionId, orgId } = parsedBody.data; + + // Check if the user has permission to add user actions + const hasPermission = await checkUserActionPermission(ActionsEnum.addUserAction, req); + if (!hasPermission) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + // Check if the user exists + const user = await db.select().from(users).where(eq(users.id, userId)).limit(1); + if (user.length === 0) { + return next(createHttpError(HttpCode.NOT_FOUND, `User with ID ${userId} not found`)); + } + + const newUserAction = await db.insert(userActions).values({ + userId, + actionId, + orgId, + }).returning(); + + return response(res, { + data: newUserAction[0], + success: true, + error: false, + message: "Action 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 diff --git a/server/routers/user/addUserOrg.ts b/server/routers/user/addUserOrg.ts new file mode 100644 index 00000000..01b59aba --- /dev/null +++ b/server/routers/user/addUserOrg.ts @@ -0,0 +1,87 @@ +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import { db } from '@server/db'; +import { userOrgs, users, roles } 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 addUserParamsSchema = z.object({ + userId: z.string().uuid(), + orgId: z.number().int().positive(), +}); + +const addUserSchema = z.object({ + roleId: z.number().int().positive(), +}); + +export async function addUserOrg(req: Request, res: Response, next: NextFunction): Promise { + try { + const parsedParams = addUserParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedParams.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { userId, orgId } = parsedParams.data; + + const parsedBody = addUserSchema.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 add users + const hasPermission = await checkUserActionPermission(ActionsEnum.addUser, req); + if (!hasPermission) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + // Check if the user exists + const user = await db.select().from(users).where(eq(users.id, userId)).limit(1); + if (user.length === 0) { + return next(createHttpError(HttpCode.NOT_FOUND, `User with ID ${userId} not found`)); + } + + // Check if the user is already in the organization + const existingUserOrg = await db.select() + .from(userOrgs) + .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))) + .limit(1); + + if (existingUserOrg.length > 0) { + return next(createHttpError(HttpCode.CONFLICT, 'User is already a member of this organization')); + } + + // Add the user to the userOrgs table + const newUserOrg = await db.insert(userOrgs).values({ + userId, + orgId, + roleId + }).returning(); + + return response(res, { + data: newUserOrg[0], + success: true, + error: false, + message: "User added to organization 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/user/addUserResource.ts b/server/routers/user/addUserResource.ts new file mode 100644 index 00000000..f62eefbf --- /dev/null +++ b/server/routers/user/addUserResource.ts @@ -0,0 +1,52 @@ +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import { db } from '@server/db'; +import { 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'; + +const addUserResourceSchema = z.object({ + userId: z.string(), + resourceId: z.string(), +}); + +export async function addUserResource(req: Request, res: Response, next: NextFunction): Promise { + try { + const parsedBody = addUserResourceSchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedBody.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { userId, resourceId } = parsedBody.data; + + // Check if the user has permission to add user resources + const hasPermission = await checkUserActionPermission(ActionsEnum.addUserResource, req); + if (!hasPermission) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + const newUserResource = await db.insert(userResources).values({ + userId, + resourceId, + }).returning(); + + return response(res, { + data: newUserResource[0], + success: true, + error: false, + message: "Resource 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 diff --git a/server/routers/user/addUserSite.ts b/server/routers/user/addUserSite.ts new file mode 100644 index 00000000..8be945a0 --- /dev/null +++ b/server/routers/user/addUserSite.ts @@ -0,0 +1,65 @@ +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import { db } from '@server/db'; +import { resources, userResources, userSites } 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 addUserSiteSchema = z.object({ + userId: z.string(), + siteId: z.string().transform(Number).pipe(z.number().int().positive()), +}); + +export async function addUserSite(req: Request, res: Response, next: NextFunction): Promise { + try { + const parsedBody = addUserSiteSchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedBody.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { userId, siteId } = parsedBody.data; + + // Check if the user has permission to add user sites + const hasPermission = await checkUserActionPermission(ActionsEnum.addUserSite, req); + if (!hasPermission) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + const newUserSite = await db.insert(userSites).values({ + userId, + siteId, + }).returning(); + + // Add all resources associated with the site to the user + const siteResources = await db.select() + .from(resources) + .where(eq(resources.siteId, siteId)); + + for (const resource of siteResources) { + await db.insert(userResources).values({ + userId, + resourceId: resource.resourceId, + }); + } + + return response(res, { + data: newUserSite[0], + success: true, + error: false, + message: "Site 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 diff --git a/server/routers/user/index.ts b/server/routers/user/index.ts index 42478702..148217cf 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 "./removeUserOrg"; +export * from "./addUserOrg"; +export * from "./listUsers"; +export * from "./setUserRole"; diff --git a/server/routers/user/listUsers.ts b/server/routers/user/listUsers.ts index 3a365788..c8b2fa5a 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'; @@ -9,6 +9,10 @@ import { sql } from 'drizzle-orm'; import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import logger from '@server/logger'; +const listUsersParamsSchema = z.object({ + orgId: z.string().optional().transform(Number).pipe(z.number().int().positive()), +}); + 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)), @@ -25,33 +29,54 @@ export async function listUsers(req: Request, res: Response, next: NextFunction) ) ); } - 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 parsedParams = listUsersParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedParams.error.errors.map(e => e.message).join(', ') + ) + ); } - const usersList = await db.select() + const { orgId } = parsedParams.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}`) + .where(sql`${userOrgs.orgId} = ${orgId}`) .limit(limit) .offset(offset); - const totalCountResult = await db - .select({ count: sql`cast(count(*) as integer)` }) + // Count total users + const [{ count }] = await db + .select({ count: sql`count(*)` }) .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, + users: usersWithRoles, pagination: { - total: totalCount, + total: count, limit, offset, }, diff --git a/server/routers/user/removeUserAction.ts b/server/routers/user/removeUserAction.ts new file mode 100644 index 00000000..3b58ff72 --- /dev/null +++ b/server/routers/user/removeUserAction.ts @@ -0,0 +1,81 @@ +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import { db } from '@server/db'; +import { userActions } 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 removeUserActionParamsSchema = z.object({ + userId: z.string(), +}); + +const removeUserActionSchema = z.object({ + actionId: z.string(), + orgId: z.number().int().positive(), +}); + +export async function removeUserAction(req: Request, res: Response, next: NextFunction): Promise { + try { + const parsedParams = removeUserActionParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedParams.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { userId } = parsedParams.data; + + const parsedBody = removeUserActionSchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedBody.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { actionId, orgId } = parsedBody.data; + + // Check if the user has permission to remove user actions + const hasPermission = await checkUserActionPermission(ActionsEnum.removeUserAction, req); + if (!hasPermission) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + const deletedUserAction = await db.delete(userActions) + .where(and( + eq(userActions.userId, userId), + eq(userActions.actionId, actionId), + eq(userActions.orgId, orgId) + )) + .returning(); + + if (deletedUserAction.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Action with ID ${actionId} not found for user with ID ${userId} in organization ${orgId}` + ) + ); + } + + return response(res, { + data: null, + success: true, + error: false, + message: "Action removed from user 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/removeUserOrg.ts b/server/routers/user/removeUserOrg.ts new file mode 100644 index 00000000..3c8eda5c --- /dev/null +++ b/server/routers/user/removeUserOrg.ts @@ -0,0 +1,52 @@ +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import { db } from '@server/db'; +import { userOrgs, users } 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 removeUserSchema = z.object({ + userId: z.string().uuid(), + orgId: z.number().int().positive(), +}); + +export async function removeUserOrg(req: Request, res: Response, next: NextFunction): Promise { + try { + const parsedParams = removeUserSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedParams.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { userId, orgId } = parsedParams.data; + + // Check if the user has permission to list sites + const hasPermission = await checkUserActionPermission(ActionsEnum.removeUser, req); + if (!hasPermission) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + // remove the user from the userOrgs table + await db.delete(userOrgs) + .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))); + + return response(res, { + data: null, + success: true, + error: false, + message: "User deleted successfully", + status: HttpCode.OK, + }); + } catch (error) { + logger.error(error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); + } +} diff --git a/server/routers/user/removeUserResource.ts b/server/routers/user/removeUserResource.ts new file mode 100644 index 00000000..925d4ee1 --- /dev/null +++ b/server/routers/user/removeUserResource.ts @@ -0,0 +1,61 @@ +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import { db } from '@server/db'; +import { userResources } 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 removeUserResourceSchema = z.object({ + userId: z.string(), + resourceId: z.string(), +}); + +export async function removeUserResource(req: Request, res: Response, next: NextFunction): Promise { + try { + const parsedParams = removeUserResourceSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedParams.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { userId, resourceId } = parsedParams.data; + + // Check if the user has permission to remove user resources + const hasPermission = await checkUserActionPermission(ActionsEnum.removeUserResource, req); + if (!hasPermission) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + const deletedUserResource = await db.delete(userResources) + .where(and(eq(userResources.userId, userId), eq(userResources.resourceId, resourceId))) + .returning(); + + if (deletedUserResource.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Resource with ID ${resourceId} not found for user with ID ${userId}` + ) + ); + } + + return response(res, { + data: null, + success: true, + error: false, + message: "Resource removed from user 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/removeUserSite.ts b/server/routers/user/removeUserSite.ts new file mode 100644 index 00000000..362c02a8 --- /dev/null +++ b/server/routers/user/removeUserSite.ts @@ -0,0 +1,86 @@ +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import { db } from '@server/db'; +import { resources, userResources, userSites } 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 removeUserSiteParamsSchema = z.object({ + userId: z.string(), +}); + +const removeUserSiteSchema = z.object({ + siteId: z.number().int().positive(), +}); + +export async function removeUserSite(req: Request, res: Response, next: NextFunction): Promise { + try { + const parsedParams = removeUserSiteParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedParams.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { userId } = parsedParams.data; + + const parsedBody = removeUserSiteSchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedBody.error.errors.map(e => e.message).join(', ') + ) + ); + } + + const { siteId } = parsedBody.data; + + // Check if the user has permission to remove user sites + const hasPermission = await checkUserActionPermission(ActionsEnum.removeUserSite, req); + if (!hasPermission) { + return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); + } + + const deletedUserSite = await db.delete(userSites) + .where(and(eq(userSites.userId, userId), eq(userSites.siteId, siteId))) + .returning(); + + if (deletedUserSite.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Site with ID ${siteId} not found for user with ID ${userId}` + ) + ); + } + + const siteResources = await db.select() + .from(resources) + .where(eq(resources.siteId, siteId)); + + for (const resource of siteResources) { + await db.delete(userResources) + .where(and(eq(userResources.userId, userId), eq(userResources.resourceId, resource.resourceId))) + .returning(); + } + + return response(res, { + data: null, + success: true, + error: false, + message: "Site removed from user 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