Pārlūkot izejas kodu

more user role stuff

Milo Schwartz 8 mēneši atpakaļ
vecāks
revīzija
231e1d2e2d
32 mainītis faili ar 897 papildinājumiem un 138 dzēšanām
  1. 1 0
      server/auth/actions.ts
  2. 4 5
      server/routers/auth/verifyAdmin.ts
  3. 4 5
      server/routers/auth/verifyOrgAccess.ts
  4. 4 5
      server/routers/auth/verifyResourceAccess.ts
  5. 14 6
      server/routers/auth/verifyRoleAccess.ts
  6. 15 12
      server/routers/auth/verifySiteAccess.ts
  7. 6 7
      server/routers/auth/verifyTargetAccess.ts
  8. 3 5
      server/routers/auth/verifyUserAccess.ts
  9. 4 5
      server/routers/auth/verifyUserIsOrgOwner.ts
  10. 14 14
      server/routers/external.ts
  11. 22 1
      server/routers/org/createOrg.ts
  12. 9 8
      server/routers/role/createRole.ts
  13. 41 5
      server/routers/user/addUserRole.ts
  14. 120 0
      server/routers/user/getOrgUser.ts
  15. 21 20
      server/routers/user/getUser.ts
  16. 3 2
      server/routers/user/index.ts
  17. 2 0
      server/types/ArrayElement.ts
  18. 43 0
      src/app/[orgId]/page.tsx
  19. 3 4
      src/app/[orgId]/settings/access/layout.tsx
  20. 179 0
      src/app/[orgId]/settings/access/roles/components/CreateRoleForm.tsx
  21. 4 0
      src/app/[orgId]/settings/access/roles/components/RolesDataTable.tsx
  22. 27 4
      src/app/[orgId]/settings/access/roles/components/RolesTable.tsx
  23. 57 0
      src/app/[orgId]/settings/access/users/[userId]/layout.tsx
  24. 20 0
      src/app/[orgId]/settings/access/users/[userId]/page.tsx
  25. 226 0
      src/app/[orgId]/settings/access/users/components/ManageUserForm.tsx
  26. 4 0
      src/app/[orgId]/settings/access/users/components/UsersDataTable.tsx
  27. 22 10
      src/app/[orgId]/settings/access/users/components/UsersTable.tsx
  28. 10 5
      src/app/[orgId]/settings/layout.tsx
  29. 4 0
      src/app/[orgId]/settings/resources/components/ResourcesDataTable.tsx
  30. 4 0
      src/app/[orgId]/settings/sites/components/SitesDataTable.tsx
  31. 1 1
      src/components/DataTablePagination.tsx
  32. 6 14
      src/components/sidebar-nav.tsx

+ 1 - 0
server/auth/actions.ts

@@ -51,6 +51,7 @@ export enum ActionsEnum {
     // removeUserAction = "removeUserAction",
     // removeUserAction = "removeUserAction",
     removeUserResource = "removeUserResource",
     removeUserResource = "removeUserResource",
     removeUserSite = "removeUserSite",
     removeUserSite = "removeUserSite",
+    getOrgUser = "getOrgUser",
 }
 }
 
 
 export async function checkUserActionPermission(
 export async function checkUserActionPermission(

+ 4 - 5
server/routers/auth/verifyAdmin.ts

@@ -12,7 +12,6 @@ export async function verifyAdmin(
 ) {
 ) {
     const userId = req.user?.userId;
     const userId = req.user?.userId;
     const orgId = req.userOrgId;
     const orgId = req.userOrgId;
-    let userOrg = req.userOrg;
 
 
     if (!userId) {
     if (!userId) {
         return next(
         return next(
@@ -26,16 +25,16 @@ export async function verifyAdmin(
         );
         );
     }
     }
 
 
-    if (!userOrg) {
+    if (!req.userOrg) {
         const userOrgRes = await db
         const userOrgRes = await db
             .select()
             .select()
             .from(userOrgs)
             .from(userOrgs)
             .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId!)))
             .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId!)))
             .limit(1);
             .limit(1);
-        userOrg = userOrgRes[0];
+        req.userOrg = userOrgRes[0];
     }
     }
 
 
-    if (!userOrg) {
+    if (!req.userOrg) {
         return next(
         return next(
             createHttpError(
             createHttpError(
                 HttpCode.FORBIDDEN,
                 HttpCode.FORBIDDEN,
@@ -47,7 +46,7 @@ export async function verifyAdmin(
     const userRole = await db
     const userRole = await db
         .select()
         .select()
         .from(roles)
         .from(roles)
-        .where(eq(roles.roleId, userOrg.roleId))
+        .where(eq(roles.roleId, req.userOrg.roleId))
         .limit(1);
         .limit(1);
 
 
     if (userRole.length === 0 || !userRole[0].isAdmin) {
     if (userRole.length === 0 || !userRole[0].isAdmin) {

+ 4 - 5
server/routers/auth/verifyOrgAccess.ts

@@ -12,7 +12,6 @@ export async function verifyOrgAccess(
 ) {
 ) {
     const userId = req.user!.userId;
     const userId = req.user!.userId;
     const orgId = req.params.orgId;
     const orgId = req.params.orgId;
-    let userOrg = req.userOrg;
 
 
     if (!userId) {
     if (!userId) {
         return next(
         return next(
@@ -27,17 +26,17 @@ export async function verifyOrgAccess(
     }
     }
 
 
     try {
     try {
-        if (!userOrg) {
+        if (!req.userOrg) {
             const userOrgRes = await db
             const userOrgRes = await db
                 .select()
                 .select()
                 .from(userOrgs)
                 .from(userOrgs)
                 .where(
                 .where(
                     and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))
                     and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))
                 );
                 );
-            userOrg = userOrgRes[0];
+            req.userOrg = userOrgRes[0];
         }
         }
 
 
-        if (!userOrg) {
+        if (!req.userOrg) {
             next(
             next(
                 createHttpError(
                 createHttpError(
                     HttpCode.FORBIDDEN,
                     HttpCode.FORBIDDEN,
@@ -46,7 +45,7 @@ export async function verifyOrgAccess(
             );
             );
         } else {
         } else {
             // User has access, attach the user's role to the request for potential future use
             // User has access, attach the user's role to the request for potential future use
-            req.userOrgRoleId = userOrg.roleId;
+            req.userOrgRoleId = req.userOrg.roleId;
             req.userOrgId = orgId;
             req.userOrgId = orgId;
             return next();
             return next();
         }
         }

+ 4 - 5
server/routers/auth/verifyResourceAccess.ts

@@ -18,7 +18,6 @@ export async function verifyResourceAccess(
     const userId = req.user!.userId;
     const userId = req.user!.userId;
     const resourceId =
     const resourceId =
         req.params.resourceId || req.body.resourceId || req.query.resourceId;
         req.params.resourceId || req.body.resourceId || req.query.resourceId;
-    let userOrg = req.userOrg;
 
 
     if (!userId) {
     if (!userId) {
         return next(
         return next(
@@ -51,7 +50,7 @@ export async function verifyResourceAccess(
             );
             );
         }
         }
 
 
-        if (!userOrg) {
+        if (!req.userOrg) {
             const userOrgRole = await db
             const userOrgRole = await db
                 .select()
                 .select()
                 .from(userOrgs)
                 .from(userOrgs)
@@ -62,10 +61,10 @@ export async function verifyResourceAccess(
                     )
                     )
                 )
                 )
                 .limit(1);
                 .limit(1);
-            userOrg = userOrgRole[0];
+            req.userOrg = userOrgRole[0];
         }
         }
 
 
-        if (!userOrg) {
+        if (!req.userOrg) {
             return next(
             return next(
                 createHttpError(
                 createHttpError(
                     HttpCode.FORBIDDEN,
                     HttpCode.FORBIDDEN,
@@ -74,7 +73,7 @@ export async function verifyResourceAccess(
             );
             );
         }
         }
 
 
-        const userOrgRoleId = userOrg.roleId;
+        const userOrgRoleId = req.userOrg.roleId;
         req.userOrgRoleId = userOrgRoleId;
         req.userOrgRoleId = userOrgRoleId;
         req.userOrgId = resource[0].orgId;
         req.userOrgId = resource[0].orgId;
 
 

+ 14 - 6
server/routers/auth/verifyRoleAccess.ts

@@ -15,7 +15,6 @@ export async function verifyRoleAccess(
     const roleId = parseInt(
     const roleId = parseInt(
         req.params.roleId || req.body.roleId || req.query.roleId
         req.params.roleId || req.body.roleId || req.query.roleId
     );
     );
-    let userOrg = req.userOrg;
 
 
     if (!userId) {
     if (!userId) {
         return next(
         return next(
@@ -43,7 +42,7 @@ export async function verifyRoleAccess(
             );
             );
         }
         }
 
 
-        if (!userOrg) {
+        if (!req.userOrg) {
             const userOrgRole = await db
             const userOrgRole = await db
                 .select()
                 .select()
                 .from(userOrgs)
                 .from(userOrgs)
@@ -54,10 +53,10 @@ export async function verifyRoleAccess(
                     )
                     )
                 )
                 )
                 .limit(1);
                 .limit(1);
-            userOrg = userOrgRole[0];
+            req.userOrg = userOrgRole[0];
         }
         }
 
 
-        if (!userOrg) {
+        if (!req.userOrg) {
             return next(
             return next(
                 createHttpError(
                 createHttpError(
                     HttpCode.FORBIDDEN,
                     HttpCode.FORBIDDEN,
@@ -66,8 +65,17 @@ export async function verifyRoleAccess(
             );
             );
         }
         }
 
 
-        req.userOrgRoleId = userOrg.roleId;
-        req.userOrgId = userOrg.orgId;
+        if (req.userOrg.orgId !== role[0].orgId) {
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "Role does not belong to the organization"
+                )
+            );
+        }
+
+        req.userOrgRoleId = req.userOrg.roleId;
+        req.userOrgId = req.userOrg.orgId;
 
 
         return next();
         return next();
     } catch (error) {
     } catch (error) {

+ 15 - 12
server/routers/auth/verifySiteAccess.ts

@@ -57,19 +57,22 @@ export async function verifySiteAccess(
             );
             );
         }
         }
 
 
-        // Get user's role ID in the organization
-        const userOrgRole = await db
-            .select()
-            .from(userOrgs)
-            .where(
-                and(
-                    eq(userOrgs.userId, userId),
-                    eq(userOrgs.orgId, site[0].orgId)
+        if (!req.userOrg) {
+            // Get user's role ID in the organization
+            const userOrgRole = await db
+                .select()
+                .from(userOrgs)
+                .where(
+                    and(
+                        eq(userOrgs.userId, userId),
+                        eq(userOrgs.orgId, site[0].orgId)
+                    )
                 )
                 )
-            )
-            .limit(1);
+                .limit(1);
+            req.userOrg = userOrgRole[0];
+        }
 
 
-        if (userOrgRole.length === 0) {
+        if (!req.userOrg) {
             return next(
             return next(
                 createHttpError(
                 createHttpError(
                     HttpCode.FORBIDDEN,
                     HttpCode.FORBIDDEN,
@@ -78,7 +81,7 @@ export async function verifySiteAccess(
             );
             );
         }
         }
 
 
-        const userOrgRoleId = userOrgRole[0].roleId;
+        const userOrgRoleId = req.userOrg.roleId;
         req.userOrgRoleId = userOrgRoleId;
         req.userOrgRoleId = userOrgRoleId;
         req.userOrgId = site[0].orgId;
         req.userOrgId = site[0].orgId;
 
 

+ 6 - 7
server/routers/auth/verifyTargetAccess.ts

@@ -12,7 +12,6 @@ export async function verifyTargetAccess(
 ) {
 ) {
     const userId = req.user!.userId;
     const userId = req.user!.userId;
     const targetId = parseInt(req.params.targetId);
     const targetId = parseInt(req.params.targetId);
-    let userOrg = req.userOrg;
 
 
     if (!userId) {
     if (!userId) {
         return next(
         return next(
@@ -36,7 +35,7 @@ export async function verifyTargetAccess(
         return next(
         return next(
             createHttpError(
             createHttpError(
                 HttpCode.NOT_FOUND,
                 HttpCode.NOT_FOUND,
-                `target with ID ${targetId} not found`
+                `Target with ID ${targetId} not found`
             )
             )
         );
         );
     }
     }
@@ -47,7 +46,7 @@ export async function verifyTargetAccess(
         return next(
         return next(
             createHttpError(
             createHttpError(
                 HttpCode.INTERNAL_SERVER_ERROR,
                 HttpCode.INTERNAL_SERVER_ERROR,
-                `target with ID ${targetId} does not have a resource ID`
+                `Target with ID ${targetId} does not have a resource ID`
             )
             )
         );
         );
     }
     }
@@ -77,7 +76,7 @@ export async function verifyTargetAccess(
             );
             );
         }
         }
 
 
-        if (!userOrg) {
+        if (!req.userOrg) {
             const res = await db
             const res = await db
                 .select()
                 .select()
                 .from(userOrgs)
                 .from(userOrgs)
@@ -87,10 +86,10 @@ export async function verifyTargetAccess(
                         eq(userOrgs.orgId, resource[0].orgId)
                         eq(userOrgs.orgId, resource[0].orgId)
                     )
                     )
                 );
                 );
-            userOrg = res[0];
+            req.userOrg = res[0];
         }
         }
 
 
-        if (!userOrg) {
+        if (!req.userOrg) {
             next(
             next(
                 createHttpError(
                 createHttpError(
                     HttpCode.FORBIDDEN,
                     HttpCode.FORBIDDEN,
@@ -98,7 +97,7 @@ export async function verifyTargetAccess(
                 )
                 )
             );
             );
         } else {
         } else {
-            req.userOrgRoleId = userOrg.roleId;
+            req.userOrgRoleId = req.userOrg.roleId;
             req.userOrgId = resource[0].orgId!;
             req.userOrgId = resource[0].orgId!;
             next();
             next();
         }
         }

+ 3 - 5
server/routers/auth/verifyUserAccess.ts

@@ -13,8 +13,6 @@ export async function verifyUserAccess(
     const userId = req.user!.userId;
     const userId = req.user!.userId;
     const reqUserId = req.params.userId || req.body.userId || req.query.userId;
     const reqUserId = req.params.userId || req.body.userId || req.query.userId;
 
 
-    let userOrg = req.userOrg;
-
     if (!userId) {
     if (!userId) {
         return next(
         return next(
             createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
             createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
@@ -26,7 +24,7 @@ export async function verifyUserAccess(
     }
     }
 
 
     try {
     try {
-        if (!userOrg) {
+        if (!req.userOrg) {
             const res = await db
             const res = await db
                 .select()
                 .select()
                 .from(userOrgs)
                 .from(userOrgs)
@@ -37,10 +35,10 @@ export async function verifyUserAccess(
                     )
                     )
                 )
                 )
                 .limit(1);
                 .limit(1);
-            userOrg = res[0];
+            req.userOrg = res[0];
         }
         }
 
 
-        if (userOrg) {
+        if (req.userOrg) {
             return next(
             return next(
                 createHttpError(
                 createHttpError(
                     HttpCode.FORBIDDEN,
                     HttpCode.FORBIDDEN,

+ 4 - 5
server/routers/auth/verifyUserIsOrgOwner.ts

@@ -12,7 +12,6 @@ export async function verifyUserIsOrgOwner(
 ) {
 ) {
     const userId = req.user!.userId;
     const userId = req.user!.userId;
     const orgId = req.params.orgId;
     const orgId = req.params.orgId;
-    let userOrg = req.userOrg;
 
 
     if (!userId) {
     if (!userId) {
         return next(
         return next(
@@ -30,17 +29,17 @@ export async function verifyUserIsOrgOwner(
     }
     }
 
 
     try {
     try {
-        if (!userOrg) {
+        if (!req.userOrg) {
             const res = await db
             const res = await db
                 .select()
                 .select()
                 .from(userOrgs)
                 .from(userOrgs)
                 .where(
                 .where(
                     and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))
                     and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))
                 );
                 );
-            userOrg = res[0];
+            req.userOrg = res[0];
         }
         }
 
 
-        if (!userOrg) {
+        if (!req.userOrg) {
             return next(
             return next(
                 createHttpError(
                 createHttpError(
                     HttpCode.FORBIDDEN,
                     HttpCode.FORBIDDEN,
@@ -49,7 +48,7 @@ export async function verifyUserIsOrgOwner(
             );
             );
         }
         }
 
 
-        if (!userOrg.isOwner) {
+        if (!req.userOrg.isOwner) {
             return next(
             return next(
                 createHttpError(
                 createHttpError(
                     HttpCode.FORBIDDEN,
                     HttpCode.FORBIDDEN,

+ 14 - 14
server/routers/external.ts

@@ -19,7 +19,6 @@ import {
     verifyResourceAccess,
     verifyResourceAccess,
     verifyTargetAccess,
     verifyTargetAccess,
     verifyRoleAccess,
     verifyRoleAccess,
-    verifyAdmin,
     verifyUserInRole,
     verifyUserInRole,
     verifyUserAccess,
     verifyUserAccess,
 } from "./auth";
 } from "./auth";
@@ -195,7 +194,6 @@ authenticated.delete(
 authenticated.put(
 authenticated.put(
     "/org/:orgId/role",
     "/org/:orgId/role",
     verifyOrgAccess,
     verifyOrgAccess,
-    verifyAdmin,
     verifyUserHasAction(ActionsEnum.createRole),
     verifyUserHasAction(ActionsEnum.createRole),
     role.createRole
     role.createRole
 );
 );
@@ -215,17 +213,22 @@ authenticated.get(
 // authenticated.post(
 // authenticated.post(
 //     "/role/:roleId",
 //     "/role/:roleId",
 //     verifyRoleAccess,
 //     verifyRoleAccess,
-//     verifyAdmin,
 //     verifyUserHasAction(ActionsEnum.updateRole),
 //     verifyUserHasAction(ActionsEnum.updateRole),
 //     role.updateRole
 //     role.updateRole
 // );
 // );
-// authenticated.delete(
-//     "/role/:roleId",
-//     verifyRoleAccess,
-//     verifyAdmin,
-//     verifyUserHasAction(ActionsEnum.deleteRole),
-//     role.deleteRole
-// );
+authenticated.delete(
+    "/role/:roleId",
+    verifyRoleAccess,
+    verifyUserHasAction(ActionsEnum.deleteRole),
+    role.deleteRole
+);
+authenticated.post(
+    "/role/:roleId/add/:userId",
+    verifyRoleAccess,
+    verifyUserAccess,
+    verifyUserHasAction(ActionsEnum.addUserRole),
+    user.addUserRole
+);
 
 
 // authenticated.put(
 // authenticated.put(
 //     "/role/:roleId/site",
 //     "/role/:roleId/site",
@@ -280,7 +283,6 @@ authenticated.get(
 //     "/role/:roleId/action",
 //     "/role/:roleId/action",
 //     verifyRoleAccess,
 //     verifyRoleAccess,
 //     verifyUserInRole,
 //     verifyUserInRole,
-//     verifyAdmin,
 //     verifyUserHasAction(ActionsEnum.removeRoleAction),
 //     verifyUserHasAction(ActionsEnum.removeRoleAction),
 //     role.removeRoleAction
 //     role.removeRoleAction
 // );
 // );
@@ -288,13 +290,13 @@ authenticated.get(
 //     "/role/:roleId/actions",
 //     "/role/:roleId/actions",
 //     verifyRoleAccess,
 //     verifyRoleAccess,
 //     verifyUserInRole,
 //     verifyUserInRole,
-//     verifyAdmin,
 //     verifyUserHasAction(ActionsEnum.listRoleActions),
 //     verifyUserHasAction(ActionsEnum.listRoleActions),
 //     role.listRoleActions
 //     role.listRoleActions
 // );
 // );
 
 
 unauthenticated.get("/user", verifySessionMiddleware, user.getUser);
 unauthenticated.get("/user", verifySessionMiddleware, user.getUser);
 
 
+authenticated.get("/org/:orgId/user/:userId", verifyOrgAccess, user.getOrgUser);
 authenticated.get(
 authenticated.get(
     "/org/:orgId/users",
     "/org/:orgId/users",
     verifyOrgAccess,
     verifyOrgAccess,
@@ -341,7 +343,6 @@ authenticated.delete(
 //     "/org/:orgId/user/:userId/action",
 //     "/org/:orgId/user/:userId/action",
 //     verifyOrgAccess,
 //     verifyOrgAccess,
 //     verifyUserAccess,
 //     verifyUserAccess,
-//     verifyAdmin,
 //     verifyUserHasAction(ActionsEnum.addRoleAction),
 //     verifyUserHasAction(ActionsEnum.addRoleAction),
 //     role.addRoleAction
 //     role.addRoleAction
 // );
 // );
@@ -349,7 +350,6 @@ authenticated.delete(
 //     "/org/:orgId/user/:userId/action",
 //     "/org/:orgId/user/:userId/action",
 //     verifyOrgAccess,
 //     verifyOrgAccess,
 //     verifyUserAccess,
 //     verifyUserAccess,
-//     verifyAdmin,
 //     verifyUserHasAction(ActionsEnum.removeRoleAction),
 //     verifyUserHasAction(ActionsEnum.removeRoleAction),
 //     role.removeRoleAction
 //     role.removeRoleAction
 // );
 // );

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

@@ -2,7 +2,7 @@ import { Request, Response, NextFunction } from "express";
 import { z } from "zod";
 import { z } from "zod";
 import { db } from "@server/db";
 import { db } from "@server/db";
 import { eq } from "drizzle-orm";
 import { eq } from "drizzle-orm";
-import { orgs, userOrgs } from "@server/db/schema";
+import { orgs, roleActions, roles, userOrgs } from "@server/db/schema";
 import response from "@server/utils/response";
 import response from "@server/utils/response";
 import HttpCode from "@server/types/HttpCode";
 import HttpCode from "@server/types/HttpCode";
 import createHttpError from "http-errors";
 import createHttpError from "http-errors";
@@ -10,6 +10,7 @@ import logger from "@server/logger";
 import { createAdminRole } from "@server/db/ensureActions";
 import { createAdminRole } from "@server/db/ensureActions";
 import config from "@server/config";
 import config from "@server/config";
 import { fromError } from "zod-validation-error";
 import { fromError } from "zod-validation-error";
+import { defaultRoleAllowedActions } from "../role";
 
 
 const createOrgSchema = z.object({
 const createOrgSchema = z.object({
     orgId: z.string(),
     orgId: z.string(),
@@ -96,6 +97,26 @@ export async function createOrg(
             })
             })
             .execute();
             .execute();
 
 
+        const memberRole = await db
+            .insert(roles)
+            .values({
+                name: "Member",
+                description: "Members can only view resources",
+                orgId,
+            })
+            .returning();
+
+        await db
+            .insert(roleActions)
+            .values(
+                defaultRoleAllowedActions.map((action) => ({
+                    roleId: memberRole[0].roleId,
+                    actionId: action,
+                    orgId,
+                }))
+            )
+            .execute();
+
         return response(res, {
         return response(res, {
             data: newOrg[0],
             data: newOrg[0],
             success: true,
             success: true,

+ 9 - 8
server/routers/role/createRole.ts

@@ -19,6 +19,14 @@ const createRoleSchema = z.object({
     description: z.string().optional(),
     description: z.string().optional(),
 });
 });
 
 
+export const defaultRoleAllowedActions: ActionsEnum[] = [
+    ActionsEnum.getOrg,
+    ActionsEnum.getResource,
+    ActionsEnum.listResources,
+];
+
+export type CreateRoleBody = z.infer<typeof createRoleSchema>;
+
 export type CreateRoleResponse = Role;
 export type CreateRoleResponse = Role;
 
 
 export async function createRole(
 export async function createRole(
@@ -78,17 +86,10 @@ export async function createRole(
             })
             })
             .returning();
             .returning();
 
 
-        // default allowed actions for a non admin role
-        const allowedActions: ActionsEnum[] = [
-            ActionsEnum.getOrg,
-            ActionsEnum.getResource,
-            ActionsEnum.listResources,
-        ];
-
         await db
         await db
             .insert(roleActions)
             .insert(roleActions)
             .values(
             .values(
-                allowedActions.map((action) => ({
+                defaultRoleAllowedActions.map((action) => ({
                     roleId: newRole[0].roleId,
                     roleId: newRole[0].roleId,
                     actionId: action,
                     actionId: action,
                     orgId,
                     orgId,

+ 41 - 5
server/routers/user/setUserRole.ts → server/routers/user/addUserRole.ts

@@ -9,19 +9,20 @@ import createHttpError from "http-errors";
 import logger from "@server/logger";
 import logger from "@server/logger";
 import { fromError } from "zod-validation-error";
 import { fromError } from "zod-validation-error";
 
 
-const addUserRoleSchema = z.object({
+const addUserRoleParamsSchema = z.object({
     userId: z.string(),
     userId: z.string(),
     roleId: z.number().int().positive(),
     roleId: z.number().int().positive(),
-    orgId: z.string(),
 });
 });
 
 
+export type AddUserRoleResponse = z.infer<typeof addUserRoleParamsSchema>;
+
 export async function addUserRole(
 export async function addUserRole(
     req: Request,
     req: Request,
     res: Response,
     res: Response,
     next: NextFunction
     next: NextFunction
 ): Promise<any> {
 ): Promise<any> {
     try {
     try {
-        const parsedBody = addUserRoleSchema.safeParse(req.body);
+        const parsedBody = addUserRoleParamsSchema.safeParse(req.body);
         if (!parsedBody.success) {
         if (!parsedBody.success) {
             return next(
             return next(
                 createHttpError(
                 createHttpError(
@@ -31,7 +32,42 @@ export async function addUserRole(
             );
             );
         }
         }
 
 
-        const { userId, roleId, orgId } = parsedBody.data;
+        const { userId, roleId } = parsedBody.data;
+
+        if (!req.userOrg) {
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "You do not have access to this organization"
+                )
+            );
+        }
+
+        const orgId = req.userOrg.orgId;
+
+        const existingUser = await db
+            .select()
+            .from(userOrgs)
+            .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
+            .limit(1);
+
+        if (existingUser.length === 0) {
+            return next(
+                createHttpError(
+                    HttpCode.NOT_FOUND,
+                    "User not found or does not belong to the specified organization"
+                )
+            );
+        }
+
+        if (existingUser[0].isOwner) {
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "Cannot change the role of the owner of the organization"
+                )
+            );
+        }
 
 
         const roleExists = await db
         const roleExists = await db
             .select()
             .select()
@@ -59,7 +95,7 @@ export async function addUserRole(
             success: true,
             success: true,
             error: false,
             error: false,
             message: "Role added to user successfully",
             message: "Role added to user successfully",
-            status: HttpCode.CREATED,
+            status: HttpCode.OK,
         });
         });
     } catch (error) {
     } catch (error) {
         logger.error(error);
         logger.error(error);

+ 120 - 0
server/routers/user/getOrgUser.ts

@@ -0,0 +1,120 @@
+import { Request, Response, NextFunction } from "express";
+import { z } from "zod";
+import { db } from "@server/db";
+import { roles, 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 logger from "@server/logger";
+import { fromError } from "zod-validation-error";
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+
+async function queryUser(orgId: string, userId: string) {
+    const [user] = await db
+        .select({
+            orgId: userOrgs.orgId,
+            userId: users.userId,
+            email: users.email,
+            roleId: userOrgs.roleId,
+            roleName: roles.name,
+            isOwner: userOrgs.isOwner,
+            isAdmin: roles.isAdmin,
+        })
+        .from(userOrgs)
+        .leftJoin(roles, eq(userOrgs.roleId, roles.roleId))
+        .leftJoin(users, eq(userOrgs.userId, users.userId))
+        .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
+        .limit(1);
+    return user;
+}
+
+export type GetOrgUserResponse = NonNullable<
+    Awaited<ReturnType<typeof queryUser>>
+>;
+
+const getOrgUserParamsSchema = z.object({
+    userId: z.string(),
+    orgId: z.string(),
+});
+
+export async function getOrgUser(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
+    try {
+        const parsedParams = getOrgUserParamsSchema.safeParse(req.params);
+        if (!parsedParams.success) {
+            return next(
+                createHttpError(
+                    HttpCode.BAD_REQUEST,
+                    fromError(parsedParams.error).toString()
+                )
+            );
+        }
+
+        const { orgId, userId } = parsedParams.data;
+
+        if (!req.userOrg) {
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "You do not have access to this organization"
+                )
+            );
+        }
+
+        let user;
+        user = await queryUser(orgId, userId);
+
+        if (!user) {
+            const [fullUser] = await db
+                .select()
+                .from(users)
+                .where(eq(users.email, userId))
+                .limit(1);
+
+            if (fullUser) {
+                user = await queryUser(orgId, fullUser.userId);
+            }
+        }
+
+        if (!user) {
+            return next(
+                createHttpError(
+                    HttpCode.NOT_FOUND,
+                    `User with ID ${userId} not found in org`
+                )
+            );
+        }
+
+        if (user.userId !== req.userOrg.userId) {
+            const hasPermission = await checkUserActionPermission(
+                ActionsEnum.getOrgUser,
+                req
+            );
+            if (!hasPermission) {
+                return next(
+                    createHttpError(
+                        HttpCode.FORBIDDEN,
+                        "User does not have permission perform this action"
+                    )
+                );
+            }
+        }
+
+        return response<GetOrgUserResponse>(res, {
+            data: user,
+            success: true,
+            error: false,
+            message: "User retrieved successfully",
+            status: HttpCode.OK,
+        });
+    } catch (error) {
+        logger.error(error);
+        return next(
+            createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
+        );
+    }
+}

+ 21 - 20
server/routers/user/getUser.ts

@@ -8,11 +8,23 @@ import HttpCode from "@server/types/HttpCode";
 import createHttpError from "http-errors";
 import createHttpError from "http-errors";
 import logger from "@server/logger";
 import logger from "@server/logger";
 
 
-export type GetUserResponse = {
-    email: string;
-    twoFactorEnabled: boolean;
-    emailVerified: boolean;
-};
+async function queryUser(userId: string) {
+    const [user] = await db
+        .select({
+            userId: users.userId,
+            email: users.email,
+            twoFactorEnabled: users.twoFactorEnabled,
+            emailVerified: users.emailVerified,
+        })
+        .from(users)
+        .where(eq(users.userId, userId))
+        .limit(1);
+    return user;
+}
+
+export type GetUserResponse = NonNullable<
+    Awaited<ReturnType<typeof queryUser>>
+>;
 
 
 export async function getUser(
 export async function getUser(
     req: Request,
     req: Request,
@@ -28,13 +40,9 @@ export async function getUser(
             );
             );
         }
         }
 
 
-        const user = await db
-            .select()
-            .from(users)
-            .where(eq(users.userId, userId))
-            .limit(1);
+        const user = await queryUser(userId);
 
 
-        if (user.length === 0) {
+        if (!user) {
             return next(
             return next(
                 createHttpError(
                 createHttpError(
                     HttpCode.NOT_FOUND,
                     HttpCode.NOT_FOUND,
@@ -44,11 +52,7 @@ export async function getUser(
         }
         }
 
 
         return response<GetUserResponse>(res, {
         return response<GetUserResponse>(res, {
-            data: {
-                email: user[0].email,
-                twoFactorEnabled: user[0].twoFactorEnabled,
-                emailVerified: user[0].emailVerified,
-            },
+            data: user,
             success: true,
             success: true,
             error: false,
             error: false,
             message: "User retrieved successfully",
             message: "User retrieved successfully",
@@ -57,10 +61,7 @@ export async function getUser(
     } catch (error) {
     } catch (error) {
         logger.error(error);
         logger.error(error);
         return next(
         return next(
-            createHttpError(
-                HttpCode.INTERNAL_SERVER_ERROR,
-                "An error occurred..."
-            )
+            createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
         );
         );
     }
     }
 }
 }

+ 3 - 2
server/routers/user/index.ts

@@ -1,6 +1,7 @@
 export * from "./getUser";
 export * from "./getUser";
 export * from "./removeUserOrg";
 export * from "./removeUserOrg";
 export * from "./listUsers";
 export * from "./listUsers";
-export * from "./setUserRole";
+export * from "./addUserRole";
 export * from "./inviteUser";
 export * from "./inviteUser";
-export * from "./acceptInvite";
+export * from "./acceptInvite";
+export * from "./getOrgUser";

+ 2 - 0
server/types/ArrayElement.ts

@@ -0,0 +1,2 @@
+export type ArrayElement<ArrayType extends readonly unknown[]> =
+    ArrayType extends readonly (infer ElementType)[] ? ElementType : never;

+ 43 - 0
src/app/[orgId]/page.tsx

@@ -0,0 +1,43 @@
+import { internal } from "@app/api";
+import { authCookieHeader } from "@app/api/cookies";
+import { verifySession } from "@app/lib/auth/verifySession";
+import { GetOrgUserResponse } from "@server/routers/user";
+import { AxiosResponse } from "axios";
+import { redirect } from "next/navigation";
+import { cache } from "react";
+
+type OrgPageProps = {
+    params: Promise<{ orgId: string }>;
+};
+
+export default async function OrgPage(props: OrgPageProps) {
+    const params = await props.params;
+    const orgId = params.orgId;
+
+    const getUser = cache(verifySession);
+    const user = await getUser();
+
+    if (!user) {
+        redirect("/auth/login");
+    }
+
+    const cookie = await authCookieHeader();
+
+    try {
+        const getOrgUser = cache(() =>
+            internal.get<AxiosResponse<GetOrgUserResponse>>(
+                `/org/${orgId}/user/${user.userId}`,
+                cookie
+            )
+        );
+        const orgUser = await getOrgUser();
+    } catch {
+        redirect(`/`);
+    }
+
+    return (
+        <>
+            <p>Welcome to {orgId} dashboard</p>
+        </>
+    );
+}

+ 3 - 4
src/app/[orgId]/settings/access/layout.tsx

@@ -27,13 +27,12 @@ export default async function ResourceLayout(props: AccessLayoutProps) {
                     Users & Roles
                     Users & Roles
                 </h2>
                 </h2>
                 <p className="text-muted-foreground">
                 <p className="text-muted-foreground">
-                    Manage users and roles for your organization.
+                    Invite users and add them to roles to manage access to your
+                    organization.
                 </p>
                 </p>
             </div>
             </div>
 
 
-            <SidebarSettings
-                sidebarNavItems={sidebarNavItems}
-            >
+            <SidebarSettings sidebarNavItems={sidebarNavItems}>
                 {children}
                 {children}
             </SidebarSettings>
             </SidebarSettings>
         </>
         </>

+ 179 - 0
src/app/[orgId]/settings/access/roles/components/CreateRoleForm.tsx

@@ -0,0 +1,179 @@
+"use client";
+
+import api from "@app/api";
+import { Button } from "@app/components/ui/button";
+import {
+    Form,
+    FormControl,
+    FormField,
+    FormItem,
+    FormLabel,
+    FormMessage,
+} from "@app/components/ui/form";
+import { Input } from "@app/components/ui/input";
+import { useToast } from "@app/hooks/useToast";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { AxiosResponse } from "axios";
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+import {
+    Credenza,
+    CredenzaBody,
+    CredenzaClose,
+    CredenzaContent,
+    CredenzaDescription,
+    CredenzaFooter,
+    CredenzaHeader,
+    CredenzaTitle,
+} from "@app/components/Credenza";
+import { useOrgContext } from "@app/hooks/useOrgContext";
+import { CreateRoleBody, CreateRoleResponse } from "@server/routers/role";
+
+type CreateRoleFormProps = {
+    open: boolean;
+    setOpen: (open: boolean) => void;
+    afterCreate?: (res: CreateRoleResponse) => Promise<void>;
+};
+
+const formSchema = z.object({
+    name: z.string({ message: "Name is required" }).max(32),
+    description: z.string().max(255).optional(),
+});
+
+export default function CreateRoleForm({
+    open,
+    setOpen,
+    afterCreate,
+}: CreateRoleFormProps) {
+    const { toast } = useToast();
+    const { org } = useOrgContext();
+
+    const [loading, setLoading] = useState(false);
+
+    const form = useForm<z.infer<typeof formSchema>>({
+        resolver: zodResolver(formSchema),
+        defaultValues: {
+            name: "",
+            description: "",
+        },
+    });
+
+    async function onSubmit(values: z.infer<typeof formSchema>) {
+        setLoading(true);
+
+        const res = await api
+            .put<AxiosResponse<CreateRoleResponse>>(
+                `/org/${org?.org.orgId}/role`,
+                {
+                    name: values.name,
+                    description: values.description,
+                } as CreateRoleBody
+            )
+            .catch((e) => {
+                toast({
+                    variant: "destructive",
+                    title: "Failed to create role",
+                    description:
+                        e.response?.data?.message ||
+                        "An error occurred while creating the role.",
+                });
+            });
+
+        if (res && res.status === 201) {
+            toast({
+                variant: "default",
+                title: "Role created",
+                description: "The role has been successfully created.",
+            });
+
+            if (open) {
+                setOpen(false);
+            }
+
+            if (afterCreate) {
+                afterCreate(res.data.data);
+            }
+        }
+
+        setLoading(false);
+    }
+
+    return (
+        <>
+            <Credenza
+                open={open}
+                onOpenChange={(val) => {
+                    setOpen(val);
+                    setLoading(false);
+                    form.reset();
+                }}
+            >
+                <CredenzaContent>
+                    <CredenzaHeader>
+                        <CredenzaTitle>Create Role</CredenzaTitle>
+                        <CredenzaDescription>
+                            Create a new role to group users and manage their
+                            permissions.
+                        </CredenzaDescription>
+                    </CredenzaHeader>
+                    <CredenzaBody>
+                        <Form {...form}>
+                            <form
+                                onSubmit={form.handleSubmit(onSubmit)}
+                                className="space-y-4"
+                                id="create-role-form"
+                            >
+                                <FormField
+                                    control={form.control}
+                                    name="name"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>Role Name</FormLabel>
+                                            <FormControl>
+                                                <Input
+                                                    placeholder="Enter name for the role"
+                                                    {...field}
+                                                />
+                                            </FormControl>
+                                            <FormMessage />
+                                        </FormItem>
+                                    )}
+                                />
+                                <FormField
+                                    control={form.control}
+                                    name="description"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>Description</FormLabel>
+                                            <FormControl>
+                                                <Input
+                                                    placeholder="Describe the role"
+                                                    {...field}
+                                                />
+                                            </FormControl>
+                                            <FormMessage />
+                                        </FormItem>
+                                    )}
+                                />
+                            </form>
+                        </Form>
+                    </CredenzaBody>
+                    <CredenzaFooter>
+                        <Button
+                            type="submit"
+                            form="create-role-form"
+                            loading={loading}
+                            disabled={loading}
+                        >
+                            Create Role
+                        </Button>
+                        <CredenzaClose asChild>
+                            <Button variant="outline">Close</Button>
+                        </CredenzaClose>
+                    </CredenzaFooter>
+                </CredenzaContent>
+            </Credenza>
+        </>
+    );
+}

+ 4 - 0
src/app/[orgId]/settings/access/roles/components/RolesDataTable.tsx

@@ -51,6 +51,10 @@ export function RolesDataTable<TData, TValue>({
         state: {
         state: {
             sorting,
             sorting,
             columnFilters,
             columnFilters,
+            pagination: {
+                pageSize: 100,
+                pageIndex: 0,
+            },
         },
         },
     });
     });
 
 

+ 27 - 4
src/app/[orgId]/settings/access/roles/components/RolesTable.tsx

@@ -16,6 +16,7 @@ import { useOrgContext } from "@app/hooks/useOrgContext";
 import { useToast } from "@app/hooks/useToast";
 import { useToast } from "@app/hooks/useToast";
 import { RolesDataTable } from "./RolesDataTable";
 import { RolesDataTable } from "./RolesDataTable";
 import { Role } from "@server/db/schema";
 import { Role } from "@server/db/schema";
+import CreateRoleForm from "./CreateRoleForm";
 
 
 export type RoleRow = Role;
 export type RoleRow = Role;
 
 
@@ -23,8 +24,12 @@ type RolesTableProps = {
     roles: RoleRow[];
     roles: RoleRow[];
 };
 };
 
 
-export default function UsersTable({ roles }: RolesTableProps) {
+export default function UsersTable({ roles: r }: RolesTableProps) {
+    const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
     const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
     const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+
+    const [roles, setRoles] = useState<RoleRow[]>(r);
+
     const [roleToRemove, setUserToRemove] = useState<RoleRow | null>(null);
     const [roleToRemove, setUserToRemove] = useState<RoleRow | null>(null);
 
 
     const { org } = useOrgContext();
     const { org } = useOrgContext();
@@ -80,7 +85,7 @@ export default function UsersTable({ roles }: RolesTableProps) {
                                                 setUserToRemove(roleRow);
                                                 setUserToRemove(roleRow);
                                             }}
                                             }}
                                         >
                                         >
-                                            Remove User
+                                            Delete Role
                                         </button>
                                         </button>
                                     </DropdownMenuItem>
                                     </DropdownMenuItem>
                                 </DropdownMenuContent>
                                 </DropdownMenuContent>
@@ -95,7 +100,7 @@ export default function UsersTable({ roles }: RolesTableProps) {
     async function removeRole() {
     async function removeRole() {
         if (roleToRemove) {
         if (roleToRemove) {
             const res = await api
             const res = await api
-                .delete(`/org/${org!.org.orgId}/role/${roleToRemove.roleId}`)
+                .delete(`/role/${roleToRemove.roleId}`)
                 .catch((e) => {
                 .catch((e) => {
                     toast({
                     toast({
                         variant: "destructive",
                         variant: "destructive",
@@ -112,6 +117,10 @@ export default function UsersTable({ roles }: RolesTableProps) {
                     title: "Role removed",
                     title: "Role removed",
                     description: `The role ${roleToRemove.name} has been removed from the organization.`,
                     description: `The role ${roleToRemove.name} has been removed from the organization.`,
                 });
                 });
+
+                setRoles((prev) =>
+                    prev.filter((role) => role.roleId !== roleToRemove.roleId)
+                );
             }
             }
         }
         }
         setIsDeleteModalOpen(false);
         setIsDeleteModalOpen(false);
@@ -119,6 +128,14 @@ export default function UsersTable({ roles }: RolesTableProps) {
 
 
     return (
     return (
         <>
         <>
+            <CreateRoleForm
+                open={isCreateModalOpen}
+                setOpen={setIsCreateModalOpen}
+                afterCreate={async (role) => {
+                    setRoles((prev) => [...prev, role]);
+                }}
+            />
+
             <ConfirmDeleteDialog
             <ConfirmDeleteDialog
                 open={isDeleteModalOpen}
                 open={isDeleteModalOpen}
                 setOpen={(val) => {
                 setOpen={(val) => {
@@ -148,7 +165,13 @@ export default function UsersTable({ roles }: RolesTableProps) {
                 title="Remove role from organization"
                 title="Remove role from organization"
             />
             />
 
 
-            <RolesDataTable columns={columns} data={roles} />
+            <RolesDataTable
+                columns={columns}
+                data={roles}
+                addRole={() => {
+                    setIsCreateModalOpen(true);
+                }}
+            />
         </>
         </>
     );
     );
 }
 }

+ 57 - 0
src/app/[orgId]/settings/access/users/[userId]/layout.tsx

@@ -0,0 +1,57 @@
+import SiteProvider from "@app/providers/SiteProvider";
+import { internal } from "@app/api";
+import { GetSiteResponse } from "@server/routers/site";
+import { AxiosResponse } from "axios";
+import { redirect } from "next/navigation";
+import { authCookieHeader } from "@app/api/cookies";
+import { SidebarSettings } from "@app/components/SidebarSettings";
+import { GetOrgUserResponse } from "@server/routers/user";
+
+interface UserLayoutProps {
+    children: React.ReactNode;
+    params: Promise<{ userId: string; orgId: string }>;
+}
+
+export default async function UserLayoutProps(props: UserLayoutProps) {
+    const params = await props.params;
+
+    const { children } = props;
+
+    let user = null;
+    try {
+        const res = await internal.get<AxiosResponse<GetOrgUserResponse>>(
+            `/org/${params.orgId}/user/${params.userId}`,
+            await authCookieHeader()
+        );
+        user = res.data.data;
+    } catch {
+        redirect(`/${params.orgId}/settings/sites`);
+    }
+
+    const sidebarNavItems = [
+        {
+            title: "General",
+            href: "/{orgId}/settings/access/users/{userId}",
+        },
+    ];
+
+    return (
+        <>
+            <div className="space-y-0.5 select-none mb-6">
+                <h2 className="text-2xl font-bold tracking-tight">
+                    User {user?.email}
+                </h2>
+                <p className="text-muted-foreground">
+                    Manage user access and permissions
+                </p>
+            </div>
+
+            <SidebarSettings
+                sidebarNavItems={sidebarNavItems}
+                limitWidth={true}
+            >
+                {children}
+            </SidebarSettings>
+        </>
+    );
+}

+ 20 - 0
src/app/[orgId]/settings/access/users/[userId]/page.tsx

@@ -0,0 +1,20 @@
+import React from "react";
+import { Separator } from "@/components/ui/separator";
+
+export default async function UserPage(props: {
+    params: Promise<{ niceId: string }>;
+}) {
+    const params = await props.params;
+
+    return (
+        <div className="space-y-6">
+            <div>
+                <h3 className="text-lg font-medium">Manage User</h3>
+                <p className="text-sm text-muted-foreground">
+                    Manage user access and permissions
+                </p>
+            </div>
+            <Separator />
+        </div>
+    );
+}

+ 226 - 0
src/app/[orgId]/settings/access/users/components/ManageUserForm.tsx

@@ -0,0 +1,226 @@
+"use client";
+
+import api from "@app/api";
+import { Button } from "@app/components/ui/button";
+import {
+    Form,
+    FormControl,
+    FormField,
+    FormItem,
+    FormLabel,
+    FormMessage,
+} from "@app/components/ui/form";
+import { Input } from "@app/components/ui/input";
+import {
+    Select,
+    SelectContent,
+    SelectItem,
+    SelectTrigger,
+    SelectValue,
+} from "@app/components/ui/select";
+import { useToast } from "@app/hooks/useToast";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { InviteUserResponse, ListUsersResponse } from "@server/routers/user";
+import { AxiosResponse } from "axios";
+import { useEffect, useState } from "react";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+import {
+    Credenza,
+    CredenzaBody,
+    CredenzaClose,
+    CredenzaContent,
+    CredenzaDescription,
+    CredenzaFooter,
+    CredenzaHeader,
+    CredenzaTitle,
+} from "@app/components/Credenza";
+import { useOrgContext } from "@app/hooks/useOrgContext";
+import { ListRolesResponse } from "@server/routers/role";
+import { ArrayElement } from "@server/types/ArrayElement";
+
+type ManageUserFormProps = {
+    open: boolean;
+    setOpen: (open: boolean) => void;
+    user: ArrayElement<ListUsersResponse["users"]>;
+    onUserUpdate(): (
+        user: ArrayElement<ListUsersResponse["users"]>
+    ) => Promise<void>;
+};
+
+const formSchema = z.object({
+    email: z.string().email({ message: "Please enter a valid email" }),
+    roleId: z.string().min(1, { message: "Please select a role" }),
+});
+
+export default function ManageUserForm({
+    open,
+    setOpen,
+    user,
+}: ManageUserFormProps) {
+    const { toast } = useToast();
+    const { org } = useOrgContext();
+
+    const [loading, setLoading] = useState(false);
+    const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]);
+
+    const form = useForm<z.infer<typeof formSchema>>({
+        resolver: zodResolver(formSchema),
+        defaultValues: {
+            email: user.email,
+            roleId: user.roleId?.toString(),
+        },
+    });
+
+    useEffect(() => {
+        if (!open) {
+            return;
+        }
+
+        async function fetchRoles() {
+            const res = await api
+                .get<AxiosResponse<ListRolesResponse>>(
+                    `/org/${org?.org.orgId}/roles`
+                )
+                .catch((e) => {
+                    console.error(e);
+                    toast({
+                        variant: "destructive",
+                        title: "Failed to fetch roles",
+                        description:
+                            e.message ||
+                            "An error occurred while fetching the roles",
+                    });
+                });
+
+            if (res?.status === 200) {
+                setRoles(res.data.data.roles);
+                // form.setValue(
+                //     "roleId",
+                //     res.data.data.roles[0].roleId.toString()
+                // );
+            }
+        }
+
+        fetchRoles();
+    }, [open]);
+
+    async function onSubmit(values: z.infer<typeof formSchema>) {
+        setLoading(true);
+
+        const res = await api
+            .post<AxiosResponse<InviteUserResponse>>(
+                `/role/${values.roleId}/add/${user.id}`
+            )
+            .catch((e) => {
+                toast({
+                    variant: "destructive",
+                    title: "Failed to add user to role",
+                    description:
+                        e.response?.data?.message ||
+                        "An error occurred while adding user to the role.",
+                });
+            });
+
+        if (res && res.status === 200) {
+            toast({
+                variant: "default",
+                title: "User invited",
+                description: "The user has been updated.",
+            });
+        }
+
+        setLoading(false);
+    }
+
+    return (
+        <>
+            <Credenza
+                open={open}
+                onOpenChange={(val) => {
+                    setOpen(val);
+                    setLoading(false);
+                    form.reset();
+                }}
+            >
+                <CredenzaContent>
+                    <CredenzaHeader>
+                        <CredenzaTitle>Manage User</CredenzaTitle>
+                        <CredenzaDescription>
+                            Update the role of the user in the organization.
+                        </CredenzaDescription>
+                    </CredenzaHeader>
+                    <CredenzaBody>
+                        <Form {...form}>
+                            <form
+                                onSubmit={form.handleSubmit(onSubmit)}
+                                className="space-y-4"
+                                id="manage-user-form"
+                            >
+                                <FormField
+                                    control={form.control}
+                                    name="email"
+                                    disabled={true}
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>Email</FormLabel>
+                                            <FormControl>
+                                                <Input
+                                                    placeholder="User's email"
+                                                    {...field}
+                                                />
+                                            </FormControl>
+                                            <FormMessage />
+                                        </FormItem>
+                                    )}
+                                />
+                                <FormField
+                                    control={form.control}
+                                    name="roleId"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>Role</FormLabel>
+                                            <Select
+                                                onValueChange={field.onChange}
+                                            >
+                                                <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>
+                                    )}
+                                />
+                            </form>
+                        </Form>
+                    </CredenzaBody>
+                    <CredenzaFooter>
+                        <Button
+                            type="submit"
+                            form="manage-user-form"
+                            loading={loading}
+                            disabled={loading}
+                        >
+                            Save User
+                        </Button>
+                        <CredenzaClose asChild>
+                            <Button variant="outline">Close</Button>
+                        </CredenzaClose>
+                    </CredenzaFooter>
+                </CredenzaContent>
+            </Credenza>
+        </>
+    );
+}

+ 4 - 0
src/app/[orgId]/settings/access/users/components/UsersDataTable.tsx

@@ -51,6 +51,10 @@ export function UsersDataTable<TData, TValue>({
         state: {
         state: {
             sorting,
             sorting,
             columnFilters,
             columnFilters,
+            pagination: {
+                pageSize: 100,
+                pageIndex: 0,
+            },
         },
         },
     });
     });
 
 

+ 22 - 10
src/app/[orgId]/settings/access/users/components/UsersTable.tsx

@@ -17,6 +17,8 @@ import { useUserContext } from "@app/hooks/useUserContext";
 import api from "@app/api";
 import api from "@app/api";
 import { useOrgContext } from "@app/hooks/useOrgContext";
 import { useOrgContext } from "@app/hooks/useOrgContext";
 import { useToast } from "@app/hooks/useToast";
 import { useToast } from "@app/hooks/useToast";
+import ManageUserForm from "./ManageUserForm";
+import Link from "next/link";
 
 
 export type UserRow = {
 export type UserRow = {
     id: string;
     id: string;
@@ -30,10 +32,12 @@ type UsersTableProps = {
     users: UserRow[];
     users: UserRow[];
 };
 };
 
 
-export default function UsersTable({ users }: UsersTableProps) {
+export default function UsersTable({ users: u }: UsersTableProps) {
     const [isInviteModalOpen, setIsInviteModalOpen] = useState(false);
     const [isInviteModalOpen, setIsInviteModalOpen] = useState(false);
     const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
     const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
-    const [userToRemove, setUserToRemove] = useState<UserRow | null>(null);
+    const [selectedUser, setSelectedUser] = useState<UserRow | null>(null);
+
+    const [users, setUsers] = useState<UserRow[]>(u);
 
 
     const user = useUserContext();
     const user = useUserContext();
     const { org } = useOrgContext();
     const { org } = useOrgContext();
@@ -120,7 +124,11 @@ export default function UsersTable({ users }: UsersTableProps) {
                                 </DropdownMenuTrigger>
                                 </DropdownMenuTrigger>
                                 <DropdownMenuContent align="end">
                                 <DropdownMenuContent align="end">
                                     <DropdownMenuItem>
                                     <DropdownMenuItem>
-                                        Manage user
+                                        <Link
+                                            href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
+                                        >
+                                            Manage User
+                                        </Link>
                                     </DropdownMenuItem>
                                     </DropdownMenuItem>
                                     {userRow.email !== user?.email && (
                                     {userRow.email !== user?.email && (
                                         <DropdownMenuItem>
                                         <DropdownMenuItem>
@@ -128,7 +136,7 @@ export default function UsersTable({ users }: UsersTableProps) {
                                                 className="text-red-600 hover:text-red-800"
                                                 className="text-red-600 hover:text-red-800"
                                                 onClick={() => {
                                                 onClick={() => {
                                                     setIsDeleteModalOpen(true);
                                                     setIsDeleteModalOpen(true);
-                                                    setUserToRemove(userRow);
+                                                    setSelectedUser(userRow);
                                                 }}
                                                 }}
                                             >
                                             >
                                                 Remove User
                                                 Remove User
@@ -145,9 +153,9 @@ export default function UsersTable({ users }: UsersTableProps) {
     ];
     ];
 
 
     async function removeUser() {
     async function removeUser() {
-        if (userToRemove) {
+        if (selectedUser) {
             const res = await api
             const res = await api
-                .delete(`/org/${org!.org.orgId}/user/${userToRemove.id}`)
+                .delete(`/org/${org!.org.orgId}/user/${selectedUser.id}`)
                 .catch((e) => {
                 .catch((e) => {
                     toast({
                     toast({
                         variant: "destructive",
                         variant: "destructive",
@@ -162,8 +170,12 @@ export default function UsersTable({ users }: UsersTableProps) {
                 toast({
                 toast({
                     variant: "default",
                     variant: "default",
                     title: "User removed",
                     title: "User removed",
-                    description: `The user ${userToRemove.email} has been removed from the organization.`,
+                    description: `The user ${selectedUser.email} has been removed from the organization.`,
                 });
                 });
+
+                setUsers((prev) =>
+                    prev.filter((u) => u.id !== selectedUser?.id)
+                );
             }
             }
         }
         }
         setIsDeleteModalOpen(false);
         setIsDeleteModalOpen(false);
@@ -175,13 +187,13 @@ export default function UsersTable({ users }: UsersTableProps) {
                 open={isDeleteModalOpen}
                 open={isDeleteModalOpen}
                 setOpen={(val) => {
                 setOpen={(val) => {
                     setIsDeleteModalOpen(val);
                     setIsDeleteModalOpen(val);
-                    setUserToRemove(null);
+                    setSelectedUser(null);
                 }}
                 }}
                 dialog={
                 dialog={
                     <div>
                     <div>
                         <p className="mb-2">
                         <p className="mb-2">
                             Are you sure you want to remove{" "}
                             Are you sure you want to remove{" "}
-                            <b>{userToRemove?.email}</b> from the organization?
+                            <b>{selectedUser?.email}</b> from the organization?
                         </p>
                         </p>
 
 
                         <p className="mb-2">
                         <p className="mb-2">
@@ -199,7 +211,7 @@ export default function UsersTable({ users }: UsersTableProps) {
                 }
                 }
                 buttonText="Confirm remove user"
                 buttonText="Confirm remove user"
                 onConfirm={removeUser}
                 onConfirm={removeUser}
-                string={userToRemove?.email ?? ""}
+                string={selectedUser?.email ?? ""}
                 title="Remove user from organization"
                 title="Remove user from organization"
             />
             />
 
 

+ 10 - 5
src/app/[orgId]/settings/layout.tsx

@@ -9,6 +9,7 @@ import { AxiosResponse } from "axios";
 import { GetOrgResponse, ListOrgsResponse } from "@server/routers/org";
 import { GetOrgResponse, ListOrgsResponse } from "@server/routers/org";
 import { authCookieHeader } from "@app/api/cookies";
 import { authCookieHeader } from "@app/api/cookies";
 import { cache } from "react";
 import { cache } from "react";
+import { GetOrgUserResponse } from "@server/routers/user";
 
 
 export const dynamic = "force-dynamic";
 export const dynamic = "force-dynamic";
 
 
@@ -60,15 +61,19 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
     const cookie = await authCookieHeader();
     const cookie = await authCookieHeader();
 
 
     try {
     try {
-        const getOrg = cache(() =>
-            internal.get<AxiosResponse<GetOrgResponse>>(
-                `/org/${params.orgId}`,
+        const getOrgUser = cache(() =>
+            internal.get<AxiosResponse<GetOrgUserResponse>>(
+                `/org/${params.orgId}/user/${user.userId}`,
                 cookie
                 cookie
             )
             )
         );
         );
-        const org = await getOrg();
+        const orgUser = await getOrgUser();
+
+        if (!orgUser.data.data.isAdmin || !orgUser.data.data.isOwner) {
+            throw new Error("User is not an admin or owner");
+        }
     } catch {
     } catch {
-        redirect(`/`);
+        redirect(`/${params.orgId}`);
     }
     }
 
 
     let orgs: ListOrgsResponse["orgs"] = [];
     let orgs: ListOrgsResponse["orgs"] = [];

+ 4 - 0
src/app/[orgId]/settings/resources/components/ResourcesDataTable.tsx

@@ -52,6 +52,10 @@ export function ResourcesDataTable<TData, TValue>({
         state: {
         state: {
             sorting,
             sorting,
             columnFilters,
             columnFilters,
+            pagination: {
+                pageSize: 100,
+                pageIndex: 0,
+            },
         },
         },
     });
     });
 
 

+ 4 - 0
src/app/[orgId]/settings/sites/components/SitesDataTable.tsx

@@ -52,6 +52,10 @@ export function SitesDataTable<TData, TValue>({
         state: {
         state: {
             sorting,
             sorting,
             columnFilters,
             columnFilters,
+            pagination: {
+                pageSize: 100,
+                pageIndex: 0,
+            },
         },
         },
     });
     });
 
 

+ 1 - 1
src/components/DataTablePagination.tsx

@@ -41,7 +41,7 @@ export function DataTablePagination<TData>({
                             />
                             />
                         </SelectTrigger>
                         </SelectTrigger>
                         <SelectContent side="top">
                         <SelectContent side="top">
-                            {[10, 20, 30, 40, 50].map((pageSize) => (
+                            {[10, 20, 30, 40, 50, 100, 200].map((pageSize) => (
                                 <SelectItem
                                 <SelectItem
                                     key={pageSize}
                                     key={pageSize}
                                     value={`${pageSize}`}
                                     value={`${pageSize}`}

+ 6 - 14
src/components/sidebar-nav.tsx

@@ -32,6 +32,7 @@ export function SidebarNav({
     const orgId = params.orgId as string;
     const orgId = params.orgId as string;
     const niceId = params.niceId as string;
     const niceId = params.niceId as string;
     const resourceId = params.resourceId as string;
     const resourceId = params.resourceId as string;
+    const userId = params.userId as string;
 
 
     const router = useRouter();
     const router = useRouter();
 
 
@@ -50,7 +51,8 @@ export function SidebarNav({
         return val
         return val
             .replace("{orgId}", orgId)
             .replace("{orgId}", orgId)
             .replace("{niceId}", niceId)
             .replace("{niceId}", niceId)
-            .replace("{resourceId}", resourceId);
+            .replace("{resourceId}", resourceId)
+            .replace("{userId}", userId);
     }
     }
 
 
     return (
     return (
@@ -86,21 +88,11 @@ export function SidebarNav({
             >
             >
                 {items.map((item) => (
                 {items.map((item) => (
                     <Link
                     <Link
-                        key={item.href
-                            .replace("{orgId}", orgId)
-                            .replace("{niceId}", niceId)
-                            .replace("{resourceId}", resourceId)}
-                        href={item.href
-                            .replace("{orgId}", orgId)
-                            .replace("{niceId}", niceId)
-                            .replace("{resourceId}", resourceId)}
+                        key={hydrateHref(item.href)}
+                        href={hydrateHref(item.href)}
                         className={cn(
                         className={cn(
                             buttonVariants({ variant: "ghost" }),
                             buttonVariants({ variant: "ghost" }),
-                            pathname ===
-                                item.href
-                                    .replace("{orgId}", orgId)
-                                    .replace("{niceId}", niceId)
-                                    .replace("{resourceId}", resourceId) &&
+                            pathname === hydrateHref(item.href) &&
                                 !pathname.includes("create")
                                 !pathname.includes("create")
                                 ? "bg-muted hover:bg-muted dark:bg-border dark:hover:bg-border"
                                 ? "bg-muted hover:bg-muted dark:bg-border dark:hover:bg-border"
                                 : "hover:bg-transparent hover:underline",
                                 : "hover:bg-transparent hover:underline",