Browse Source

refactor contexts, format zod errors, and more refactoring

Milo Schwartz 9 months ago
parent
commit
2852d62258
83 changed files with 2150 additions and 1264 deletions
  1. 2 1
      server/config.ts
  2. 19 25
      server/routers/external.ts
  3. 2 1
      server/routers/gerbil/getConfig.ts
  4. 24 13
      server/routers/org/checkId.ts
  5. 2 1
      server/routers/org/createOrg.ts
  6. 2 1
      server/routers/org/deleteOrg.ts
  7. 3 2
      server/routers/org/updateOrg.ts
  8. 3 2
      server/routers/resource/createResource.ts
  9. 34 15
      server/routers/resource/deleteResource.ts
  10. 36 17
      server/routers/resource/getResource.ts
  11. 33 15
      server/routers/resource/listResourceRoles.ts
  12. 43 22
      server/routers/resource/updateResource.ts
  13. 53 23
      server/routers/role/addRoleAction.ts
  14. 40 19
      server/routers/role/addRoleResource.ts
  15. 43 21
      server/routers/role/addRoleSite.ts
  16. 41 20
      server/routers/role/createRole.ts
  17. 41 21
      server/routers/role/deleteRole.ts
  18. 36 17
      server/routers/role/getRole.ts
  19. 34 16
      server/routers/role/listRoleActions.ts
  20. 38 17
      server/routers/role/listRoleResources.ts
  21. 34 16
      server/routers/role/listRoleSites.ts
  22. 54 22
      server/routers/role/listRoles.ts
  23. 42 18
      server/routers/role/removeRoleAction.ts
  24. 42 18
      server/routers/role/removeRoleResource.ts
  25. 51 23
      server/routers/role/removeRoleSite.ts
  26. 50 28
      server/routers/role/updateRole.ts
  27. 63 38
      server/routers/site/createSite.ts
  28. 44 23
      server/routers/site/deleteSite.ts
  29. 45 20
      server/routers/site/getSite.ts
  30. 33 15
      server/routers/site/listSiteRoles.ts
  31. 13 20
      server/routers/site/pickSiteDefaults.ts
  32. 49 28
      server/routers/site/updateSite.ts
  33. 58 33
      server/routers/target/createTarget.ts
  34. 51 29
      server/routers/target/deleteTarget.ts
  35. 35 16
      server/routers/target/getTarget.ts
  36. 47 26
      server/routers/target/updateTarget.ts
  37. 51 21
      server/routers/user/addUserAction.ts
  38. 0 87
      server/routers/user/addUserOrg.ts
  39. 40 19
      server/routers/user/addUserResource.ts
  40. 42 20
      server/routers/user/addUserSite.ts
  41. 0 1
      server/routers/user/index.ts
  42. 44 23
      server/routers/user/removeUserAction.ts
  43. 35 16
      server/routers/user/removeUserOrg.ts
  44. 42 18
      server/routers/user/removeUserResource.ts
  45. 49 21
      server/routers/user/removeUserSite.ts
  46. 44 19
      server/routers/user/setUserRole.ts
  47. 1 1
      src/app/[orgId]/settings/components/Header.tsx
  48. 14 7
      src/app/[orgId]/settings/layout.tsx
  49. 1 1
      src/app/[orgId]/settings/resources/[resourceId]/components/CreateResource.tsx
  50. 21 1
      src/app/[orgId]/settings/resources/[resourceId]/components/GeneralForm.tsx
  51. 2 2
      src/app/[orgId]/settings/sites/[niceId]/components/CreateSite.tsx
  52. 18 1
      src/app/[orgId]/settings/sites/[niceId]/components/GeneralForm.tsx
  53. 1 1
      src/app/[orgId]/settings/sites/components/SitesTable.tsx
  54. 157 108
      src/app/[orgId]/settings/users/components/InviteUserForm.tsx
  55. 4 21
      src/app/[orgId]/settings/users/components/UsersTable.tsx
  56. 32 6
      src/app/[orgId]/settings/users/page.tsx
  57. 5 1
      src/app/auth/login/page.tsx
  58. 5 1
      src/app/auth/signup/page.tsx
  59. 1 1
      src/app/auth/verify-email/VerifyEmailForm.tsx
  60. 5 1
      src/app/auth/verify-email/page.tsx
  61. 5 1
      src/app/page.tsx
  62. 1 1
      src/app/profile/account/account-form.tsx
  63. 1 1
      src/app/profile/appearance/appearance-form.tsx
  64. 1 1
      src/app/profile/display/display-form.tsx
  65. 1 1
      src/app/profile/notifications/notifications-form.tsx
  66. 1 1
      src/app/profile/profile-form.tsx
  67. 1 1
      src/app/setup/page.tsx
  68. 1 1
      src/components/account-form.tsx
  69. 160 150
      src/components/appearance-form.tsx
  70. 1 1
      src/components/display-form.tsx
  71. 1 1
      src/components/notifications-form.tsx
  72. 1 1
      src/components/profile-form.tsx
  73. 1 1
      src/components/ui/toaster.tsx
  74. 11 0
      src/contexts/orgContext.ts
  75. 5 3
      src/contexts/resourceContext.ts
  76. 2 2
      src/contexts/siteContext.ts
  77. 10 0
      src/hooks/useOrgContext.ts
  78. 4 2
      src/hooks/useResourceContext.ts
  79. 0 0
      src/hooks/useToast.ts
  80. 1 1
      src/lib/auth/verifySession.ts
  81. 39 0
      src/providers/OrgProvider.tsx
  82. 26 27
      src/providers/ResourceProvider.tsx
  83. 22 27
      src/providers/SiteProvider.tsx

+ 2 - 1
server/config.ts

@@ -120,7 +120,8 @@ if (!parsedConfig.success) {
     throw new Error(`Invalid configuration file: ${errors}`);
 }
 
-process.env.SERVER_EXTERNAL_PORT = parsedConfig.data.server.external_port.toString();
+process.env.SERVER_EXTERNAL_PORT =
+    parsedConfig.data.server.external_port.toString();
 process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.data.flags
     ?.require_email_verification
     ? "true"

+ 19 - 25
server/routers/external.ts

@@ -47,7 +47,7 @@ authenticated.get("/org/:orgId/sites", verifyOrgAccess, site.listSites);
 authenticated.get("/org/:orgId/site/:niceId", verifyOrgAccess, site.getSite);
 
 authenticated.get(
-    "/org/:orgId/pickSiteDefaults",
+    "/org/:orgId/pick-site-defaults",
     verifyOrgAccess,
     site.pickSiteDefaults
 );
@@ -118,12 +118,12 @@ authenticated.delete(
     target.deleteTarget
 );
 
-authenticated.put(
-    "/org/:orgId/role",
-    verifyOrgAccess,
-    verifySuperuser,
-    role.createRole
-);
+// authenticated.put(
+//     "/org/:orgId/role",
+//     verifyOrgAccess,
+//     verifySuperuser,
+//     role.createRole
+// );
 authenticated.get("/org/:orgId/roles", verifyOrgAccess, role.listRoles);
 authenticated.get(
     "/role/:roleId",
@@ -131,18 +131,18 @@ authenticated.get(
     verifyUserInRole,
     role.getRole
 );
-authenticated.post(
-    "/role/:roleId",
-    verifyRoleAccess,
-    verifySuperuser,
-    role.updateRole
-);
-authenticated.delete(
-    "/role/:roleId",
-    verifyRoleAccess,
-    verifySuperuser,
-    role.deleteRole
-);
+// authenticated.post(
+//     "/role/:roleId",
+//     verifyRoleAccess,
+//     verifySuperuser,
+//     role.updateRole
+// );
+// authenticated.delete(
+//     "/role/:roleId",
+//     verifyRoleAccess,
+//     verifySuperuser,
+//     role.deleteRole
+// );
 
 authenticated.put(
     "/role/:roleId/site",
@@ -210,12 +210,6 @@ authenticated.delete(
     verifyUserAccess,
     user.removeUserOrg
 );
-authenticated.put(
-    "/org/:orgId/user/:userId",
-    verifyOrgAccess,
-    verifyUserAccess,
-    user.addUserOrg
-);
 
 authenticated.put(
     "/user/:userId/site",

+ 2 - 1
server/routers/gerbil/getConfig.ts

@@ -10,6 +10,7 @@ import logger from '@server/logger';
 import config from "@server/config";
 import { getUniqueExitNodeEndpointName } from '@server/db/names';
 import { findNextAvailableCidr } from "@server/utils/ip";
+import { fromError } from 'zod-validation-error';
 // Define Zod schema for request validation
 const getConfigSchema = z.object({
     publicKey: z.string(),
@@ -33,7 +34,7 @@ export async function getConfig(req: Request, res: Response, next: NextFunction)
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString() 
                 )
             );
         }

+ 24 - 13
server/routers/org/checkId.ts

@@ -1,32 +1,38 @@
-import { Request, Response, NextFunction } from 'express';
-import { z } from 'zod';
-import { db } from '@server/db';
-import { orgs } from '@server/db/schema';
-import { eq } from 'drizzle-orm';
+import { Request, Response, NextFunction } from "express";
+import { z } from "zod";
+import { db } from "@server/db";
+import { orgs } 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 logger from '@server/logger';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
 
 const getOrgSchema = z.object({
-    orgId: z.string()
+    orgId: z.string(),
 });
 
-export async function checkId(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function checkId(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedQuery = getOrgSchema.safeParse(req.query);
         if (!parsedQuery.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedQuery.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedQuery.error).toString()
                 )
             );
         }
 
         const { orgId } = parsedQuery.data;
 
-        const org = await db.select()
+        const org = await db
+            .select()
             .from(orgs)
             .where(eq(orgs.orgId, orgId))
             .limit(1);
@@ -50,6 +56,11 @@ export async function checkId(req: Request, res: Response, next: NextFunction):
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
 }

+ 2 - 1
server/routers/org/createOrg.ts

@@ -10,6 +10,7 @@ import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions';
 import logger from '@server/logger';
 import { createSuperuserRole } from '@server/db/ensureActions';
 import config, { APP_PATH } from "@server/config";
+import { fromError } from 'zod-validation-error';
 
 const createOrgSchema = z.object({
     orgId: z.string(),
@@ -26,7 +27,7 @@ export async function createOrg(req: Request, res: Response, next: NextFunction)
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedBody.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedBody.error).toString()
                 )
             );
         }

+ 2 - 1
server/routers/org/deleteOrg.ts

@@ -8,6 +8,7 @@ import HttpCode from '@server/types/HttpCode';
 import createHttpError from 'http-errors';
 import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions';
 import logger from '@server/logger';
+import { fromError } from 'zod-validation-error';
 
 const deleteOrgSchema = z.object({
     orgId: z.string()
@@ -20,7 +21,7 @@ export async function deleteOrg(req: Request, res: Response, next: NextFunction)
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }

+ 3 - 2
server/routers/org/updateOrg.ts

@@ -8,6 +8,7 @@ import HttpCode from '@server/types/HttpCode';
 import createHttpError from 'http-errors';
 import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions';
 import logger from '@server/logger';
+import { fromError } from 'zod-validation-error';
 
 const updateOrgParamsSchema = z.object({
     orgId: z.string()
@@ -27,7 +28,7 @@ export async function updateOrg(req: Request, res: Response, next: NextFunction)
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -37,7 +38,7 @@ export async function updateOrg(req: Request, res: Response, next: NextFunction)
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedBody.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedBody.error).toString()
                 )
             );
         }

+ 3 - 2
server/routers/resource/createResource.ts

@@ -9,6 +9,7 @@ import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions';
 import logger from '@server/logger';
 import { eq, and } from 'drizzle-orm';
 import stoi from '@server/utils/stoi';
+import { fromError } from 'zod-validation-error';
 
 const createResourceParamsSchema = z.object({
     siteId: z.string().optional().transform(stoi).pipe(z.number().int().positive().optional()),
@@ -29,7 +30,7 @@ export async function createResource(req: Request, res: Response, next: NextFunc
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedBody.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedBody.error).toString()
                 )
             );
         }
@@ -42,7 +43,7 @@ export async function createResource(req: Request, res: Response, next: NextFunc
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }

+ 34 - 15
server/routers/resource/deleteResource.ts

@@ -1,20 +1,25 @@
-import { Request, Response, NextFunction } from 'express';
-import { z } from 'zod';
-import { db } from '@server/db';
-import { resources } from '@server/db/schema';
-import { eq } from 'drizzle-orm';
+import { Request, Response, NextFunction } from "express";
+import { z } from "zod";
+import { db } from "@server/db";
+import { 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';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
 
 // Define Zod schema for request parameters validation
 const deleteResourceSchema = z.object({
     resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
 });
 
-export async function deleteResource(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function deleteResource(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         // Validate request parameters
         const parsedParams = deleteResourceSchema.safeParse(req.params);
@@ -22,7 +27,7 @@ export async function deleteResource(req: Request, res: Response, next: NextFunc
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -30,13 +35,22 @@ export async function deleteResource(req: Request, res: Response, next: NextFunc
         const { resourceId } = parsedParams.data;
 
         // Check if the user has permission to list sites
-        const hasPermission = await checkUserActionPermission(ActionsEnum.deleteResource, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.deleteResource,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "User does not have permission to perform this action"
+                )
+            );
         }
 
         // Delete the resource from the database
-        const deletedResource = await db.delete(resources)
+        const deletedResource = await db
+            .delete(resources)
             .where(eq(resources.resourceId, resourceId))
             .returning();
 
@@ -58,6 +72,11 @@ export async function deleteResource(req: Request, res: Response, next: NextFunc
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
 }

+ 36 - 17
server/routers/resource/getResource.ts

@@ -1,13 +1,14 @@
-import { Request, Response, NextFunction } from 'express';
-import { z } from 'zod';
-import { db } from '@server/db';
-import { resources } from '@server/db/schema';
-import { eq } from 'drizzle-orm';
+import { Request, Response, NextFunction } from "express";
+import { z } from "zod";
+import { db } from "@server/db";
+import { 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';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
 
 // Define Zod schema for request parameters validation
 const getResourceSchema = z.object({
@@ -19,9 +20,13 @@ export type GetResourceResponse = {
     siteId: number;
     orgId: string;
     name: string;
-}
+};
 
-export async function getResource(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function getResource(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         // Validate request parameters
         const parsedParams = getResourceSchema.safeParse(req.params);
@@ -29,7 +34,7 @@ export async function getResource(req: Request, res: Response, next: NextFunctio
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -37,13 +42,22 @@ export async function getResource(req: Request, res: Response, next: NextFunctio
         const { resourceId } = parsedParams.data;
 
         // Check if the user has permission to list sites
-        const hasPermission = await checkUserActionPermission(ActionsEnum.getResource, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.getResource,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "User does not have permission to perform this action"
+                )
+            );
         }
 
         // Fetch the resource from the database
-        const resource = await db.select()
+        const resource = await db
+            .select()
             .from(resources)
             .where(eq(resources.resourceId, resourceId))
             .limit(1);
@@ -62,7 +76,7 @@ export async function getResource(req: Request, res: Response, next: NextFunctio
                 resourceId: resource[0].resourceId,
                 siteId: resource[0].siteId,
                 orgId: resource[0].orgId,
-                name: resource[0].name
+                name: resource[0].name,
             },
             success: true,
             error: false,
@@ -71,6 +85,11 @@ export async function getResource(req: Request, res: Response, next: NextFunctio
         });
     } catch (error) {
         throw error;
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
 }

+ 33 - 15
server/routers/resource/listResourceRoles.ts

@@ -1,26 +1,31 @@
-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 { 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';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
 
 const listResourceRolesSchema = z.object({
     resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
 });
 
-export async function listResourceRoles(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function listResourceRoles(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedParams = listResourceRolesSchema.safeParse(req.params);
         if (!parsedParams.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -28,9 +33,17 @@ export async function listResourceRoles(req: Request, res: Response, next: NextF
         const { resourceId } = parsedParams.data;
 
         // Check if the user has permission to list resource roles
-        const hasPermission = await checkUserActionPermission(ActionsEnum.listResourceRoles, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.listResourceRoles,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "User does not have permission to perform this action"
+                )
+            );
         }
 
         const resourceRolesList = await db
@@ -53,6 +66,11 @@ export async function listResourceRoles(req: Request, res: Response, next: NextF
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
-}
+}

+ 43 - 22
server/routers/resource/updateResource.ts

@@ -1,13 +1,14 @@
-import { Request, Response, NextFunction } from 'express';
-import { z } from 'zod';
-import { db } from '@server/db';
-import { resources } from '@server/db/schema';
-import { eq } from 'drizzle-orm';
+import { Request, Response, NextFunction } from "express";
+import { z } from "zod";
+import { db } from "@server/db";
+import { 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';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
 
 // Define Zod schema for request parameters validation
 const updateResourceParamsSchema = z.object({
@@ -15,14 +16,20 @@ const updateResourceParamsSchema = z.object({
 });
 
 // Define Zod schema for request body validation
-const updateResourceBodySchema = z.object({
-    name: z.string().min(1).max(255).optional(),
-    subdomain: z.string().min(1).max(255).optional(),
-}).refine(data => Object.keys(data).length > 0, {
-    message: "At least one field must be provided for update"
-});
+const updateResourceBodySchema = z
+    .object({
+        name: z.string().min(1).max(255).optional(),
+        subdomain: z.string().min(1).max(255).optional(),
+    })
+    .refine((data) => Object.keys(data).length > 0, {
+        message: "At least one field must be provided for update",
+    });
 
-export async function updateResource(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function updateResource(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         // Validate request parameters
         const parsedParams = updateResourceParamsSchema.safeParse(req.params);
@@ -30,7 +37,7 @@ export async function updateResource(req: Request, res: Response, next: NextFunc
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -41,7 +48,7 @@ export async function updateResource(req: Request, res: Response, next: NextFunc
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedBody.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedBody.error).toString()
                 )
             );
         }
@@ -50,13 +57,22 @@ export async function updateResource(req: Request, res: Response, next: NextFunc
         const updateData = parsedBody.data;
 
         // Check if the user has permission to list sites
-        const hasPermission = await checkUserActionPermission(ActionsEnum.updateResource, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.updateResource,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "User does not have permission to perform this action"
+                )
+            );
         }
 
         // Update the resource in the database
-        const updatedResource = await db.update(resources)
+        const updatedResource = await db
+            .update(resources)
             .set(updateData)
             .where(eq(resources.resourceId, resourceId))
             .returning();
@@ -79,6 +95,11 @@ export async function updateResource(req: Request, res: Response, next: NextFunc
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
 }

+ 53 - 23
server/routers/role/addRoleAction.ts

@@ -1,13 +1,14 @@
-import { Request, Response, NextFunction } from 'express';
-import { z } from 'zod';
-import { db } from '@server/db';
-import { roleActions, roles } from '@server/db/schema';
+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';
-import { eq } from 'drizzle-orm';
+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";
+import { fromError } from "zod-validation-error";
 
 const addRoleActionParamSchema = z.object({
     roleId: z.string().transform(Number).pipe(z.number().int().positive()),
@@ -17,14 +18,18 @@ const addRoleActionSchema = z.object({
     actionId: z.string(),
 });
 
-export async function addRoleAction(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function addRoleAction(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedBody = addRoleActionSchema.safeParse(req.body);
         if (!parsedBody.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedBody.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedBody.error).toString()
                 )
             );
         }
@@ -36,7 +41,7 @@ export async function addRoleAction(req: Request, res: Response, next: NextFunct
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -44,22 +49,42 @@ export async function addRoleAction(req: Request, res: Response, next: NextFunct
         const { roleId } = parsedParams.data;
 
         // Check if the user has permission to add role actions
-        const hasPermission = await checkUserActionPermission(ActionsEnum.addRoleAction, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.addRoleAction,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            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);
+        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`));
+            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();
+        const newRoleAction = await db
+            .insert(roleActions)
+            .values({
+                roleId,
+                actionId,
+                orgId: role[0].orgId!,
+            })
+            .returning();
 
         return response(res, {
             data: newRoleAction[0],
@@ -70,6 +95,11 @@ export async function addRoleAction(req: Request, res: Response, next: NextFunct
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
-}
+}

+ 40 - 19
server/routers/role/addRoleResource.ts

@@ -1,12 +1,13 @@
-import { Request, Response, NextFunction } from 'express';
-import { z } from 'zod';
-import { db } from '@server/db';
-import { roleResources } from '@server/db/schema';
+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';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
 
 const addRoleResourceParamsSchema = z.object({
     roleId: z.string().transform(Number).pipe(z.number().int().positive()),
@@ -16,14 +17,18 @@ const addRoleResourceSchema = z.object({
     resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
 });
 
-export async function addRoleResource(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function addRoleResource(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedBody = addRoleResourceSchema.safeParse(req.body);
         if (!parsedBody.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedBody.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedBody.error).toString()
                 )
             );
         }
@@ -35,7 +40,7 @@ export async function addRoleResource(req: Request, res: Response, next: NextFun
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -43,15 +48,26 @@ export async function addRoleResource(req: Request, res: Response, next: NextFun
         const { roleId } = parsedParams.data;
 
         // Check if the user has permission to add role resources
-        const hasPermission = await checkUserActionPermission(ActionsEnum.addRoleResource, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.addRoleResource,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "User does not have permission to perform this action"
+                )
+            );
         }
 
-        const newRoleResource = await db.insert(roleResources).values({
-            roleId,
-            resourceId,
-        }).returning();
+        const newRoleResource = await db
+            .insert(roleResources)
+            .values({
+                roleId,
+                resourceId,
+            })
+            .returning();
 
         return response(res, {
             data: newRoleResource[0],
@@ -62,6 +78,11 @@ export async function addRoleResource(req: Request, res: Response, next: NextFun
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
-}
+}

+ 43 - 21
server/routers/role/addRoleSite.ts

@@ -1,13 +1,14 @@
-import { Request, Response, NextFunction } from 'express';
-import { z } from 'zod';
-import { db } from '@server/db';
-import { resources, roleResources, roleSites } from '@server/db/schema';
+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';
+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";
+import { fromError } from "zod-validation-error";
 
 const addRoleSiteParamsSchema = z.object({
     roleId: z.string().transform(Number).pipe(z.number().int().positive()),
@@ -17,14 +18,18 @@ 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<any> {
+export async function addRoleSite(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedBody = addRoleSiteSchema.safeParse(req.body);
         if (!parsedBody.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedBody.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedBody.error).toString()
                 )
             );
         }
@@ -36,7 +41,7 @@ export async function addRoleSite(req: Request, res: Response, next: NextFunctio
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -44,17 +49,29 @@ export async function addRoleSite(req: Request, res: Response, next: NextFunctio
         const { roleId } = parsedParams.data;
 
         // Check if the user has permission to add role sites
-        const hasPermission = await checkUserActionPermission(ActionsEnum.addRoleSite, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.addRoleSite,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            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 newRoleSite = await db
+            .insert(roleSites)
+            .values({
+                roleId,
+                siteId,
+            })
+            .returning();
 
-        const siteResources = await db.select()
+        const siteResources = await db
+            .select()
             .from(resources)
             .where(eq(resources.siteId, siteId));
 
@@ -74,6 +91,11 @@ export async function addRoleSite(req: Request, res: Response, next: NextFunctio
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
-}
+}

+ 41 - 20
server/routers/role/createRole.ts

@@ -1,15 +1,16 @@
-import { Request, Response, NextFunction } from 'express';
-import { z } from 'zod';
-import { db } from '@server/db';
-import { roles } from '@server/db/schema';
+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';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
 
 const createRoleParamsSchema = z.object({
-    orgId: z.string()
+    orgId: z.string(),
 });
 
 const createRoleSchema = z.object({
@@ -17,14 +18,18 @@ const createRoleSchema = z.object({
     description: z.string().optional(),
 });
 
-export async function createRole(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function createRole(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedBody = createRoleSchema.safeParse(req.body);
         if (!parsedBody.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedBody.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedBody.error).toString()
                 )
             );
         }
@@ -36,7 +41,7 @@ export async function createRole(req: Request, res: Response, next: NextFunction
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -44,15 +49,26 @@ export async function createRole(req: Request, res: Response, next: NextFunction
         const { orgId } = parsedParams.data;
 
         // Check if the user has permission to create roles
-        const hasPermission = await checkUserActionPermission(ActionsEnum.createRole, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.createRole,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "User does not have permission to perform this action"
+                )
+            );
         }
 
-        const newRole = await db.insert(roles).values({
-            ...roleData,
-            orgId,
-        }).returning();
+        const newRole = await db
+            .insert(roles)
+            .values({
+                ...roleData,
+                orgId,
+            })
+            .returning();
 
         return response(res, {
             data: newRole[0],
@@ -63,6 +79,11 @@ export async function createRole(req: Request, res: Response, next: NextFunction
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
-}
+}

+ 41 - 21
server/routers/role/deleteRole.ts

@@ -1,26 +1,31 @@
-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 { 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';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
 
 const deleteRoleSchema = z.object({
-    roleId: z.string().transform(Number).pipe(z.number().int().positive())
+    roleId: z.string().transform(Number).pipe(z.number().int().positive()),
 });
 
-export async function deleteRole(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function deleteRole(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedParams = deleteRoleSchema.safeParse(req.params);
         if (!parsedParams.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -28,15 +33,24 @@ export async function deleteRole(req: Request, res: Response, next: NextFunction
         const { roleId } = parsedParams.data;
 
         // Check if the user has permission to delete roles
-        const hasPermission = await checkUserActionPermission(ActionsEnum.deleteRole, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.deleteRole,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            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);
+        const role = await db
+            .select()
+            .from(roles)
+            .where(eq(roles.roleId, roleId))
+            .limit(1);
 
         if (role.length === 0) {
             return next(
@@ -56,7 +70,8 @@ export async function deleteRole(req: Request, res: Response, next: NextFunction
             );
         }
 
-        const deletedRole = await db.delete(roles)
+        const deletedRole = await db
+            .delete(roles)
             .where(eq(roles.roleId, roleId))
             .returning();
 
@@ -78,6 +93,11 @@ export async function deleteRole(req: Request, res: Response, next: NextFunction
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
-}
+}

+ 36 - 17
server/routers/role/getRole.ts

@@ -1,26 +1,31 @@
-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 { 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';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
 
 const getRoleSchema = z.object({
-    roleId: z.string().transform(Number).pipe(z.number().int().positive())
+    roleId: z.string().transform(Number).pipe(z.number().int().positive()),
 });
 
-export async function getRole(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function getRole(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedParams = getRoleSchema.safeParse(req.params);
         if (!parsedParams.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -28,12 +33,21 @@ export async function getRole(req: Request, res: Response, next: NextFunction):
         const { roleId } = parsedParams.data;
 
         // Check if the user has permission to get roles
-        const hasPermission = await checkUserActionPermission(ActionsEnum.getRole, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.getRole,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "User does not have permission to perform this action"
+                )
+            );
         }
 
-        const role = await db.select()
+        const role = await db
+            .select()
             .from(roles)
             .where(eq(roles.roleId, roleId))
             .limit(1);
@@ -56,6 +70,11 @@ export async function getRole(req: Request, res: Response, next: NextFunction):
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
-}
+}

+ 34 - 16
server/routers/role/listRoleActions.ts

@@ -1,26 +1,31 @@
-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 { 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';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
 
 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<any> {
+export async function listRoleActions(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedParams = listRoleActionsSchema.safeParse(req.params);
         if (!parsedParams.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -28,9 +33,17 @@ export async function listRoleActions(req: Request, res: Response, next: NextFun
         const { roleId } = parsedParams.data;
 
         // Check if the user has permission to list role actions
-        const hasPermission = await checkUserActionPermission(ActionsEnum.listRoleActions, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.listRoleActions,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "User does not have permission to perform this action"
+                )
+            );
         }
 
         const roleActionsList = await db
@@ -43,7 +56,7 @@ export async function listRoleActions(req: Request, res: Response, next: NextFun
             .innerJoin(actions, eq(roleActions.actionId, actions.actionId))
             .where(eq(roleActions.roleId, roleId));
 
-            // TODO: Do we need to filter out what the user can see?
+        // TODO: Do we need to filter out what the user can see?
 
         return response(res, {
             data: roleActionsList,
@@ -54,6 +67,11 @@ export async function listRoleActions(req: Request, res: Response, next: NextFun
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
-}
+}

+ 38 - 17
server/routers/role/listRoleResources.ts

@@ -1,26 +1,31 @@
-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 { 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';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
 
 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<any> {
+export async function listRoleResources(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedParams = listRoleResourcesSchema.safeParse(req.params);
         if (!parsedParams.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -28,9 +33,17 @@ export async function listRoleResources(req: Request, res: Response, next: NextF
         const { roleId } = parsedParams.data;
 
         // Check if the user has permission to list role resources
-        const hasPermission = await checkUserActionPermission(ActionsEnum.listRoleResources, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.listRoleResources,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "User does not have permission to perform this action"
+                )
+            );
         }
 
         const roleResourcesList = await db
@@ -40,10 +53,13 @@ export async function listRoleResources(req: Request, res: Response, next: NextF
                 subdomain: resources.subdomain,
             })
             .from(roleResources)
-            .innerJoin(resources, eq(roleResources.resourceId, resources.resourceId))
+            .innerJoin(
+                resources,
+                eq(roleResources.resourceId, resources.resourceId)
+            )
             .where(eq(roleResources.roleId, roleId));
 
-            // TODO: Do we need to filter out what the user can see?
+        // TODO: Do we need to filter out what the user can see?
 
         return response(res, {
             data: roleResourcesList,
@@ -54,6 +70,11 @@ export async function listRoleResources(req: Request, res: Response, next: NextF
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
-}
+}

+ 34 - 16
server/routers/role/listRoleSites.ts

@@ -1,26 +1,31 @@
-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 { 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';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
 
 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<any> {
+export async function listRoleSites(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedParams = listRoleSitesSchema.safeParse(req.params);
         if (!parsedParams.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -28,9 +33,17 @@ export async function listRoleSites(req: Request, res: Response, next: NextFunct
         const { roleId } = parsedParams.data;
 
         // Check if the user has permission to list role sites
-        const hasPermission = await checkUserActionPermission(ActionsEnum.listRoleSites, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.listRoleSites,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "User does not have permission to perform this action"
+                )
+            );
         }
 
         const roleSitesList = await db
@@ -42,7 +55,7 @@ export async function listRoleSites(req: Request, res: Response, next: NextFunct
             .innerJoin(sites, eq(roleSites.siteId, sites.siteId))
             .where(eq(roleSites.roleId, roleId));
 
-            // TODO: Do we need to filter out what the user can see?
+        // TODO: Do we need to filter out what the user can see?
 
         return response(res, {
             data: roleSitesList,
@@ -53,6 +66,11 @@ export async function listRoleSites(req: Request, res: Response, next: NextFunct
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
-}
+}

+ 54 - 22
server/routers/role/listRoles.ts

@@ -1,32 +1,49 @@
-import { Request, Response, NextFunction } from 'express';
-import { z } from 'zod';
-import { db } from '@server/db';
-import { roles, orgs } from '@server/db/schema';
+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';
+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";
+import { fromError } from "zod-validation-error";
 
 const listRolesParamsSchema = z.object({
-    orgId: z.string()
+    orgId: z.string(),
 });
 
 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()),
+    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<any> {
+export async function listRoles(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedQuery = listRolesSchema.safeParse(req.query);
         if (!parsedQuery.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedQuery.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedQuery.error).toString()
                 )
             );
         }
@@ -38,7 +55,7 @@ export async function listRoles(req: Request, res: Response, next: NextFunction)
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -46,9 +63,17 @@ export async function listRoles(req: Request, res: Response, next: NextFunction)
         const { orgId } = parsedParams.data;
 
         // Check if the user has permission to list roles
-        const hasPermission = await checkUserActionPermission(ActionsEnum.listRoles, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.listRoles,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "User does not have permission to perform this action"
+                )
+            );
         }
 
         let baseQuery: any = db
@@ -64,8 +89,10 @@ export async function listRoles(req: Request, res: Response, next: NextFunction)
             .leftJoin(orgs, eq(roles.orgId, orgs.orgId))
             .where(eq(roles.orgId, orgId));
 
-        let countQuery: any = db.select({ count: sql<number>`cast(count(*) as integer)` }).from(roles)
-        .where(eq(roles.orgId, orgId));
+        let countQuery: any = db
+            .select({ count: sql<number>`cast(count(*) as integer)` })
+            .from(roles)
+            .where(eq(roles.orgId, orgId));
 
         const rolesList = await baseQuery.limit(limit).offset(offset);
         const totalCountResult = await countQuery;
@@ -87,6 +114,11 @@ export async function listRoles(req: Request, res: Response, next: NextFunction)
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
-}
+}

+ 42 - 18
server/routers/role/removeRoleAction.ts

@@ -1,13 +1,14 @@
-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 { 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';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
 
 const removeRoleActionParamsSchema = z.object({
     roleId: z.string().transform(Number).pipe(z.number().int().positive()),
@@ -17,14 +18,18 @@ const removeRoleActionSchema = z.object({
     actionId: z.string(),
 });
 
-export async function removeRoleAction(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function removeRoleAction(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedParams = removeRoleActionSchema.safeParse(req.params);
         if (!parsedParams.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -36,7 +41,7 @@ export async function removeRoleAction(req: Request, res: Response, next: NextFu
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedBody.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedBody.error).toString()
                 )
             );
         }
@@ -44,13 +49,27 @@ export async function removeRoleAction(req: Request, res: Response, next: NextFu
         const { roleId } = parsedBody.data;
 
         // Check if the user has permission to remove role actions
-        const hasPermission = await checkUserActionPermission(ActionsEnum.removeRoleAction, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.removeRoleAction,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            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)))
+        const deletedRoleAction = await db
+            .delete(roleActions)
+            .where(
+                and(
+                    eq(roleActions.roleId, roleId),
+                    eq(roleActions.actionId, actionId)
+                )
+            )
             .returning();
 
         if (deletedRoleAction.length === 0) {
@@ -71,6 +90,11 @@ export async function removeRoleAction(req: Request, res: Response, next: NextFu
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
-}
+}

+ 42 - 18
server/routers/role/removeRoleResource.ts

@@ -1,13 +1,14 @@
-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 { 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';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
 
 const removeRoleResourceParamsSchema = z.object({
     roleId: z.string().transform(Number).pipe(z.number().int().positive()),
@@ -17,14 +18,18 @@ const removeRoleResourceSchema = z.object({
     resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
 });
 
-export async function removeRoleResource(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function removeRoleResource(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedParams = removeRoleResourceSchema.safeParse(req.params);
         if (!parsedParams.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -36,7 +41,7 @@ export async function removeRoleResource(req: Request, res: Response, next: Next
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedBody.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedBody.error).toString()
                 )
             );
         }
@@ -44,13 +49,27 @@ export async function removeRoleResource(req: Request, res: Response, next: Next
         const { roleId } = parsedBody.data;
 
         // Check if the user has permission to remove role resources
-        const hasPermission = await checkUserActionPermission(ActionsEnum.removeRoleResource, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.removeRoleResource,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            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)))
+        const deletedRoleResource = await db
+            .delete(roleResources)
+            .where(
+                and(
+                    eq(roleResources.roleId, roleId),
+                    eq(roleResources.resourceId, resourceId)
+                )
+            )
             .returning();
 
         if (deletedRoleResource.length === 0) {
@@ -71,6 +90,11 @@ export async function removeRoleResource(req: Request, res: Response, next: Next
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
-}
+}

+ 51 - 23
server/routers/role/removeRoleSite.ts

@@ -1,13 +1,14 @@
-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 { 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';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
 
 const removeRoleSiteParamsSchema = z.object({
     roleId: z.string().transform(Number).pipe(z.number().int().positive()),
@@ -17,14 +18,18 @@ 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<any> {
+export async function removeRoleSite(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedParams = removeRoleSiteSchema.safeParse(req.params);
         if (!parsedParams.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -36,7 +41,7 @@ export async function removeRoleSite(req: Request, res: Response, next: NextFunc
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedBody.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedBody.error).toString()
                 )
             );
         }
@@ -44,13 +49,24 @@ export async function removeRoleSite(req: Request, res: Response, next: NextFunc
         const { roleId } = parsedBody.data;
 
         // Check if the user has permission to remove role sites
-        const hasPermission = await checkUserActionPermission(ActionsEnum.removeRoleSite, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.removeRoleSite,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            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)))
+        const deletedRoleSite = await db
+            .delete(roleSites)
+            .where(
+                and(eq(roleSites.roleId, roleId), eq(roleSites.siteId, siteId))
+            )
             .returning();
 
         if (deletedRoleSite.length === 0) {
@@ -62,13 +78,20 @@ export async function removeRoleSite(req: Request, res: Response, next: NextFunc
             );
         }
 
-        const siteResources = await db.select()
-        .from(resources)
-        .where(eq(resources.siteId, siteId));
+        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)))
+            await db
+                .delete(roleResources)
+                .where(
+                    and(
+                        eq(roleResources.roleId, roleId),
+                        eq(roleResources.resourceId, resource.resourceId)
+                    )
+                )
                 .returning();
         }
 
@@ -81,6 +104,11 @@ export async function removeRoleSite(req: Request, res: Response, next: NextFunc
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
-}
+}

+ 50 - 28
server/routers/role/updateRole.ts

@@ -1,33 +1,40 @@
-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 { 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';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
 
 const updateRoleParamsSchema = z.object({
-    roleId: z.string().transform(Number).pipe(z.number().int().positive())
+    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"
-});
+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<any> {
+export async function updateRole(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedParams = updateRoleParamsSchema.safeParse(req.params);
         if (!parsedParams.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -37,7 +44,7 @@ export async function updateRole(req: Request, res: Response, next: NextFunction
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedBody.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedBody.error).toString()
                 )
             );
         }
@@ -46,15 +53,24 @@ export async function updateRole(req: Request, res: Response, next: NextFunction
         const updateData = parsedBody.data;
 
         // Check if the user has permission to update roles
-        const hasPermission = await checkUserActionPermission(ActionsEnum.updateRole, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.updateRole,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            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);
+        const role = await db
+            .select()
+            .from(roles)
+            .where(eq(roles.roleId, roleId))
+            .limit(1);
 
         if (role.length === 0) {
             return next(
@@ -74,7 +90,8 @@ export async function updateRole(req: Request, res: Response, next: NextFunction
             );
         }
 
-        const updatedRole = await db.update(roles)
+        const updatedRole = await db
+            .update(roles)
             .set(updateData)
             .where(eq(roles.roleId, roleId))
             .returning();
@@ -97,6 +114,11 @@ export async function updateRole(req: Request, res: Response, next: NextFunction
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
-}
+}

+ 63 - 38
server/routers/site/createSite.ts

@@ -1,20 +1,25 @@
-import { Request, Response, NextFunction } from 'express';
-import { z } from 'zod';
-import { db } from '@server/db';
-import { roles, userSites, sites, roleSites, exitNodes } from '@server/db/schema';
+import { Request, Response, NextFunction } from "express";
+import { z } from "zod";
+import { db } from "@server/db";
+import {
+    roles,
+    userSites,
+    sites,
+    roleSites,
+    exitNodes,
+} 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';
-import { getUniqueSiteName } from '@server/db/names';
-import { addPeer } from '../gerbil/peers';
-
-const API_BASE_URL = "http://localhost:3000";
+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";
+import { getUniqueSiteName } from "@server/db/names";
+import { addPeer } from "../gerbil/peers";
+import { fromError } from "zod-validation-error";
 
 const createSiteParamsSchema = z.object({
-    orgId: z.string()
+    orgId: z.string(),
 });
 
 // Define Zod schema for request body validation
@@ -36,7 +41,11 @@ export type CreateSiteResponse = {
     // subnet: string;
 };
 
-export async function createSite(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function createSite(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         // Validate request body
         const parsedBody = createSiteSchema.safeParse(req.body);
@@ -44,7 +53,7 @@ export async function createSite(req: Request, res: Response, next: NextFunction
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedBody.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedBody.error).toString()
                 )
             );
         }
@@ -57,7 +66,7 @@ export async function createSite(req: Request, res: Response, next: NextFunction
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -65,38 +74,49 @@ export async function createSite(req: Request, res: Response, next: NextFunction
         const { orgId } = parsedParams.data;
 
         // Check if the user has permission to list sites
-        const hasPermission = await checkUserActionPermission(ActionsEnum.createSite, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.createSite,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission perform this action'));
+            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'));
+            return next(
+                createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
+            );
         }
 
         const niceId = await getUniqueSiteName(orgId);
 
         // Create new site in the database
-        const [newSite] = await db.insert(sites).values({
-            orgId,
-            exitNodeId,
-            name,
-            niceId,
-            pubKey,
-            subnet,
-        }).returning();
+        const [newSite] = await db
+            .insert(sites)
+            .values({
+                orgId,
+                exitNodeId,
+                name,
+                niceId,
+                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);
+        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`
-                )
+                createHttpError(HttpCode.NOT_FOUND, `Superuser role not found`)
             );
         }
 
@@ -135,6 +155,11 @@ export async function createSite(req: Request, res: Response, next: NextFunction
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
-}
+}

+ 44 - 23
server/routers/site/deleteSite.ts

@@ -1,23 +1,28 @@
-import { Request, Response, NextFunction } from 'express';
-import { z } from 'zod';
-import { db } from '@server/db';
-import { sites } from '@server/db/schema';
-import { eq } from 'drizzle-orm';
+import { Request, Response, NextFunction } from "express";
+import { z } from "zod";
+import { db } from "@server/db";
+import { 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';
-import { deletePeer } from '../gerbil/peers';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { deletePeer } from "../gerbil/peers";
+import { fromError } from "zod-validation-error";
 
 const API_BASE_URL = "http://localhost:3000";
 
 // Define Zod schema for request parameters validation
 const deleteSiteSchema = z.object({
-    siteId: z.string().transform(Number).pipe(z.number().int().positive())
+    siteId: z.string().transform(Number).pipe(z.number().int().positive()),
 });
 
-export async function deleteSite(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function deleteSite(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         // Validate request parameters
         const parsedParams = deleteSiteSchema.safeParse(req.params);
@@ -25,7 +30,7 @@ export async function deleteSite(req: Request, res: Response, next: NextFunction
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -33,13 +38,22 @@ export async function deleteSite(req: Request, res: Response, next: NextFunction
         const { siteId } = parsedParams.data;
 
         // Check if the user has permission to list sites
-        const hasPermission = await checkUserActionPermission(ActionsEnum.deleteSite, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.deleteSite,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "User does not have permission to perform this action"
+                )
+            );
         }
 
         // Delete the site from the database
-        const [deletedSite] = await db.delete(sites)
+        const [deletedSite] = await db
+            .delete(sites)
             .where(eq(sites.siteId, siteId))
             .returning();
 
@@ -63,26 +77,33 @@ export async function deleteSite(req: Request, res: Response, next: NextFunction
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
 }
 
-
 async function removePeer(publicKey: string) {
     try {
-        const response = await fetch(`${API_BASE_URL}/peer?public_key=${encodeURIComponent(publicKey)}`, {
-            method: 'DELETE',
-        });
+        const response = await fetch(
+            `${API_BASE_URL}/peer?public_key=${encodeURIComponent(publicKey)}`,
+            {
+                method: "DELETE",
+            }
+        );
 
         if (!response.ok) {
             throw new Error(`HTTP error! status: ${response.status}`);
         }
 
         const data = await response.json();
-        console.log('Peer removed successfully:', data.status);
+        console.log("Peer removed successfully:", data.status);
         return data;
     } catch (error: any) {
-        console.error('Error removing peer:', error.message);
+        console.error("Error removing peer:", error.message);
         throw error;
     }
 }

+ 45 - 20
server/routers/site/getSite.ts

@@ -1,18 +1,24 @@
-import { Request, Response, NextFunction } from 'express';
-import { z } from 'zod';
-import { db } from '@server/db';
-import { sites } from '@server/db/schema';
-import { eq, and } from 'drizzle-orm';
+import { Request, Response, NextFunction } from "express";
+import { z } from "zod";
+import { db } from "@server/db";
+import { sites } 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';
-import stoi from '@server/utils/stoi';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import stoi from "@server/utils/stoi";
+import { fromError } from "zod-validation-error";
 
 // Define Zod schema for request parameters validation
 const getSiteSchema = z.object({
-            siteId: z.string().optional().transform(stoi).pipe(z.number().int().positive().optional()).optional(),
+    siteId: z
+        .string()
+        .optional()
+        .transform(stoi)
+        .pipe(z.number().int().positive().optional())
+        .optional(),
     niceId: z.string().optional(),
     orgId: z.string().optional(),
 });
@@ -22,9 +28,13 @@ export type GetSiteResponse = {
     name: string;
     subdomain: string;
     subnet: string;
-}
+};
 
-export async function getSite(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function getSite(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         // Validate request parameters
         const parsedParams = getSiteSchema.safeParse(req.params);
@@ -32,7 +42,7 @@ export async function getSite(req: Request, res: Response, next: NextFunction):
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -40,27 +50,37 @@ export async function getSite(req: Request, res: Response, next: NextFunction):
         const { siteId, niceId, orgId } = parsedParams.data;
 
         // Check if the user has permission to list sites
-        const hasPermission = await checkUserActionPermission(ActionsEnum.updateSite, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.updateSite,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "User does not have permission to perform this action"
+                )
+            );
         }
 
         let site;
         // Fetch the site from the database
         if (siteId) {
-            site = await db.select()
+            site = await db
+                .select()
                 .from(sites)
                 .where(eq(sites.siteId, siteId))
                 .limit(1);
         } else if (niceId && orgId) {
-            site = await db.select()
+            site = await db
+                .select()
                 .from(sites)
                 .where(and(eq(sites.niceId, niceId), eq(sites.orgId, orgId)))
                 .limit(1);
         }
 
         if (!site) {
-            return next(createHttpError(HttpCode.NOT_FOUND, 'Site not found'));
+            return next(createHttpError(HttpCode.NOT_FOUND, "Site not found"));
         }
 
         if (site.length === 0) {
@@ -86,6 +106,11 @@ export async function getSite(req: Request, res: Response, next: NextFunction):
         });
     } catch (error) {
         logger.error("Error from getSite: ", error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
 }

+ 33 - 15
server/routers/site/listSiteRoles.ts

@@ -1,26 +1,31 @@
-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 { 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';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
 
 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<any> {
+export async function listSiteRoles(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedParams = listSiteRolesSchema.safeParse(req.params);
         if (!parsedParams.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -28,9 +33,17 @@ export async function listSiteRoles(req: Request, res: Response, next: NextFunct
         const { siteId } = parsedParams.data;
 
         // Check if the user has permission to list site roles
-        const hasPermission = await checkUserActionPermission(ActionsEnum.listSiteRoles, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.listSiteRoles,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "User does not have permission to perform this action"
+                )
+            );
         }
 
         const siteRolesList = await db
@@ -53,6 +66,11 @@ export async function listSiteRoles(req: Request, res: Response, next: NextFunct
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
-}
+}

+ 13 - 20
server/routers/site/pickSiteDefaults.ts

@@ -18,26 +18,25 @@ export type PickSiteDefaultsResponse = {
     listenPort: number;
     endpoint: string;
     subnet: string;
-}
+};
 
 export async function pickSiteDefaults(
     req: Request,
     res: Response,
-    next: NextFunction,
+    next: NextFunction
 ): Promise<any> {
     try {
-
         // Check if the user has permission to list sites
         const hasPermission = await checkUserActionPermission(
             ActionsEnum.createSite,
-            req,
+            req
         );
         if (!hasPermission) {
             return next(
                 createHttpError(
                     HttpCode.FORBIDDEN,
-                    "User does not have permission to perform this action",
-                ),
+                    "User does not have permission to perform this action"
+                )
             );
         }
 
@@ -47,10 +46,7 @@ export async function pickSiteDefaults(
         const nodes = await db.select().from(exitNodes);
         if (nodes.length === 0) {
             return next(
-                createHttpError(
-                    HttpCode.NOT_FOUND,
-                    "No exit nodes available",
-                ),
+                createHttpError(HttpCode.NOT_FOUND, "No exit nodes available")
             );
         }
 
@@ -59,9 +55,10 @@ export async function pickSiteDefaults(
 
         // TODO: this probably can be optimized...
         // list all of the sites on that exit node
-        const sitesQuery = await db.select({
-            subnet: sites.subnet
-        })
+        const sitesQuery = await db
+            .select({
+                subnet: sites.subnet,
+            })
             .from(sites)
             .where(eq(sites.exitNodeId, exitNode.exitNodeId));
 
@@ -74,8 +71,8 @@ export async function pickSiteDefaults(
             return next(
                 createHttpError(
                     HttpCode.INTERNAL_SERVER_ERROR,
-                    "No available subnets",
-                ),
+                    "No available subnets"
+                )
             );
         }
 
@@ -97,11 +94,7 @@ export async function pickSiteDefaults(
     } catch (error) {
         logger.error(error);
         return next(
-            createHttpError(
-                HttpCode.INTERNAL_SERVER_ERROR,
-                "An error occurred...",
-            ),
+            createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
         );
     }
 }
-

+ 49 - 28
server/routers/site/updateSite.ts

@@ -1,33 +1,40 @@
-import { Request, Response, NextFunction } from 'express';
-import { z } from 'zod';
-import { db } from '@server/db';
-import { sites } from '@server/db/schema';
-import { eq } from 'drizzle-orm';
+import { Request, Response, NextFunction } from "express";
+import { z } from "zod";
+import { db } from "@server/db";
+import { 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';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
 
 // Define Zod schema for request parameters validation
 const updateSiteParamsSchema = z.object({
-    siteId: z.string().transform(Number).pipe(z.number().int().positive())
+    siteId: z.string().transform(Number).pipe(z.number().int().positive()),
 });
 
 // Define Zod schema for request body validation
-const updateSiteBodySchema = z.object({
-    name: z.string().min(1).max(255).optional(),
-    subdomain: z.string().min(1).max(255).optional(),
-    pubKey: z.string().optional(),
-    subnet: z.string().optional(),
-    exitNode: z.number().int().positive().optional(),
-    megabytesIn: z.number().int().nonnegative().optional(),
-    megabytesOut: z.number().int().nonnegative().optional(),
-}).refine(data => Object.keys(data).length > 0, {
-    message: "At least one field must be provided for update"
-});
+const updateSiteBodySchema = z
+    .object({
+        name: z.string().min(1).max(255).optional(),
+        subdomain: z.string().min(1).max(255).optional(),
+        pubKey: z.string().optional(),
+        subnet: z.string().optional(),
+        exitNode: z.number().int().positive().optional(),
+        megabytesIn: z.number().int().nonnegative().optional(),
+        megabytesOut: z.number().int().nonnegative().optional(),
+    })
+    .refine((data) => Object.keys(data).length > 0, {
+        message: "At least one field must be provided for update",
+    });
 
-export async function updateSite(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function updateSite(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         // Validate request parameters
         const parsedParams = updateSiteParamsSchema.safeParse(req.params);
@@ -35,7 +42,7 @@ export async function updateSite(req: Request, res: Response, next: NextFunction
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -46,7 +53,7 @@ export async function updateSite(req: Request, res: Response, next: NextFunction
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedBody.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedBody.error).toString()
                 )
             );
         }
@@ -55,13 +62,22 @@ export async function updateSite(req: Request, res: Response, next: NextFunction
         const updateData = parsedBody.data;
 
         // Check if the user has permission to list sites
-        const hasPermission = await checkUserActionPermission(ActionsEnum.updateSite, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.updateSite,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "User does not have permission to perform this action"
+                )
+            );
         }
 
         // Update the site in the database
-        const updatedSite = await db.update(sites)
+        const updatedSite = await db
+            .update(sites)
             .set(updateData)
             .where(eq(sites.siteId, siteId))
             .returning();
@@ -84,6 +100,11 @@ export async function updateSite(req: Request, res: Response, next: NextFunction
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
 }

+ 58 - 33
server/routers/target/createTarget.ts

@@ -1,15 +1,16 @@
-import { Request, Response, NextFunction } from 'express';
-import { z } from 'zod';
-import { db } from '@server/db';
-import { resources, sites, targets } from '@server/db/schema';
+import { Request, Response, NextFunction } from "express";
+import { z } from "zod";
+import { db } from "@server/db";
+import { resources, sites, targets } 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 { addPeer } from '../gerbil/peers';
-import { eq, and } from 'drizzle-orm';
-import { isIpInCidr } from '@server/utils/ip';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { addPeer } from "../gerbil/peers";
+import { eq, and } from "drizzle-orm";
+import { isIpInCidr } from "@server/utils/ip";
+import { fromError } from "zod-validation-error";
 
 const createTargetParamsSchema = z.object({
     resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
@@ -23,14 +24,18 @@ const createTargetSchema = z.object({
     enabled: z.boolean().default(true),
 });
 
-export async function createTarget(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function createTarget(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedBody = createTargetSchema.safeParse(req.body);
         if (!parsedBody.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedBody.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedBody.error).toString()
                 )
             );
         }
@@ -42,7 +47,7 @@ export async function createTarget(req: Request, res: Response, next: NextFuncti
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -50,15 +55,24 @@ export async function createTarget(req: Request, res: Response, next: NextFuncti
         const { resourceId } = parsedParams.data;
 
         // Check if the user has permission to list sites
-        const hasPermission = await checkUserActionPermission(ActionsEnum.createTarget, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.createTarget,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "User does not have permission to perform this action"
+                )
+            );
         }
 
         // get the resource
-        const [resource] = await db.select({
-            siteId: resources.siteId,
-        })
+        const [resource] = await db
+            .select({
+                siteId: resources.siteId,
+            })
             .from(resources)
             .where(eq(resources.resourceId, resourceId));
 
@@ -72,9 +86,10 @@ export async function createTarget(req: Request, res: Response, next: NextFuncti
         }
 
         // TODO: is this all inefficient?
-        
+
         // get the site
-        const [site] = await db.select()
+        const [site] = await db
+            .select()
             .from(sites)
             .where(eq(sites.siteId, resource.siteId!))
             .limit(1);
@@ -98,10 +113,13 @@ export async function createTarget(req: Request, res: Response, next: NextFuncti
             );
         }
 
-        const newTarget = await db.insert(targets).values({
-            resourceId,
-            ...targetData
-        }).returning();
+        const newTarget = await db
+            .insert(targets)
+            .values({
+                resourceId,
+                ...targetData,
+            })
+            .returning();
 
         // Fetch resources for this site
         const resourcesRes = await db.query.resources.findMany({
@@ -109,16 +127,18 @@ export async function createTarget(req: Request, res: Response, next: NextFuncti
         });
 
         // Fetch targets for all resources of this site
-        const targetIps = await Promise.all(resourcesRes.map(async (resource) => {
-            const targetsRes = await db.query.targets.findMany({
-                where: eq(targets.resourceId, resource.resourceId),
-            });
-            return targetsRes.map(target => `${target.ip}/32`);
-        }));
+        const targetIps = await Promise.all(
+            resourcesRes.map(async (resource) => {
+                const targetsRes = await db.query.targets.findMany({
+                    where: eq(targets.resourceId, resource.resourceId),
+                });
+                return targetsRes.map((target) => `${target.ip}/32`);
+            })
+        );
 
         await addPeer(site.exitNodeId!, {
             publicKey: site.pubKey,
-            allowedIps: targetIps.flat()
+            allowedIps: targetIps.flat(),
         });
 
         return response(res, {
@@ -130,6 +150,11 @@ export async function createTarget(req: Request, res: Response, next: NextFuncti
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
 }

+ 51 - 29
server/routers/target/deleteTarget.ts

@@ -1,27 +1,32 @@
-import { Request, Response, NextFunction } from 'express';
-import { z } from 'zod';
-import { db } from '@server/db';
-import { resources, sites, targets } from '@server/db/schema';
-import { eq } from 'drizzle-orm';
+import { Request, Response, NextFunction } from "express";
+import { z } from "zod";
+import { db } from "@server/db";
+import { resources, sites, targets } 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';
-import { addPeer } from '../gerbil/peers';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { addPeer } from "../gerbil/peers";
+import { fromError } from "zod-validation-error";
 
 const deleteTargetSchema = z.object({
-    targetId: z.string().transform(Number).pipe(z.number().int().positive())
+    targetId: z.string().transform(Number).pipe(z.number().int().positive()),
 });
 
-export async function deleteTarget(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function deleteTarget(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedParams = deleteTargetSchema.safeParse(req.params);
         if (!parsedParams.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -29,13 +34,21 @@ export async function deleteTarget(req: Request, res: Response, next: NextFuncti
         const { targetId } = parsedParams.data;
 
         // Check if the user has permission to list sites
-        const hasPermission = await checkUserActionPermission(ActionsEnum.deleteTarget, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.deleteTarget,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "User does not have permission to perform this action"
+                )
+            );
         }
 
-
-        const [deletedTarget] = await db.delete(targets)
+        const [deletedTarget] = await db
+            .delete(targets)
             .where(eq(targets.targetId, targetId))
             .returning();
 
@@ -48,9 +61,10 @@ export async function deleteTarget(req: Request, res: Response, next: NextFuncti
             );
         }
         // get the resource
-        const [resource] = await db.select({
-            siteId: resources.siteId,
-        })
+        const [resource] = await db
+            .select({
+                siteId: resources.siteId,
+            })
             .from(resources)
             .where(eq(resources.resourceId, deletedTarget.resourceId!));
 
@@ -66,7 +80,8 @@ export async function deleteTarget(req: Request, res: Response, next: NextFuncti
         // TODO: is this all inefficient?
 
         // get the site
-        const [site] = await db.select()
+        const [site] = await db
+            .select()
             .from(sites)
             .where(eq(sites.siteId, resource.siteId!))
             .limit(1);
@@ -86,16 +101,18 @@ export async function deleteTarget(req: Request, res: Response, next: NextFuncti
         });
 
         // Fetch targets for all resources of this site
-        const targetIps = await Promise.all(resourcesRes.map(async (resource) => {
-            const targetsRes = await db.query.targets.findMany({
-                where: eq(targets.resourceId, resource.resourceId),
-            });
-            return targetsRes.map(target => `${target.ip}/32`);
-        }));
+        const targetIps = await Promise.all(
+            resourcesRes.map(async (resource) => {
+                const targetsRes = await db.query.targets.findMany({
+                    where: eq(targets.resourceId, resource.resourceId),
+                });
+                return targetsRes.map((target) => `${target.ip}/32`);
+            })
+        );
 
         await addPeer(site.exitNodeId!, {
             publicKey: site.pubKey,
-            allowedIps: targetIps.flat()
+            allowedIps: targetIps.flat(),
         });
 
         return response(res, {
@@ -107,6 +124,11 @@ export async function deleteTarget(req: Request, res: Response, next: NextFuncti
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
 }

+ 35 - 16
server/routers/target/getTarget.ts

@@ -1,26 +1,31 @@
-import { Request, Response, NextFunction } from 'express';
-import { z } from 'zod';
-import { db } from '@server/db';
-import { targets } from '@server/db/schema';
-import { eq } from 'drizzle-orm';
+import { Request, Response, NextFunction } from "express";
+import { z } from "zod";
+import { db } from "@server/db";
+import { targets } 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';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
 
 const getTargetSchema = z.object({
-    targetId: z.string().transform(Number).pipe(z.number().int().positive())
+    targetId: z.string().transform(Number).pipe(z.number().int().positive()),
 });
 
-export async function getTarget(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function getTarget(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedParams = getTargetSchema.safeParse(req.params);
         if (!parsedParams.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -28,12 +33,21 @@ export async function getTarget(req: Request, res: Response, next: NextFunction)
         const { targetId } = parsedParams.data;
 
         // Check if the user has permission to list sites
-        const hasPermission = await checkUserActionPermission(ActionsEnum.getTarget, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.getTarget,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "User does not have permission to perform this action"
+                )
+            );
         }
 
-        const target = await db.select()
+        const target = await db
+            .select()
             .from(targets)
             .where(eq(targets.targetId, targetId))
             .limit(1);
@@ -56,6 +70,11 @@ export async function getTarget(req: Request, res: Response, next: NextFunction)
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
 }

+ 47 - 26
server/routers/target/updateTarget.ts

@@ -1,36 +1,43 @@
-import { Request, Response, NextFunction } from 'express';
-import { z } from 'zod';
-import { db } from '@server/db';
-import { targets } from '@server/db/schema';
-import { eq } from 'drizzle-orm';
+import { Request, Response, NextFunction } from "express";
+import { z } from "zod";
+import { db } from "@server/db";
+import { targets } 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';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
 
 const updateTargetParamsSchema = z.object({
-    targetId: z.string().transform(Number).pipe(z.number().int().positive())
+    targetId: z.string().transform(Number).pipe(z.number().int().positive()),
 });
 
-const updateTargetBodySchema = z.object({
-    // ip: z.string().ip().optional(), // for now we cant update the ip; you will have to delete
-    method: z.string().min(1).max(10).optional(),
-    port: z.number().int().min(1).max(65535).optional(),
-    protocol: z.string().optional(),
-    enabled: z.boolean().optional(),
-}).refine(data => Object.keys(data).length > 0, {
-    message: "At least one field must be provided for update"
-});
+const updateTargetBodySchema = z
+    .object({
+        // ip: z.string().ip().optional(), // for now we cant update the ip; you will have to delete
+        method: z.string().min(1).max(10).optional(),
+        port: z.number().int().min(1).max(65535).optional(),
+        protocol: z.string().optional(),
+        enabled: z.boolean().optional(),
+    })
+    .refine((data) => Object.keys(data).length > 0, {
+        message: "At least one field must be provided for update",
+    });
 
-export async function updateTarget(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function updateTarget(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedParams = updateTargetParamsSchema.safeParse(req.params);
         if (!parsedParams.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -40,7 +47,7 @@ export async function updateTarget(req: Request, res: Response, next: NextFuncti
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedBody.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedBody.error).toString()
                 )
             );
         }
@@ -49,12 +56,21 @@ export async function updateTarget(req: Request, res: Response, next: NextFuncti
         const updateData = parsedBody.data;
 
         // Check if the user has permission to list sites
-        const hasPermission = await checkUserActionPermission(ActionsEnum.updateTarget, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.updateTarget,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "User does not have permission to perform this action"
+                )
+            );
         }
 
-        const updatedTarget = await db.update(targets)
+        const updatedTarget = await db
+            .update(targets)
             .set(updateData)
             .where(eq(targets.targetId, targetId))
             .returning();
@@ -77,6 +93,11 @@ export async function updateTarget(req: Request, res: Response, next: NextFuncti
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
 }

+ 51 - 21
server/routers/user/addUserAction.ts

@@ -1,13 +1,14 @@
-import { Request, Response, NextFunction } from 'express';
-import { z } from 'zod';
-import { db } from '@server/db';
-import { userActions, users } from '@server/db/schema';
+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';
+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";
+import { fromError } from "zod-validation-error";
 
 const addUserActionSchema = z.object({
     userId: z.string(),
@@ -15,14 +16,18 @@ const addUserActionSchema = z.object({
     orgId: z.string(),
 });
 
-export async function addUserAction(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function addUserAction(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedBody = addUserActionSchema.safeParse(req.body);
         if (!parsedBody.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedBody.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedBody.error).toString()
                 )
             );
         }
@@ -30,22 +35,42 @@ export async function addUserAction(req: Request, res: Response, next: NextFunct
         const { userId, actionId, orgId } = parsedBody.data;
 
         // Check if the user has permission to add user actions
-        const hasPermission = await checkUserActionPermission(ActionsEnum.addUserAction, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.addUserAction,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            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.userId, userId)).limit(1);
+        const user = await db
+            .select()
+            .from(users)
+            .where(eq(users.userId, userId))
+            .limit(1);
         if (user.length === 0) {
-            return next(createHttpError(HttpCode.NOT_FOUND, `User with ID ${userId} not found`));
+            return next(
+                createHttpError(
+                    HttpCode.NOT_FOUND,
+                    `User with ID ${userId} not found`
+                )
+            );
         }
 
-        const newUserAction = await db.insert(userActions).values({
-            userId,
-            actionId,
-            orgId,
-        }).returning();
+        const newUserAction = await db
+            .insert(userActions)
+            .values({
+                userId,
+                actionId,
+                orgId,
+            })
+            .returning();
 
         return response(res, {
             data: newUserAction[0],
@@ -56,6 +81,11 @@ export async function addUserAction(req: Request, res: Response, next: NextFunct
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
 }

+ 0 - 87
server/routers/user/addUserOrg.ts

@@ -1,87 +0,0 @@
-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.string()
-});
-
-const addUserSchema = z.object({
-    roleId: z.number().int().positive(),
-});
-
-export async function addUserOrg(req: Request, res: Response, next: NextFunction): Promise<any> {
-    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.userId, 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..."));
-    }
-}

+ 40 - 19
server/routers/user/addUserResource.ts

@@ -1,26 +1,31 @@
-import { Request, Response, NextFunction } from 'express';
-import { z } from 'zod';
-import { db } from '@server/db';
-import { userResources } from '@server/db/schema';
+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';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
 
 const addUserResourceSchema = z.object({
     userId: z.string(),
-    resourceId: z.string().transform(Number).pipe(z.number().int().positive())
+    resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
 });
 
-export async function addUserResource(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function addUserResource(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedBody = addUserResourceSchema.safeParse(req.body);
         if (!parsedBody.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedBody.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedBody.error).toString()
                 )
             );
         }
@@ -28,15 +33,26 @@ export async function addUserResource(req: Request, res: Response, next: NextFun
         const { userId, resourceId } = parsedBody.data;
 
         // Check if the user has permission to add user resources
-        const hasPermission = await checkUserActionPermission(ActionsEnum.addUserResource, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.addUserResource,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "User does not have permission to perform this action"
+                )
+            );
         }
 
-        const newUserResource = await db.insert(userResources).values({
-            userId,
-            resourceId,
-        }).returning();
+        const newUserResource = await db
+            .insert(userResources)
+            .values({
+                userId,
+                resourceId,
+            })
+            .returning();
 
         return response(res, {
             data: newUserResource[0],
@@ -47,6 +63,11 @@ export async function addUserResource(req: Request, res: Response, next: NextFun
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
-}
+}

+ 42 - 20
server/routers/user/addUserSite.ts

@@ -1,27 +1,32 @@
-import { Request, Response, NextFunction } from 'express';
-import { z } from 'zod';
-import { db } from '@server/db';
-import { resources, userResources, userSites } from '@server/db/schema';
+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';
+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";
+import { fromError } from "zod-validation-error";
 
 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<any> {
+export async function addUserSite(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedBody = addUserSiteSchema.safeParse(req.body);
         if (!parsedBody.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedBody.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedBody.error).toString()
                 )
             );
         }
@@ -29,18 +34,30 @@ export async function addUserSite(req: Request, res: Response, next: NextFunctio
         const { userId, siteId } = parsedBody.data;
 
         // Check if the user has permission to add user sites
-        const hasPermission = await checkUserActionPermission(ActionsEnum.addUserSite, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.addUserSite,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "User does not have permission to perform this action"
+                )
+            );
         }
 
-        const newUserSite = await db.insert(userSites).values({
-            userId,
-            siteId,
-        }).returning();
+        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()
+        const siteResources = await db
+            .select()
             .from(resources)
             .where(eq(resources.siteId, siteId));
 
@@ -60,6 +77,11 @@ export async function addUserSite(req: Request, res: Response, next: NextFunctio
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
-}
+}

+ 0 - 1
server/routers/user/index.ts

@@ -1,6 +1,5 @@
 export * from "./getUser";
 export * from "./removeUserOrg";
-export * from "./addUserOrg";
 export * from "./listUsers";
 export * from "./setUserRole";
 export * from "./inviteUser";

+ 44 - 23
server/routers/user/removeUserAction.ts

@@ -1,13 +1,14 @@
-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 { 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';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
 
 const removeUserActionParamsSchema = z.object({
     userId: z.string(),
@@ -15,17 +16,21 @@ const removeUserActionParamsSchema = z.object({
 
 const removeUserActionSchema = z.object({
     actionId: z.string(),
-    orgId: z.string()
+    orgId: z.string(),
 });
 
-export async function removeUserAction(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function removeUserAction(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedParams = removeUserActionParamsSchema.safeParse(req.params);
         if (!parsedParams.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -37,7 +42,7 @@ export async function removeUserAction(req: Request, res: Response, next: NextFu
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedBody.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedBody.error).toString()
                 )
             );
         }
@@ -45,17 +50,28 @@ export async function removeUserAction(req: Request, res: Response, next: NextFu
         const { actionId, orgId } = parsedBody.data;
 
         // Check if the user has permission to remove user actions
-        const hasPermission = await checkUserActionPermission(ActionsEnum.removeUserAction, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.removeUserAction,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            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)
-            ))
+        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) {
@@ -76,6 +92,11 @@ export async function removeUserAction(req: Request, res: Response, next: NextFu
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
-}
+}

+ 35 - 16
server/routers/user/removeUserOrg.ts

@@ -1,27 +1,32 @@
-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 { 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';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
 
 const removeUserSchema = z.object({
     userId: z.string().uuid(),
-    orgId: z.string()
+    orgId: z.string(),
 });
 
-export async function removeUserOrg(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function removeUserOrg(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedParams = removeUserSchema.safeParse(req.params);
         if (!parsedParams.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -29,13 +34,22 @@ export async function removeUserOrg(req: Request, res: Response, next: NextFunct
         const { userId, orgId } = parsedParams.data;
 
         // Check if the user has permission to list sites
-        const hasPermission = await checkUserActionPermission(ActionsEnum.removeUser, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.removeUser,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            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)
+        await db
+            .delete(userOrgs)
             .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)));
 
         return response(res, {
@@ -47,6 +61,11 @@ export async function removeUserOrg(req: Request, res: Response, next: NextFunct
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
 }

+ 42 - 18
server/routers/user/removeUserResource.ts

@@ -1,27 +1,32 @@
-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 { 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';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
 
 const removeUserResourceSchema = z.object({
     userId: z.string(),
-    resourceId: z.string().transform(Number).pipe(z.number().int().positive())
+    resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
 });
 
-export async function removeUserResource(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function removeUserResource(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedParams = removeUserResourceSchema.safeParse(req.params);
         if (!parsedParams.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -29,13 +34,27 @@ export async function removeUserResource(req: Request, res: Response, next: Next
         const { userId, resourceId } = parsedParams.data;
 
         // Check if the user has permission to remove user resources
-        const hasPermission = await checkUserActionPermission(ActionsEnum.removeUserResource, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.removeUserResource,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            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)))
+        const deletedUserResource = await db
+            .delete(userResources)
+            .where(
+                and(
+                    eq(userResources.userId, userId),
+                    eq(userResources.resourceId, resourceId)
+                )
+            )
             .returning();
 
         if (deletedUserResource.length === 0) {
@@ -56,6 +75,11 @@ export async function removeUserResource(req: Request, res: Response, next: Next
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
-}
+}

+ 49 - 21
server/routers/user/removeUserSite.ts

@@ -1,13 +1,14 @@
-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 { 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';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
 
 const removeUserSiteParamsSchema = z.object({
     userId: z.string(),
@@ -17,14 +18,18 @@ const removeUserSiteSchema = z.object({
     siteId: z.number().int().positive(),
 });
 
-export async function removeUserSite(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function removeUserSite(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedParams = removeUserSiteParamsSchema.safeParse(req.params);
         if (!parsedParams.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error).toString()
                 )
             );
         }
@@ -36,7 +41,7 @@ export async function removeUserSite(req: Request, res: Response, next: NextFunc
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedBody.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedBody.error).toString()
                 )
             );
         }
@@ -44,13 +49,24 @@ export async function removeUserSite(req: Request, res: Response, next: NextFunc
         const { siteId } = parsedBody.data;
 
         // Check if the user has permission to remove user sites
-        const hasPermission = await checkUserActionPermission(ActionsEnum.removeUserSite, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.removeUserSite,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            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)))
+        const deletedUserSite = await db
+            .delete(userSites)
+            .where(
+                and(eq(userSites.userId, userId), eq(userSites.siteId, siteId))
+            )
             .returning();
 
         if (deletedUserSite.length === 0) {
@@ -62,13 +78,20 @@ export async function removeUserSite(req: Request, res: Response, next: NextFunc
             );
         }
 
-        const siteResources = await db.select()
+        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)))
+            await db
+                .delete(userResources)
+                .where(
+                    and(
+                        eq(userResources.userId, userId),
+                        eq(userResources.resourceId, resource.resourceId)
+                    )
+                )
                 .returning();
         }
 
@@ -81,6 +104,11 @@ export async function removeUserSite(req: Request, res: Response, next: NextFunc
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
-}
+}

+ 44 - 19
server/routers/user/setUserRole.ts

@@ -1,28 +1,33 @@
-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 { 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';
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
 
 const addUserRoleSchema = z.object({
     userId: z.string(),
     roleId: z.number().int().positive(),
-    orgId: z.string()
+    orgId: z.string(),
 });
 
-export async function addUserRole(req: Request, res: Response, next: NextFunction): Promise<any> {
+export async function addUserRole(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
         const parsedBody = addUserRoleSchema.safeParse(req.body);
         if (!parsedBody.success) {
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedBody.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedBody.error).toString()
                 )
             );
         }
@@ -30,22 +35,37 @@ export async function addUserRole(req: Request, res: Response, next: NextFunctio
         const { userId, roleId, orgId } = parsedBody.data;
 
         // Check if the user has permission to add user roles
-        const hasPermission = await checkUserActionPermission(ActionsEnum.addUserRole, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.addUserRole,
+            req
+        );
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            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()
+        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'));
+            return next(
+                createHttpError(
+                    HttpCode.NOT_FOUND,
+                    "Role not found or does not belong to the specified organization"
+                )
+            );
         }
 
-        const newUserRole = await db.update(userOrgs)
+        const newUserRole = await db
+            .update(userOrgs)
             .set({ roleId })
             .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
             .returning();
@@ -59,6 +79,11 @@ export async function addUserRole(req: Request, res: Response, next: NextFunctio
         });
     } catch (error) {
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
-}
+}

+ 1 - 1
src/app/[orgId]/settings/components/Header.tsx

@@ -20,7 +20,7 @@ import {
     SelectTrigger,
     SelectValue,
 } from "@app/components/ui/select";
-import { useToast } from "@app/hooks/use-toast";
+import { useToast } from "@app/hooks/useToast";
 import { ListOrgsResponse } from "@server/routers/org";
 import Link from "next/link";
 import { useRouter } from "next/navigation";

+ 14 - 7
src/app/[orgId]/settings/layout.tsx

@@ -8,6 +8,9 @@ import { internal } from "@app/api";
 import { AxiosResponse } from "axios";
 import { GetOrgResponse, ListOrgsResponse } from "@server/routers/org";
 import { authCookieHeader } from "@app/api/cookies";
+import { cache } from "react";
+
+export const dynamic = "force-dynamic";
 
 export const metadata: Metadata = {
     title: `Settings - Pangolin`,
@@ -47,7 +50,8 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
 
     const { children } = props;
 
-    const user = await verifySession();
+    const getUser = cache(verifySession);
+    const user = await getUser();
 
     if (!user) {
         redirect("/auth/login");
@@ -56,20 +60,23 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
     const cookie = await authCookieHeader();
 
     try {
-        await internal.get<AxiosResponse<GetOrgResponse>>(
-            `/org/${params.orgId}`,
-            cookie
+        const getOrg = cache(() =>
+            internal.get<AxiosResponse<GetOrgResponse>>(
+                `/org/${params.orgId}`,
+                cookie
+            )
         );
+        const org = await getOrg();
     } catch {
         redirect(`/`);
     }
 
     let orgs: ListOrgsResponse["orgs"] = [];
     try {
-        const res = await internal.get<AxiosResponse<ListOrgsResponse>>(
-            `/orgs`,
-            cookie
+        const getOrgs = cache(() =>
+            internal.get<AxiosResponse<ListOrgsResponse>>(`/orgs`, cookie)
         );
+        const res = await getOrgs();
         if (res && res.data.data.orgs) {
             orgs = res.data.data.orgs;
         }

+ 1 - 1
src/app/[orgId]/settings/resources/[resourceId]/components/CreateResource.tsx

@@ -8,7 +8,7 @@ import {
 import { useForm } from "react-hook-form";
 import { z } from "zod";
 import { cn } from "@/lib/utils";
-import { toast } from "@/hooks/use-toast";
+import { toast } from "@/hooks/useToast";
 import { Button} from "@/components/ui/button";
 import {
     Form,

+ 21 - 1
src/app/[orgId]/settings/resources/[resourceId]/components/GeneralForm.tsx

@@ -36,6 +36,8 @@ import { AxiosResponse } from "axios";
 import api from "@app/api";
 import { useParams } from "next/navigation";
 import { useForm } from "react-hook-form";
+import { GetResourceResponse } from "@server/routers/resource";
+import { useToast } from "@app/hooks/useToast";
 
 const GeneralFormSchema = z.object({
     name: z.string(),
@@ -49,6 +51,7 @@ export function GeneralForm() {
     const orgId = params.orgId;
     const { resource, updateResource } = useResourceContext();
     const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
+    const { toast } = useToast();
 
     const form = useForm<GeneralFormValues>({
         resolver: zodResolver(GeneralFormSchema),
@@ -72,7 +75,24 @@ export function GeneralForm() {
     }, []);
 
     async function onSubmit(data: GeneralFormValues) {
-        await updateResource({ name: data.name, siteId: data.siteId });
+        updateResource({ name: data.name, siteId: data.siteId });
+        await api
+            .post<AxiosResponse<GetResourceResponse>>(
+                `resource/${resource?.resourceId}`,
+                {
+                    name: data.name,
+                    siteId: data.siteId,
+                }
+            )
+            .catch((e) => {
+                toast({
+                    variant: "destructive",
+                    title: "Failed to update resource",
+                    description:
+                        e.response?.data?.message ||
+                        "An error occurred while updating the resource",
+                });
+            });
     }
 
     return (

+ 2 - 2
src/app/[orgId]/settings/sites/[niceId]/components/CreateSite.tsx

@@ -5,7 +5,7 @@ import { ChevronDownIcon } from "@radix-ui/react-icons";
 import { useForm } from "react-hook-form";
 import { z } from "zod";
 import { cn } from "@/lib/utils";
-import { toast } from "@/hooks/use-toast";
+import { toast } from "@/hooks/useToast";
 import { Button, buttonVariants } from "@/components/ui/button";
 import {
     Form,
@@ -78,7 +78,7 @@ export function CreateSiteForm() {
             setKeypair(generatedKeypair);
             setIsLoading(false);
 
-            api.get(`/org/${orgId}/pickSiteDefaults`)
+            api.get(`/org/${orgId}/pick-site-defaults`)
                 .catch((e) => {
                     toast({
                         title: "Error creating site...",

+ 18 - 1
src/app/[orgId]/settings/sites/[niceId]/components/GeneralForm.tsx

@@ -15,6 +15,8 @@ import {
 import { Input } from "@/components/ui/input";
 import { useSiteContext } from "@app/hooks/useSiteContext";
 import { useForm } from "react-hook-form";
+import api from "@app/api";
+import { useToast } from "@app/hooks/useToast";
 
 const GeneralFormSchema = z.object({
     name: z.string(),
@@ -24,6 +26,7 @@ type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
 
 export function GeneralForm() {
     const { site, updateSite } = useSiteContext();
+    const { toast } = useToast();
 
     const form = useForm<GeneralFormValues>({
         resolver: zodResolver(GeneralFormSchema),
@@ -34,7 +37,21 @@ export function GeneralForm() {
     });
 
     async function onSubmit(data: GeneralFormValues) {
-        await updateSite({ name: data.name });
+        updateSite({ name: data.name });
+
+        await api
+            .post(`/site/${site?.siteId}`, {
+                name: data.name,
+            })
+            .catch((e) => {
+                toast({
+                    variant: "destructive",
+                    title: "Failed to update site",
+                    description:
+                        e.message ||
+                        "An error occurred while updating the site.",
+                });
+            });
     }
 
     return (

+ 1 - 1
src/app/[orgId]/settings/sites/components/SitesTable.tsx

@@ -92,7 +92,7 @@ export const columns: ColumnDef<SiteRow>[] = [
                     <DropdownMenuContent align="end">
                         <DropdownMenuItem>
                             <Link
-                                href={`/${siteRow.orgId}/settings/sites/${siteRow.id}`}
+                                href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
                             >
                                 View settings
                             </Link>

+ 157 - 108
src/app/[orgId]/settings/users/components/InviteUserForm.tsx

@@ -18,15 +18,30 @@ import {
     SelectTrigger,
     SelectValue,
 } from "@app/components/ui/select";
-import { useToast } from "@app/hooks/use-toast";
+import { useToast } from "@app/hooks/useToast";
 import { zodResolver } from "@hookform/resolvers/zod";
 import { InviteUserBody, InviteUserResponse } from "@server/routers/user";
 import { AxiosResponse } from "axios";
 import { useState } from "react";
 import { useForm } from "react-hook-form";
 import { z } from "zod";
-import { useParams } from "next/navigation";
 import CopyTextBox from "@app/components/CopyTextBox";
+import {
+    Credenza,
+    CredenzaBody,
+    CredenzaClose,
+    CredenzaContent,
+    CredenzaDescription,
+    CredenzaFooter,
+    CredenzaHeader,
+    CredenzaTitle,
+} from "@app/components/Credenza";
+import { useOrgContext } from "@app/hooks/useOrgContext";
+
+type InviteUserFormProps = {
+    open: boolean;
+    setOpen: (open: boolean) => void;
+};
 
 const formSchema = z.object({
     email: z.string().email({ message: "Invalid email address" }),
@@ -34,9 +49,9 @@ const formSchema = z.object({
     roleId: z.string(),
 });
 
-export default function InviteUserForm() {
+export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
     const { toast } = useToast();
-    const { orgId } = useParams();
+    const { org } = useOrgContext();
 
     const [inviteLink, setInviteLink] = useState<string | null>(null);
     const [loading, setLoading] = useState(false);
@@ -74,7 +89,7 @@ export default function InviteUserForm() {
 
         const res = await api
             .post<AxiosResponse<InviteUserResponse>>(
-                `/org/${orgId}/create-invite`,
+                `/org/${org?.org.orgId}/create-invite`,
                 {
                     email: values.email,
                     roleId: parseInt(values.roleId),
@@ -107,117 +122,151 @@ export default function InviteUserForm() {
 
     return (
         <>
-            {!inviteLink && (
-                <Form {...form}>
-                    <form
-                        onSubmit={form.handleSubmit(onSubmit)}
-                        className="space-y-4"
-                    >
-                        <FormField
-                            control={form.control}
-                            name="email"
-                            render={({ field }) => (
-                                <FormItem>
-                                    <FormLabel>Email</FormLabel>
-                                    <FormControl>
-                                        <Input
-                                            placeholder="Enter an email"
-                                            {...field}
-                                        />
-                                    </FormControl>
-                                    <FormMessage />
-                                </FormItem>
-                            )}
-                        />
-                        <FormField
-                            control={form.control}
-                            name="roleId"
-                            render={({ field }) => (
-                                <FormItem>
-                                    <FormLabel>Role</FormLabel>
-                                    <Select
-                                        onValueChange={field.onChange}
-                                        defaultValue={field.value.toString()}
-                                    >
-                                        <FormControl>
-                                            <SelectTrigger>
-                                                <SelectValue placeholder="Select role" />
-                                            </SelectTrigger>
-                                        </FormControl>
-                                        <SelectContent>
-                                            {roles.map((role) => (
-                                                <SelectItem
-                                                    key={role.roleId}
-                                                    value={role.roleId.toString()}
+            <Credenza open={open} onOpenChange={setOpen}>
+                <CredenzaContent>
+                    <CredenzaHeader>
+                        <CredenzaTitle>Invite User</CredenzaTitle>
+                        <CredenzaDescription>
+                            Give new users access to your organization
+                        </CredenzaDescription>
+                    </CredenzaHeader>
+                    <CredenzaBody>
+                        {!inviteLink && (
+                            <Form {...form}>
+                                <form
+                                    onSubmit={form.handleSubmit(onSubmit)}
+                                    className="space-y-4"
+                                    id="invite-user-form"
+                                >
+                                    <FormField
+                                        control={form.control}
+                                        name="email"
+                                        render={({ field }) => (
+                                            <FormItem>
+                                                <FormLabel>Email</FormLabel>
+                                                <FormControl>
+                                                    <Input
+                                                        placeholder="Enter an email"
+                                                        {...field}
+                                                    />
+                                                </FormControl>
+                                                <FormMessage />
+                                            </FormItem>
+                                        )}
+                                    />
+                                    <FormField
+                                        control={form.control}
+                                        name="roleId"
+                                        render={({ field }) => (
+                                            <FormItem>
+                                                <FormLabel>Role</FormLabel>
+                                                <Select
+                                                    onValueChange={
+                                                        field.onChange
+                                                    }
+                                                    defaultValue={field.value.toString()}
                                                 >
-                                                    {role.name}
-                                                </SelectItem>
-                                            ))}
-                                        </SelectContent>
-                                    </Select>
-                                    <FormMessage />
-                                </FormItem>
-                            )}
-                        />
-                        <FormField
-                            control={form.control}
-                            name="validForHours"
-                            render={({ field }) => (
-                                <FormItem>
-                                    <FormLabel>Valid For</FormLabel>
-                                    <Select
-                                        onValueChange={field.onChange}
-                                        defaultValue={field.value.toString()}
-                                    >
-                                        <FormControl>
-                                            <SelectTrigger>
-                                                <SelectValue placeholder="Select duration" />
-                                            </SelectTrigger>
-                                        </FormControl>
-                                        <SelectContent>
-                                            {validFor.map((option) => (
-                                                <SelectItem
-                                                    key={option.hours}
-                                                    value={option.hours.toString()}
+                                                    <FormControl>
+                                                        <SelectTrigger>
+                                                            <SelectValue placeholder="Select role" />
+                                                        </SelectTrigger>
+                                                    </FormControl>
+                                                    <SelectContent>
+                                                        {roles.map((role) => (
+                                                            <SelectItem
+                                                                key={
+                                                                    role.roleId
+                                                                }
+                                                                value={role.roleId.toString()}
+                                                            >
+                                                                {role.name}
+                                                            </SelectItem>
+                                                        ))}
+                                                    </SelectContent>
+                                                </Select>
+                                                <FormMessage />
+                                            </FormItem>
+                                        )}
+                                    />
+                                    <FormField
+                                        control={form.control}
+                                        name="validForHours"
+                                        render={({ field }) => (
+                                            <FormItem>
+                                                <FormLabel>Valid For</FormLabel>
+                                                <Select
+                                                    onValueChange={
+                                                        field.onChange
+                                                    }
+                                                    defaultValue={field.value.toString()}
                                                 >
-                                                    {option.name}
-                                                </SelectItem>
-                                            ))}
-                                        </SelectContent>
-                                    </Select>
-                                    <FormMessage />
-                                </FormItem>
-                            )}
-                        />
+                                                    <FormControl>
+                                                        <SelectTrigger>
+                                                            <SelectValue placeholder="Select duration" />
+                                                        </SelectTrigger>
+                                                    </FormControl>
+                                                    <SelectContent>
+                                                        {validFor.map(
+                                                            (option) => (
+                                                                <SelectItem
+                                                                    key={
+                                                                        option.hours
+                                                                    }
+                                                                    value={option.hours.toString()}
+                                                                >
+                                                                    {
+                                                                        option.name
+                                                                    }
+                                                                </SelectItem>
+                                                            )
+                                                        )}
+                                                    </SelectContent>
+                                                </Select>
+                                                <FormMessage />
+                                            </FormItem>
+                                        )}
+                                    />
+                                </form>
+                            </Form>
+                        )}
+
+                        {inviteLink && (
+                            <div className="max-w-md">
+                                <p className="mb-4">
+                                    The user has been successfully invited. They
+                                    must access the link below to accept the
+                                    invitation.
+                                </p>
+                                <p className="mb-4">
+                                    The invite will expire in{" "}
+                                    <b>
+                                        {expiresInDays}{" "}
+                                        {expiresInDays === 1 ? "day" : "days"}
+                                    </b>
+                                    .
+                                </p>
+                                <CopyTextBox
+                                    text={inviteLink}
+                                    wrapText={false}
+                                />
+                            </div>
+                        )}
+                    </CredenzaBody>
+                    <CredenzaFooter>
                         <Button
                             type="submit"
-                            className="w-full"
+                            form="invite-user-form"
                             loading={loading}
                             disabled={inviteLink !== null}
                         >
-                            Invite User
+                            Create Invitation
                         </Button>
-                    </form>
-                </Form>
-            )}
-
-            {inviteLink && (
-                <div className="max-w-md">
-                    <p className="mb-4">
-                        The user has been successfully invited. They must access
-                        the link below to accept the invitation.
-                    </p>
-                    <p className="mb-4">
-                        The invite will expire in{" "}
-                        <b>
-                            {expiresInDays}{" "}
-                            {expiresInDays === 1 ? "day" : "days"}
-                        </b>
-                        .
-                    </p>
-                    <CopyTextBox text={inviteLink} wrapText={false} />
-                </div>
-            )}
+                        <CredenzaClose asChild>
+                            <Button variant="outline">Close</Button>
+                        </CredenzaClose>
+                    </CredenzaFooter>
+                </CredenzaContent>
+            </Credenza>
         </>
     );
 }

+ 4 - 21
src/app/[orgId]/settings/users/components/UsersTable.tsx

@@ -10,16 +10,8 @@ import {
 import { Button } from "@app/components/ui/button";
 import { ArrowUpDown, MoreHorizontal } from "lucide-react";
 import { UsersDataTable } from "./UsersDataTable";
-import {
-    Dialog,
-    DialogContent,
-    DialogDescription,
-    DialogHeader,
-    DialogTitle,
-} from "@app/components/ui/dialog";
 import { useState } from "react";
 import InviteUserForm from "./InviteUserForm";
-import { Credenza, CredenzaTitle, CredenzaDescription, CredenzaHeader, CredenzaClose, CredenzaFooter, CredenzaContent, CredenzaBody } from "@app/components/Credenza";
 
 export type UserRow = {
     id: string;
@@ -74,19 +66,10 @@ export default function UsersTable({ users }: UsersTableProps) {
 
     return (
         <>
-            <Credenza open={isInviteModalOpen} onOpenChange={setIsInviteModalOpen}>
-                <CredenzaContent>
-                    <CredenzaHeader>
-                        <CredenzaTitle>Invite User</CredenzaTitle>
-                        <CredenzaDescription>
-                            Give new users access to your organization
-                        </CredenzaDescription>
-                    </CredenzaHeader>
-                    <CredenzaBody>
-                    <InviteUserForm />
-                    </CredenzaBody>
-                </CredenzaContent>
-            </Credenza>
+            <InviteUserForm
+                open={isInviteModalOpen}
+                setOpen={setIsInviteModalOpen}
+            />
 
             <UsersDataTable
                 columns={columns}

+ 32 - 6
src/app/[orgId]/settings/users/page.tsx

@@ -3,6 +3,9 @@ import { authCookieHeader } from "@app/api/cookies";
 import { ListUsersResponse } from "@server/routers/user";
 import { AxiosResponse } from "axios";
 import UsersTable, { UserRow } from "./components/UsersTable";
+import { GetOrgResponse } from "@server/routers/org";
+import { cache } from "react";
+import OrgProvider from "@app/providers/OrgProvider";
 
 type UsersPageProps = {
     params: Promise<{ orgId: string }>;
@@ -10,15 +13,36 @@ type UsersPageProps = {
 
 export default async function UsersPage(props: UsersPageProps) {
     const params = await props.params;
+
     let users: ListUsersResponse["users"] = [];
-    try {
-        const res = await internal.get<AxiosResponse<ListUsersResponse>>(
+    const res = await internal
+        .get<AxiosResponse<ListUsersResponse>>(
             `/org/${params.orgId}/users`,
             await authCookieHeader()
-        );
+        )
+        .catch((e) => {
+            console.error(e);
+        });
+
+    if (res && res.status === 200) {
         users = res.data.data.users;
-    } catch (e) {
-        console.error("Error fetching users", e);
+    }
+
+    let org: GetOrgResponse | null = null;
+    const getOrg = cache(async () =>
+        internal
+            .get<AxiosResponse<GetOrgResponse>>(
+                `/org/${params.orgId}`,
+                await authCookieHeader()
+            )
+            .catch((e) => {
+                console.error(e);
+            })
+    );
+    const orgRes = await getOrg();
+
+    if (orgRes && orgRes.status === 200) {
+        org = orgRes.data.data;
     }
 
     const userRows: UserRow[] = users.map((user) => {
@@ -40,7 +64,9 @@ export default async function UsersPage(props: UsersPageProps) {
                 </p>
             </div>
 
-            <UsersTable users={userRows} />
+            <OrgProvider org={org}>
+                <UsersTable users={userRows} />
+            </OrgProvider>
         </>
     );
 }

+ 5 - 1
src/app/auth/login/page.tsx

@@ -2,12 +2,16 @@ import LoginForm from "@app/app/auth/login/LoginForm";
 import { verifySession } from "@app/lib/auth/verifySession";
 import Link from "next/link";
 import { redirect } from "next/navigation";
+import { cache } from "react";
+
+export const dynamic = 'force-dynamic';
 
 export default async function Page(props: {
     searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
 }) {
     const searchParams = await props.searchParams;
-    const user = await verifySession();
+    const getUser = cache(verifySession);
+    const user = await getUser();
 
     if (user) {
         redirect("/");

+ 5 - 1
src/app/auth/signup/page.tsx

@@ -2,12 +2,16 @@ import SignupForm from "@app/app/auth/signup/SignupForm";
 import { verifySession } from "@app/lib/auth/verifySession";
 import Link from "next/link";
 import { redirect } from "next/navigation";
+import { cache } from "react";
+
+export const dynamic = 'force-dynamic';
 
 export default async function Page(props: {
     searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
 }) {
     const searchParams = await props.searchParams;
-    const user = await verifySession();
+    const getUser = cache(verifySession);
+    const user = await getUser();
 
     if (user) {
         redirect("/");

+ 1 - 1
src/app/auth/verify-email/VerifyEmailForm.tsx

@@ -32,7 +32,7 @@ import { AxiosResponse } from "axios";
 import { VerifyEmailResponse } from "@server/routers/auth";
 import { Loader2 } from "lucide-react";
 import { Alert, AlertDescription } from "../../../components/ui/alert";
-import { useToast } from "@app/hooks/use-toast";
+import { useToast } from "@app/hooks/useToast";
 import { useRouter } from "next/navigation";
 
 const FormSchema = z.object({

+ 5 - 1
src/app/auth/verify-email/page.tsx

@@ -1,6 +1,9 @@
 import VerifyEmailForm from "@app/app/auth/verify-email/VerifyEmailForm";
 import { verifySession } from "@app/lib/auth/verifySession";
 import { redirect } from "next/navigation";
+import { cache } from "react";
+
+export const dynamic = "force-dynamic";
 
 export default async function Page(props: {
     searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
@@ -10,7 +13,8 @@ export default async function Page(props: {
     }
 
     const searchParams = await props.searchParams;
-    const user = await verifySession();
+    const getUser = cache(verifySession);
+    const user = await getUser();
 
     if (!user) {
         redirect("/");

+ 5 - 1
src/app/page.tsx

@@ -7,12 +7,16 @@ import { AxiosResponse } from "axios";
 import { ArrowUpRight } from "lucide-react";
 import Link from "next/link";
 import { redirect } from "next/navigation";
+import { cache } from "react";
+
+export const dynamic = "force-dynamic";
 
 export default async function Page(props: {
     searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
 }) {
     const params = await props.searchParams; // this is needed to prevent static optimization
-    const user = await verifySession();
+    const getUser = cache(verifySession);
+    const user = await getUser();
 
     if (!user) {
         redirect("/auth/login");

+ 1 - 1
src/app/profile/account/account-form.tsx

@@ -6,7 +6,7 @@ import { useForm } from "react-hook-form"
 import { z } from "zod"
 
 import { cn } from "@/lib/utils"
-import { toast } from "@/hooks/use-toast"
+import { toast } from "@/hooks/useToast"
 import { Button } from "@/components/ui/button"
 import {
   Command,

+ 1 - 1
src/app/profile/appearance/appearance-form.tsx

@@ -6,7 +6,7 @@ import { useForm } from "react-hook-form"
 import { z } from "zod"
 
 import { cn } from "@/lib/utils"
-import { toast } from "@/hooks/use-toast"
+import { toast } from "@/hooks/useToast"
 import { Button, buttonVariants } from "@/components/ui/button"
 import {
   Form,

+ 1 - 1
src/app/profile/display/display-form.tsx

@@ -4,7 +4,7 @@ import { zodResolver } from "@hookform/resolvers/zod"
 import { useForm } from "react-hook-form"
 import { z } from "zod"
 
-import { toast } from "@/hooks/use-toast"
+import { toast } from "@/hooks/useToast"
 import { Button } from "@/components/ui/button"
 import { Checkbox } from "@/components/ui/checkbox"
 import {

+ 1 - 1
src/app/profile/notifications/notifications-form.tsx

@@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod"
 import { useForm } from "react-hook-form"
 import { z } from "zod"
 
-import { toast } from "@/hooks/use-toast"
+import { toast } from "@/hooks/useToast"
 import { Button } from "@/components/ui/button"
 import { Checkbox } from "@/components/ui/checkbox"
 import {

+ 1 - 1
src/app/profile/profile-form.tsx

@@ -6,7 +6,7 @@ import { useFieldArray, useForm } from "react-hook-form"
 import { z } from "zod"
 
 import { cn } from "@/lib/utils"
-import { toast } from "@/hooks/use-toast"
+import { toast } from "@/hooks/useToast"
 
 import { Button } from "@/components/ui/button"
 import {

+ 1 - 1
src/app/setup/page.tsx

@@ -5,7 +5,7 @@ import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
 import Link from "next/link";
 import api from "@app/api";
-import { toast } from "@app/hooks/use-toast";
+import { toast } from "@app/hooks/useToast";
 import { useCallback, useEffect, useState } from "react";
 import {
     Card,

+ 1 - 1
src/components/account-form.tsx

@@ -6,7 +6,7 @@ import { useForm } from "react-hook-form"
 import { z } from "zod"
 
 import { cn } from "@/lib/utils"
-import { toast } from "@/hooks/use-toast"
+import { toast } from "@/hooks/useToast"
 import { Button } from "@/components/ui/button"
 import {
   Command,

+ 160 - 150
src/components/appearance-form.tsx

@@ -1,169 +1,179 @@
-"use client"
+"use client";
 
-import { zodResolver } from "@hookform/resolvers/zod"
-import { ChevronDownIcon } from "@radix-ui/react-icons"
-import { useForm } from "react-hook-form"
-import { z } from "zod"
+import { zodResolver } from "@hookform/resolvers/zod";
+import { ChevronDownIcon } from "@radix-ui/react-icons";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
 
-import { cn } from "@/lib/utils"
-import { toast } from "@/hooks/use-toast"
-import { Button, buttonVariants } from "@/components/ui/button"
+import { cn } from "@/lib/utils";
+import { toast } from "@/hooks/useToast";
+import { Button, buttonVariants } from "@/components/ui/button";
 import {
-  Form,
-  FormControl,
-  FormDescription,
-  FormField,
-  FormItem,
-  FormLabel,
-  FormMessage,
-} from "@/components/ui/form"
-import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
-import { useSiteContext } from "@app/hooks/useSiteContext"
+    Form,
+    FormControl,
+    FormDescription,
+    FormField,
+    FormItem,
+    FormLabel,
+    FormMessage,
+} from "@/components/ui/form";
+import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
+import { useSiteContext } from "@app/hooks/useSiteContext";
 
 const appearanceFormSchema = z.object({
-  theme: z.enum(["light", "dark"], {
-    required_error: "Please select a theme.",
-  }),
-  font: z.enum(["inter", "manrope", "system"], {
-    invalid_type_error: "Select a font",
-    required_error: "Please select a font.",
-  }),
-})
+    theme: z.enum(["light", "dark"], {
+        required_error: "Please select a theme.",
+    }),
+    font: z.enum(["inter", "manrope", "system"], {
+        invalid_type_error: "Select a font",
+        required_error: "Please select a font.",
+    }),
+});
 
-type AppearanceFormValues = z.infer<typeof appearanceFormSchema>
+type AppearanceFormValues = z.infer<typeof appearanceFormSchema>;
 
 // This can come from your database or API.
 const defaultValues: Partial<AppearanceFormValues> = {
-  theme: "light",
-}
+    theme: "light",
+};
 
 export function AppearanceForm() {
     const site = useSiteContext();
 
     console.log(site);
-    
-  const form = useForm<AppearanceFormValues>({
-    resolver: zodResolver(appearanceFormSchema),
-    defaultValues,
-  })
 
-  function onSubmit(data: AppearanceFormValues) {
-    toast({
-      title: "You submitted the following values:",
-      description: (
-        <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
-          <code className="text-white">{JSON.stringify(data, null, 2)}</code>
-        </pre>
-      ),
-    })
-  }
+    const form = useForm<AppearanceFormValues>({
+        resolver: zodResolver(appearanceFormSchema),
+        defaultValues,
+    });
+
+    function onSubmit(data: AppearanceFormValues) {
+        toast({
+            title: "You submitted the following values:",
+            description: (
+                <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
+                    <code className="text-white">
+                        {JSON.stringify(data, null, 2)}
+                    </code>
+                </pre>
+            ),
+        });
+    }
 
-  return (
-    <Form {...form}>
-      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
-        <FormField
-          control={form.control}
-          name="font"
-          render={({ field }) => (
-            <FormItem>
-              <FormLabel>Font</FormLabel>
-              <div className="relative w-max">
-                <FormControl>
-                  <select
-                    className={cn(
-                      buttonVariants({ variant: "outline" }),
-                      "w-[200px] appearance-none font-normal"
+    return (
+        <Form {...form}>
+            <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
+                <FormField
+                    control={form.control}
+                    name="font"
+                    render={({ field }) => (
+                        <FormItem>
+                            <FormLabel>Font</FormLabel>
+                            <div className="relative w-max">
+                                <FormControl>
+                                    <select
+                                        className={cn(
+                                            buttonVariants({
+                                                variant: "outline",
+                                            }),
+                                            "w-[200px] appearance-none font-normal"
+                                        )}
+                                        {...field}
+                                    >
+                                        <option value="inter">Inter</option>
+                                        <option value="manrope">Manrope</option>
+                                        <option value="system">System</option>
+                                    </select>
+                                </FormControl>
+                                <ChevronDownIcon className="absolute right-3 top-2.5 h-4 w-4 opacity-50" />
+                            </div>
+                            <FormDescription>
+                                Set the font you want to use in the dashboard.
+                            </FormDescription>
+                            <FormMessage />
+                        </FormItem>
+                    )}
+                />
+                <FormField
+                    control={form.control}
+                    name="theme"
+                    render={({ field }) => (
+                        <FormItem className="space-y-1">
+                            <FormLabel>Theme</FormLabel>
+                            <FormDescription>
+                                Select the theme for the dashboard.
+                            </FormDescription>
+                            <FormMessage />
+                            <RadioGroup
+                                onValueChange={field.onChange}
+                                defaultValue={field.value}
+                                className="grid max-w-md grid-cols-2 gap-8 pt-2"
+                            >
+                                <FormItem>
+                                    <FormLabel className="[&:has([data-state=checked])>div]:border-primary">
+                                        <FormControl>
+                                            <RadioGroupItem
+                                                value="light"
+                                                className="sr-only"
+                                            />
+                                        </FormControl>
+                                        <div className="items-center rounded-md border-2 border-muted p-1 hover:border-accent">
+                                            <div className="space-y-2 rounded-sm bg-[#ecedef] p-2">
+                                                <div className="space-y-2 rounded-md bg-white p-2 shadow-sm">
+                                                    <div className="h-2 w-[80px] rounded-lg bg-[#ecedef]" />
+                                                    <div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
+                                                </div>
+                                                <div className="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
+                                                    <div className="h-4 w-4 rounded-full bg-[#ecedef]" />
+                                                    <div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
+                                                </div>
+                                                <div className="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
+                                                    <div className="h-4 w-4 rounded-full bg-[#ecedef]" />
+                                                    <div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
+                                                </div>
+                                            </div>
+                                        </div>
+                                        <span className="block w-full p-2 text-center font-normal">
+                                            Light
+                                        </span>
+                                    </FormLabel>
+                                </FormItem>
+                                <FormItem>
+                                    <FormLabel className="[&:has([data-state=checked])>div]:border-primary">
+                                        <FormControl>
+                                            <RadioGroupItem
+                                                value="dark"
+                                                className="sr-only"
+                                            />
+                                        </FormControl>
+                                        <div className="items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground">
+                                            <div className="space-y-2 rounded-sm bg-slate-950 p-2">
+                                                <div className="space-y-2 rounded-md bg-slate-800 p-2 shadow-sm">
+                                                    <div className="h-2 w-[80px] rounded-lg bg-slate-400" />
+                                                    <div className="h-2 w-[100px] rounded-lg bg-slate-400" />
+                                                </div>
+                                                <div className="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
+                                                    <div className="h-4 w-4 rounded-full bg-slate-400" />
+                                                    <div className="h-2 w-[100px] rounded-lg bg-slate-400" />
+                                                </div>
+                                                <div className="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
+                                                    <div className="h-4 w-4 rounded-full bg-slate-400" />
+                                                    <div className="h-2 w-[100px] rounded-lg bg-slate-400" />
+                                                </div>
+                                            </div>
+                                        </div>
+                                        <span className="block w-full p-2 text-center font-normal">
+                                            Dark
+                                        </span>
+                                    </FormLabel>
+                                </FormItem>
+                            </RadioGroup>
+                        </FormItem>
                     )}
-                    {...field}
-                  >
-                    <option value="inter">Inter</option>
-                    <option value="manrope">Manrope</option>
-                    <option value="system">System</option>
-                  </select>
-                </FormControl>
-                <ChevronDownIcon className="absolute right-3 top-2.5 h-4 w-4 opacity-50" />
-              </div>
-              <FormDescription>
-                Set the font you want to use in the dashboard.
-              </FormDescription>
-              <FormMessage />
-            </FormItem>
-          )}
-        />
-        <FormField
-          control={form.control}
-          name="theme"
-          render={({ field }) => (
-            <FormItem className="space-y-1">
-              <FormLabel>Theme</FormLabel>
-              <FormDescription>
-                Select the theme for the dashboard.
-              </FormDescription>
-              <FormMessage />
-              <RadioGroup
-                onValueChange={field.onChange}
-                defaultValue={field.value}
-                className="grid max-w-md grid-cols-2 gap-8 pt-2"
-              >
-                <FormItem>
-                  <FormLabel className="[&:has([data-state=checked])>div]:border-primary">
-                    <FormControl>
-                      <RadioGroupItem value="light" className="sr-only" />
-                    </FormControl>
-                    <div className="items-center rounded-md border-2 border-muted p-1 hover:border-accent">
-                      <div className="space-y-2 rounded-sm bg-[#ecedef] p-2">
-                        <div className="space-y-2 rounded-md bg-white p-2 shadow-sm">
-                          <div className="h-2 w-[80px] rounded-lg bg-[#ecedef]" />
-                          <div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
-                        </div>
-                        <div className="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
-                          <div className="h-4 w-4 rounded-full bg-[#ecedef]" />
-                          <div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
-                        </div>
-                        <div className="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
-                          <div className="h-4 w-4 rounded-full bg-[#ecedef]" />
-                          <div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
-                        </div>
-                      </div>
-                    </div>
-                    <span className="block w-full p-2 text-center font-normal">
-                      Light
-                    </span>
-                  </FormLabel>
-                </FormItem>
-                <FormItem>
-                  <FormLabel className="[&:has([data-state=checked])>div]:border-primary">
-                    <FormControl>
-                      <RadioGroupItem value="dark" className="sr-only" />
-                    </FormControl>
-                    <div className="items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground">
-                      <div className="space-y-2 rounded-sm bg-slate-950 p-2">
-                        <div className="space-y-2 rounded-md bg-slate-800 p-2 shadow-sm">
-                          <div className="h-2 w-[80px] rounded-lg bg-slate-400" />
-                          <div className="h-2 w-[100px] rounded-lg bg-slate-400" />
-                        </div>
-                        <div className="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
-                          <div className="h-4 w-4 rounded-full bg-slate-400" />
-                          <div className="h-2 w-[100px] rounded-lg bg-slate-400" />
-                        </div>
-                        <div className="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
-                          <div className="h-4 w-4 rounded-full bg-slate-400" />
-                          <div className="h-2 w-[100px] rounded-lg bg-slate-400" />
-                        </div>
-                      </div>
-                    </div>
-                    <span className="block w-full p-2 text-center font-normal">
-                      Dark
-                    </span>
-                  </FormLabel>
-                </FormItem>
-              </RadioGroup>
-            </FormItem>
-          )}
-        />
+                />
 
-        <Button type="submit">Update preferences</Button>
-      </form>
-    </Form>
-  )
+                <Button type="submit">Update preferences</Button>
+            </form>
+        </Form>
+    );
 }

+ 1 - 1
src/components/display-form.tsx

@@ -4,7 +4,7 @@ import { zodResolver } from "@hookform/resolvers/zod"
 import { useForm } from "react-hook-form"
 import { z } from "zod"
 
-import { toast } from "@/hooks/use-toast"
+import { toast } from "@/hooks/useToast"
 import { Button } from "@/components/ui/button"
 import { Checkbox } from "@/components/ui/checkbox"
 import {

+ 1 - 1
src/components/notifications-form.tsx

@@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod"
 import { useForm } from "react-hook-form"
 import { z } from "zod"
 
-import { toast } from "@/hooks/use-toast"
+import { toast } from "@/hooks/useToast"
 import { Button } from "@/components/ui/button"
 import { Checkbox } from "@/components/ui/checkbox"
 import {

+ 1 - 1
src/components/profile-form.tsx

@@ -6,7 +6,7 @@ import { useFieldArray, useForm } from "react-hook-form"
 import { z } from "zod"
 
 import { cn } from "@/lib/utils"
-import { toast } from "@/hooks/use-toast"
+import { toast } from "@/hooks/useToast"
 
 import { Button } from "@/components/ui/button"
 import {

+ 1 - 1
src/components/ui/toaster.tsx

@@ -1,6 +1,6 @@
 "use client"
 
-import { useToast } from "@/hooks/use-toast"
+import { useToast } from "@/hooks/useToast"
 import {
   Toast,
   ToastClose,

+ 11 - 0
src/contexts/orgContext.ts

@@ -0,0 +1,11 @@
+import { GetOrgResponse } from "@server/routers/org";
+import { createContext } from "react";
+
+interface OrgContextType {
+    org: GetOrgResponse | null;
+    updateOrg: (updateOrg: Partial<GetOrgResponse>) => void;
+}
+
+const OrgContext = createContext<OrgContextType | undefined>(undefined);
+
+export default OrgContext;

+ 5 - 3
src/contexts/resourceContext.ts

@@ -3,9 +3,11 @@ import { createContext } from "react";
 
 interface ResourceContextType {
     resource: GetResourceResponse | null;
-    updateResource: (updatedResource: Partial<GetResourceResponse>) => Promise<void>;
+    updateResource: (updatedResource: Partial<GetResourceResponse>) => void;
 }
 
-const ResourceContext = createContext<ResourceContextType | undefined>(undefined);
+const ResourceContext = createContext<ResourceContextType | undefined>(
+    undefined
+);
 
-export default ResourceContext;
+export default ResourceContext;

+ 2 - 2
src/contexts/siteContext.ts

@@ -3,9 +3,9 @@ import { createContext } from "react";
 
 interface SiteContextType {
     site: GetSiteResponse | null;
-    updateSite: (updatedSite: Partial<GetSiteResponse>) => Promise<void>;
+    updateSite: (updatedSite: Partial<GetSiteResponse>) => void;
 }
 
 const SiteContext = createContext<SiteContextType | undefined>(undefined);
 
-export default SiteContext;
+export default SiteContext;

+ 10 - 0
src/hooks/useOrgContext.ts

@@ -0,0 +1,10 @@
+import OrgContext from "@app/contexts/orgContext";
+import { useContext } from "react";
+
+export function useOrgContext() {
+    const context = useContext(OrgContext);
+    if (context === undefined) {
+        throw new Error("useOrgContext must be used within a OrgProvider");
+    }
+    return context;
+}

+ 4 - 2
src/hooks/useResourceContext.ts

@@ -4,7 +4,9 @@ import { useContext } from "react";
 export function useResourceContext() {
     const context = useContext(ResourceContext);
     if (context === undefined) {
-        throw new Error('useResourceContext must be used within a ResourceProvider');
+        throw new Error(
+            "useResourceContext must be used within a ResourceProvider"
+        );
     }
     return context;
-}
+}

+ 0 - 0
src/hooks/use-toast.ts → src/hooks/useToast.ts


+ 1 - 1
src/lib/auth/verifySession.ts

@@ -14,4 +14,4 @@ export async function verifySession(): Promise<GetUserResponse | null> {
     } catch {
         return null;
     }
-}
+}

+ 39 - 0
src/providers/OrgProvider.tsx

@@ -0,0 +1,39 @@
+"use client";
+
+import OrgContext from "@app/contexts/orgContext";
+import { GetOrgResponse } from "@server/routers/org";
+import { useState } from "react";
+
+interface OrgProviderProps {
+    children: React.ReactNode;
+    org: GetOrgResponse | null;
+}
+
+export function OrgProvider({ children, org: serverOrg }: OrgProviderProps) {
+    const [org, setOrg] = useState<GetOrgResponse | null>(serverOrg);
+
+    const updateOrg = (updatedOrg: Partial<GetOrgResponse>) => {
+        if (!org) {
+            throw new Error("No org to update");
+        }
+
+        setOrg((prev) => {
+            if (!prev) {
+                return prev;
+            }
+
+            return {
+                ...prev,
+                ...updatedOrg,
+            };
+        });
+    };
+
+    return (
+        <OrgContext.Provider value={{ org, updateOrg }}>
+            {children}
+        </OrgContext.Provider>
+    );
+}
+
+export default OrgProvider;

+ 26 - 27
src/providers/ResourceProvider.tsx

@@ -1,10 +1,7 @@
 "use client";
 
-import api from "@app/api";
 import ResourceContext from "@app/contexts/resourceContext";
-import { toast } from "@app/hooks/use-toast";
 import { GetResourceResponse } from "@server/routers/resource/getResource";
-import { AxiosResponse } from "axios";
 import { useState } from "react";
 
 interface ResourceProviderProps {
@@ -12,34 +9,36 @@ interface ResourceProviderProps {
     resource: GetResourceResponse | null;
 }
 
-export function ResourceProvider({ children, resource: serverResource }: ResourceProviderProps) {
-    const [resource, setResource] = useState<GetResourceResponse | null>(serverResource);
+export function ResourceProvider({
+    children,
+    resource: serverResource,
+}: ResourceProviderProps) {
+    const [resource, setResource] = useState<GetResourceResponse | null>(
+        serverResource
+    );
+
+    const updateResource = (updatedResource: Partial<GetResourceResponse>) => {
+        if (!resource) {
+            throw new Error("No resource to update");
+        }
 
-    const updateResource = async (updatedResource: Partial<GetResourceResponse>) => {
-        try {
-            if (!resource) {
-                throw new Error("No resource to update");
+        setResource((prev) => {
+            if (!prev) {
+                return prev;
             }
 
-            const res = await api.post<AxiosResponse<GetResourceResponse>>(
-                `resource/${resource.resourceId}`,
-                updatedResource,
-            );
-            setResource(res.data.data);
-            toast({
-                title: "Resource updated!",
-            });
-        } catch (error) {
-            console.error(error);
-            toast({
-                variant: "destructive",
-                title: "Error updating resource...",
-            })
-        }
+            return {
+                ...prev,
+                ...updatedResource,
+            };
+        });
     };
 
-
-    return <ResourceContext.Provider value={{ resource, updateResource }}>{children}</ResourceContext.Provider>;
+    return (
+        <ResourceContext.Provider value={{ resource, updateResource }}>
+            {children}
+        </ResourceContext.Provider>
+    );
 }
 
-export default ResourceProvider;
+export default ResourceProvider;

+ 22 - 27
src/providers/SiteProvider.tsx

@@ -1,10 +1,7 @@
 "use client";
 
-import api from "@app/api";
 import SiteContext from "@app/contexts/siteContext";
-import { toast } from "@app/hooks/use-toast";
 import { GetSiteResponse } from "@server/routers/site/getSite";
-import { AxiosResponse } from "axios";
 import { useState } from "react";
 
 interface SiteProviderProps {
@@ -12,34 +9,32 @@ interface SiteProviderProps {
     site: GetSiteResponse | null;
 }
 
-export function SiteProvider({ children, site: serverSite }: SiteProviderProps) {
+export function SiteProvider({
+    children,
+    site: serverSite,
+}: SiteProviderProps) {
     const [site, setSite] = useState<GetSiteResponse | null>(serverSite);
 
-    const updateSite = async (updatedSite: Partial<GetSiteResponse>) => {
-        try {
-            if (!site) {
-                throw new Error("No site to update");
-            }
-
-            const res = await api.post<AxiosResponse<GetSiteResponse>>(
-                `site/${site.siteId}`,
-                updatedSite,
-            );
-            setSite(res.data.data);
-            toast({
-                title: "Site updated!",
-            });
-        } catch (error) {
-            console.error(error);
-            toast({
-                variant: "destructive",
-                title: "Error updating site...",
-            })
+    const updateSite = (updatedSite: Partial<GetSiteResponse>) => {
+        if (!site) {
+            throw new Error("No site to update");
         }
+        setSite((prev) => {
+            if (!prev) {
+                return prev;
+            }
+            return {
+                ...prev,
+                ...updatedSite,
+            };
+        });
     };
 
-
-    return <SiteContext.Provider value={{ site, updateSite }}>{children}</SiteContext.Provider>;
+    return (
+        <SiteContext.Provider value={{ site, updateSite }}>
+            {children}
+        </SiteContext.Provider>
+    );
 }
 
-export default SiteProvider;
+export default SiteProvider;