Просмотр исходного кода

ability to remove user from org

Milo Schwartz 8 месяцев назад
Родитель
Сommit
fadfaf1f0b

+ 3 - 3
scripts/hydrate.ts

@@ -7,7 +7,7 @@
 //     targets,
 //     targets,
 // } from "@server/db/schema";
 // } from "@server/db/schema";
 // import db from "@server/db";
 // import db from "@server/db";
-// import { createSuperuserRole } from "@server/db/ensureActions";
+// import { createSuperUserRole } from "@server/db/ensureActions";
 
 
 async function insertDummyData() {
 async function insertDummyData() {
     // // Insert dummy orgs
     // // Insert dummy orgs
@@ -21,7 +21,7 @@ async function insertDummyData() {
     //     .returning()
     //     .returning()
     //     .get();
     //     .get();
 
 
-    // await createSuperuserRole(org1.orgId!);
+    // await createSuperUserRole(org1.orgId!);
 
 
     // const org2 = db
     // const org2 = db
     //     .insert(orgs)
     //     .insert(orgs)
@@ -33,7 +33,7 @@ async function insertDummyData() {
     //     .returning()
     //     .returning()
     //     .get();
     //     .get();
 
 
-    // await createSuperuserRole(org2.orgId!);
+    // await createSuperUserRole(org2.orgId!);
 
 
     // // Insert dummy exit nodes
     // // Insert dummy exit nodes
     // const exitNode1 = db
     // const exitNode1 = db

+ 6 - 6
server/db/ensureActions.ts

@@ -15,7 +15,7 @@ export async function ensureActions() {
     const defaultRoles = await db
     const defaultRoles = await db
         .select()
         .select()
         .from(roles)
         .from(roles)
-        .where(eq(roles.isSuperuserRole, true))
+        .where(eq(roles.isSuperUserRole, true))
         .execute();
         .execute();
 
 
     // Add new actions
     // Add new actions
@@ -38,15 +38,15 @@ export async function ensureActions() {
     }
     }
 }
 }
 
 
-export async function createSuperuserRole(orgId: string) {
+export async function createSuperUserRole(orgId: string) {
     // Create the Default role if it doesn't exist
     // Create the Default role if it doesn't exist
     const [insertedRole] = await db
     const [insertedRole] = await db
         .insert(roles)
         .insert(roles)
         .values({
         .values({
             orgId,
             orgId,
-            isSuperuserRole: true,
-            name: 'Superuser',
-            description: 'Superuser role with all actions'
+            isSuperUserRole: true,
+            name: 'Super User',
+            description: 'Super User role with all actions'
         })
         })
         .returning({ roleId: roles.roleId })
         .returning({ roleId: roles.roleId })
         .execute();
         .execute();
@@ -56,7 +56,7 @@ export async function createSuperuserRole(orgId: string) {
     const actionIds = await db.select().from(actions).execute();
     const actionIds = await db.select().from(actions).execute();
 
 
     if (actionIds.length === 0) {
     if (actionIds.length === 0) {
-        logger.info('No actions to assign to the Superuser role');
+        logger.info('No actions to assign to the Super User role');
         return;
         return;
     }
     }
 
 

+ 1 - 1
server/db/schema.ts

@@ -131,7 +131,7 @@ export const roles = sqliteTable("roles", {
     orgId: text("orgId").references(() => orgs.orgId, {
     orgId: text("orgId").references(() => orgs.orgId, {
         onDelete: "cascade",
         onDelete: "cascade",
     }),
     }),
-    isSuperuserRole: integer("isSuperuserRole", { mode: "boolean" }),
+    isSuperUserRole: integer("isSuperUserRole", { mode: "boolean" }),
     name: text("name").notNull(),
     name: text("name").notNull(),
     description: text("description"),
     description: text("description"),
 });
 });

+ 75 - 0
server/emails/templates/SendInviteLink.tsx

@@ -0,0 +1,75 @@
+import {
+    Body,
+    Container,
+    Head,
+    Heading,
+    Html,
+    Preview,
+    Section,
+    Text,
+    Tailwind,
+    Button,
+} from "@react-email/components";
+import * as React from "react";
+
+interface SendInviteLinkProps {
+    email: string;
+    inviteLink: string;
+    orgName: string;
+    inviterName?: string;
+    expiresInDays: string;
+}
+
+export const SendInviteLink = ({
+    email,
+    inviteLink,
+    orgName,
+    inviterName,
+    expiresInDays,
+}: SendInviteLinkProps) => {
+    const previewText = `${inviterName} invited to join ${orgName}`;
+
+    return (
+        <Html>
+            <Head />
+            <Preview>{previewText}</Preview>
+            <Tailwind>
+                <Body className="font-sans">
+                    <Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8">
+                        <Heading className="text-2xl font-semibold text-gray-800 text-center">
+                            You're invite to join a Fossorial organization
+                        </Heading>
+                        <Text className="text-base text-gray-700 mt-4">
+                            Hi {email || "there"},
+                        </Text>
+                        <Text className="text-base text-gray-700 mt-2">
+                            You’ve been invited to join the organization{" "}
+                            {orgName}
+                            {inviterName ? ` by ${inviterName}.` : ""}. Please
+                            access the link below to accept the invite.
+                        </Text>
+                        <Text className="text-base text-gray-700 mt-2">
+                            This invite will expire in{" "}
+                            <b>{expiresInDays} days.</b>
+                        </Text>
+                        <Section className="text-center my-6">
+                            <Button
+                                href={inviteLink}
+                                className="rounded-md bg-gray-600 px-[12px] py-[12px] text-center font-semibold text-white cursor-pointer"
+                            >
+                                Accept invitation to {orgName}
+                            </Button>
+                        </Section>
+                        <Text className="text-sm text-gray-500 mt-6">
+                            Best regards,
+                            <br />
+                            Fossorial
+                        </Text>
+                    </Container>
+                </Body>
+            </Tailwind>
+        </Html>
+    );
+};
+
+export default SendInviteLink;

+ 1 - 1
server/routers/auth/index.ts

@@ -11,7 +11,7 @@ export * from "./verifyResourceAccess";
 export * from "./verifyTargetAccess";
 export * from "./verifyTargetAccess";
 export * from "./verifyRoleAccess";
 export * from "./verifyRoleAccess";
 export * from "./verifyUserAccess";
 export * from "./verifyUserAccess";
-export * from "./verifySuperuser";
+export * from "./verifySuperUser";
 export * from "./verifyEmail";
 export * from "./verifyEmail";
 export * from "./requestEmailVerificationCode";
 export * from "./requestEmailVerificationCode";
 export * from "./changePassword";
 export * from "./changePassword";

+ 1 - 1
server/routers/auth/sendEmailVerificationCode.ts

@@ -4,7 +4,7 @@ import db from "@server/db";
 import { users, emailVerificationCodes } from "@server/db/schema";
 import { users, emailVerificationCodes } from "@server/db/schema";
 import { eq } from "drizzle-orm";
 import { eq } from "drizzle-orm";
 import { sendEmail } from "@server/emails";
 import { sendEmail } from "@server/emails";
-import VerifyEmail from "@server/emails/templates/verifyEmailCode";
+import VerifyEmail from "@server/emails/templates/VerifyEmailCode";
 import config from "@server/config";
 import config from "@server/config";
 
 
 export async function sendEmailVerificationCode(
 export async function sendEmailVerificationCode(

+ 30 - 12
server/routers/auth/verifyOrgAccess.ts

@@ -1,21 +1,29 @@
-import { Request, Response, NextFunction } from 'express';
-import { db } from '@server/db';
-import { userOrgs } from '@server/db/schema';
-import { and, eq } from 'drizzle-orm';
-import createHttpError from 'http-errors';
-import HttpCode from '@server/types/HttpCode';
-import { AuthenticatedRequest } from '@server/types/Auth';
+import { Request, Response, NextFunction } from "express";
+import { db } from "@server/db";
+import { userOrgs } from "@server/db/schema";
+import { and, eq } from "drizzle-orm";
+import createHttpError from "http-errors";
+import HttpCode from "@server/types/HttpCode";
+import { AuthenticatedRequest } from "@server/types/Auth";
 
 
-export function verifyOrgAccess(req: Request, res: Response, next: NextFunction) {
+export function verifyOrgAccess(
+    req: Request,
+    res: Response,
+    next: NextFunction
+) {
     const userId = req.user!.userId; // Assuming you have user information in the request
     const userId = req.user!.userId; // Assuming you have user information in the request
     const orgId = req.params.orgId;
     const orgId = req.params.orgId;
 
 
     if (!userId) {
     if (!userId) {
-        return next(createHttpError(HttpCode.UNAUTHORIZED, 'User not authenticated'));
+        return next(
+            createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
+        );
     }
     }
 
 
     if (!orgId) {
     if (!orgId) {
-        return next(createHttpError(HttpCode.BAD_REQUEST, 'Invalid organization ID'));
+        return next(
+            createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
+        );
     }
     }
 
 
     db.select()
     db.select()
@@ -23,7 +31,12 @@ export function verifyOrgAccess(req: Request, res: Response, next: NextFunction)
         .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
         .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
         .then((result) => {
         .then((result) => {
             if (result.length === 0) {
             if (result.length === 0) {
-                next(createHttpError(HttpCode.FORBIDDEN, 'User does not have access to this organization'));
+                next(
+                    createHttpError(
+                        HttpCode.FORBIDDEN,
+                        "User does not have access to this organization"
+                    )
+                );
             } 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 = result[0].roleId;
                 req.userOrgRoleId = result[0].roleId;
@@ -32,6 +45,11 @@ export function verifyOrgAccess(req: Request, res: Response, next: NextFunction)
             }
             }
         })
         })
         .catch((error) => {
         .catch((error) => {
-            next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, 'Error verifying organization access'));
+            next(
+                createHttpError(
+                    HttpCode.INTERNAL_SERVER_ERROR,
+                    "Error verifying organization access"
+                )
+            );
         });
         });
 }
 }

+ 4 - 4
server/routers/auth/verifySuperuser.ts

@@ -6,7 +6,7 @@ import createHttpError from 'http-errors';
 import HttpCode from '@server/types/HttpCode';
 import HttpCode from '@server/types/HttpCode';
 import logger from '@server/logger';
 import logger from '@server/logger';
 
 
-export async function verifySuperuser(req: Request, res: Response, next: NextFunction) {
+export async function verifySuperUser(req: Request, res: Response, next: NextFunction) {
     const userId = req.user?.userId; // Assuming you have user information in the request
     const userId = req.user?.userId; // Assuming you have user information in the request
     const orgId = req.userOrgId;
     const orgId = req.userOrgId;
 
 
@@ -30,14 +30,14 @@ export async function verifySuperuser(req: Request, res: Response, next: NextFun
         }
         }
 
 
         // get userOrgRole[0].roleId
         // get userOrgRole[0].roleId
-        // Check if the user's role in the organization is a superuser role
+        // Check if the user's role in the organization is a Super User role
         const userRole = await db.select()
         const userRole = await db.select()
             .from(roles)
             .from(roles)
             .where(eq(roles.roleId, userOrgRole[0].roleId))
             .where(eq(roles.roleId, userOrgRole[0].roleId))
             .limit(1);
             .limit(1);
 
 
-        if (userRole.length === 0 || !userRole[0].isSuperuserRole) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have superuser access'));
+        if (userRole.length === 0 || !userRole[0].isSuperUserRole) {
+            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have Super User access'));
         }
         }
 
 
         return next();
         return next();

+ 43 - 18
server/routers/auth/verifyUserAccess.ts

@@ -1,37 +1,62 @@
-import { Request, Response, NextFunction } from 'express';
-import { db } from '@server/db';
-import { sites, userOrgs, userSites, roleSites, roles } from '@server/db/schema';
-import { and, eq, or } from 'drizzle-orm';
-import createHttpError from 'http-errors';
-import HttpCode from '@server/types/HttpCode';
-
-export async function verifyUserAccess(req: Request, res: Response, next: NextFunction) {
+import { Request, Response, NextFunction } from "express";
+import { db } from "@server/db";
+import {
+    sites,
+    userOrgs,
+    userSites,
+    roleSites,
+    roles,
+} from "@server/db/schema";
+import { and, eq, or } from "drizzle-orm";
+import createHttpError from "http-errors";
+import HttpCode from "@server/types/HttpCode";
+
+export async function verifyUserAccess(
+    req: Request,
+    res: Response,
+    next: NextFunction
+) {
     const userId = req.user!.userId; // Assuming you have user information in the request
     const userId = req.user!.userId; // Assuming you have user information in the request
     const reqUserId = req.params.userId || req.body.userId || req.query.userId;
     const reqUserId = req.params.userId || req.body.userId || req.query.userId;
 
 
     if (!userId) {
     if (!userId) {
-        return next(createHttpError(HttpCode.UNAUTHORIZED, 'User not authenticated'));
+        return next(
+            createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
+        );
     }
     }
 
 
     if (!reqUserId) {
     if (!reqUserId) {
-        return next(createHttpError(HttpCode.BAD_REQUEST, 'Invalid user ID'));
+        return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid user ID"));
     }
     }
 
 
     try {
     try {
-
-        const userOrg = await db.select()
+        const userOrg = await db
+            .select()
             .from(userOrgs)
             .from(userOrgs)
-            .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, req.userOrgId!)))
+            .where(
+                and(
+                    eq(userOrgs.userId, reqUserId),
+                    eq(userOrgs.orgId, req.userOrgId!)
+                )
+            )
             .limit(1);
             .limit(1);
 
 
         if (userOrg.length === 0) {
         if (userOrg.length === 0) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have access to this user'));
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "User does not have access to this user"
+                )
+            );
         }
         }
 
 
-        // If we reach here, the user doesn't have access to the site
-        return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have access to this site'));
-
+        return next();
     } catch (error) {
     } catch (error) {
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, 'Error verifying site access'));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "Error checking if user has access to this user"
+            )
+        );
     }
     }
 }
 }

+ 112 - 112
server/routers/external.ts

@@ -19,7 +19,7 @@ import {
     verifyResourceAccess,
     verifyResourceAccess,
     verifyTargetAccess,
     verifyTargetAccess,
     verifyRoleAccess,
     verifyRoleAccess,
-    verifySuperuser,
+    verifySuperUser,
     verifyUserInRole,
     verifyUserInRole,
     verifyUserAccess,
     verifyUserAccess,
 } from "./auth";
 } from "./auth";
@@ -40,7 +40,7 @@ authenticated.put("/org", getUserOrgs, org.createOrg);
 authenticated.get("/orgs", getUserOrgs, org.listOrgs); // TODO we need to check the orgs here
 authenticated.get("/orgs", getUserOrgs, org.listOrgs); // TODO we need to check the orgs here
 authenticated.get("/org/:orgId", verifyOrgAccess, org.getOrg);
 authenticated.get("/org/:orgId", verifyOrgAccess, org.getOrg);
 authenticated.post("/org/:orgId", verifyOrgAccess, org.updateOrg);
 authenticated.post("/org/:orgId", verifyOrgAccess, org.updateOrg);
-authenticated.delete("/org/:orgId", verifyOrgAccess, org.deleteOrg);
+// authenticated.delete("/org/:orgId", verifyOrgAccess, org.deleteOrg);
 
 
 authenticated.put("/org/:orgId/site", verifyOrgAccess, site.createSite);
 authenticated.put("/org/:orgId/site", verifyOrgAccess, site.createSite);
 authenticated.get("/org/:orgId/sites", verifyOrgAccess, site.listSites);
 authenticated.get("/org/:orgId/sites", verifyOrgAccess, site.listSites);
@@ -52,7 +52,7 @@ authenticated.get(
     site.pickSiteDefaults
     site.pickSiteDefaults
 );
 );
 authenticated.get("/site/:siteId", verifySiteAccess, site.getSite);
 authenticated.get("/site/:siteId", verifySiteAccess, site.getSite);
-authenticated.get("/site/:siteId/roles", verifySiteAccess, site.listSiteRoles);
+// authenticated.get("/site/:siteId/roles", verifySiteAccess, site.listSiteRoles);
 authenticated.post("/site/:siteId", verifySiteAccess, site.updateSite);
 authenticated.post("/site/:siteId", verifySiteAccess, site.updateSite);
 authenticated.delete("/site/:siteId", verifySiteAccess, site.deleteSite);
 authenticated.delete("/site/:siteId", verifySiteAccess, site.deleteSite);
 
 
@@ -75,11 +75,11 @@ authenticated.post(
 ); // maybe make this /invite/create instead
 ); // maybe make this /invite/create instead
 authenticated.post("/invite/accept", user.acceptInvite);
 authenticated.post("/invite/accept", user.acceptInvite);
 
 
-authenticated.get(
-    "/resource/:resourceId/roles",
-    verifyResourceAccess,
-    resource.listResourceRoles
-);
+// authenticated.get(
+//     "/resource/:resourceId/roles",
+//     verifyResourceAccess,
+//     resource.listResourceRoles
+// );
 authenticated.get(
 authenticated.get(
     "/resource/:resourceId",
     "/resource/:resourceId",
     verifyResourceAccess,
     verifyResourceAccess,
@@ -121,85 +121,85 @@ authenticated.delete(
 // authenticated.put(
 // authenticated.put(
 //     "/org/:orgId/role",
 //     "/org/:orgId/role",
 //     verifyOrgAccess,
 //     verifyOrgAccess,
-//     verifySuperuser,
+//     verifySuperUser,
 //     role.createRole
 //     role.createRole
 // );
 // );
-authenticated.get("/org/:orgId/roles", verifyOrgAccess, role.listRoles);
-authenticated.get(
-    "/role/:roleId",
-    verifyRoleAccess,
-    verifyUserInRole,
-    role.getRole
-);
+// authenticated.get("/org/:orgId/roles", verifyOrgAccess, role.listRoles);
+// authenticated.get(
+//     "/role/:roleId",
+//     verifyRoleAccess,
+//     verifyUserInRole,
+//     role.getRole
+// );
 // authenticated.post(
 // authenticated.post(
 //     "/role/:roleId",
 //     "/role/:roleId",
 //     verifyRoleAccess,
 //     verifyRoleAccess,
-//     verifySuperuser,
+//     verifySuperUser,
 //     role.updateRole
 //     role.updateRole
 // );
 // );
 // authenticated.delete(
 // authenticated.delete(
 //     "/role/:roleId",
 //     "/role/:roleId",
 //     verifyRoleAccess,
 //     verifyRoleAccess,
-//     verifySuperuser,
+//     verifySuperUser,
 //     role.deleteRole
 //     role.deleteRole
 // );
 // );
 
 
-authenticated.put(
-    "/role/:roleId/site",
-    verifyRoleAccess,
-    verifyUserInRole,
-    role.addRoleSite
-);
-authenticated.delete(
-    "/role/:roleId/site",
-    verifyRoleAccess,
-    verifyUserInRole,
-    role.removeRoleSite
-);
-authenticated.get(
-    "/role/:roleId/sites",
-    verifyRoleAccess,
-    verifyUserInRole,
-    role.listRoleSites
-);
-authenticated.put(
-    "/role/:roleId/resource",
-    verifyRoleAccess,
-    verifyUserInRole,
-    role.addRoleResource
-);
-authenticated.delete(
-    "/role/:roleId/resource",
-    verifyRoleAccess,
-    verifyUserInRole,
-    role.removeRoleResource
-);
-authenticated.get(
-    "/role/:roleId/resources",
-    verifyRoleAccess,
-    verifyUserInRole,
-    role.listRoleResources
-);
-authenticated.put(
-    "/role/:roleId/action",
-    verifyRoleAccess,
-    verifyUserInRole,
-    role.addRoleAction
-);
-authenticated.delete(
-    "/role/:roleId/action",
-    verifyRoleAccess,
-    verifyUserInRole,
-    verifySuperuser,
-    role.removeRoleAction
-);
-authenticated.get(
-    "/role/:roleId/actions",
-    verifyRoleAccess,
-    verifyUserInRole,
-    verifySuperuser,
-    role.listRoleActions
-);
+// authenticated.put(
+//     "/role/:roleId/site",
+//     verifyRoleAccess,
+//     verifyUserInRole,
+//     role.addRoleSite
+// );
+// authenticated.delete(
+//     "/role/:roleId/site",
+//     verifyRoleAccess,
+//     verifyUserInRole,
+//     role.removeRoleSite
+// );
+// authenticated.get(
+//     "/role/:roleId/sites",
+//     verifyRoleAccess,
+//     verifyUserInRole,
+//     role.listRoleSites
+// );
+// authenticated.put(
+//     "/role/:roleId/resource",
+//     verifyRoleAccess,
+//     verifyUserInRole,
+//     role.addRoleResource
+// );
+// authenticated.delete(
+//     "/role/:roleId/resource",
+//     verifyRoleAccess,
+//     verifyUserInRole,
+//     role.removeRoleResource
+// );
+// authenticated.get(
+//     "/role/:roleId/resources",
+//     verifyRoleAccess,
+//     verifyUserInRole,
+//     role.listRoleResources
+// );
+// authenticated.put(
+//     "/role/:roleId/action",
+//     verifyRoleAccess,
+//     verifyUserInRole,
+//     role.addRoleAction
+// );
+// authenticated.delete(
+//     "/role/:roleId/action",
+//     verifyRoleAccess,
+//     verifyUserInRole,
+//     verifySuperUser,
+//     role.removeRoleAction
+// );
+// authenticated.get(
+//     "/role/:roleId/actions",
+//     verifyRoleAccess,
+//     verifyUserInRole,
+//     verifySuperUser,
+//     role.listRoleActions
+// );
 
 
 unauthenticated.get("/user", verifySessionMiddleware, user.getUser);
 unauthenticated.get("/user", verifySessionMiddleware, user.getUser);
 
 
@@ -211,44 +211,44 @@ authenticated.delete(
     user.removeUserOrg
     user.removeUserOrg
 );
 );
 
 
-authenticated.put(
-    "/user/:userId/site",
-    verifySiteAccess,
-    verifyUserAccess,
-    role.addRoleSite
-);
-authenticated.delete(
-    "/user/:userId/site",
-    verifySiteAccess,
-    verifyUserAccess,
-    role.removeRoleSite
-);
-authenticated.put(
-    "/user/:userId/resource",
-    verifyResourceAccess,
-    verifyUserAccess,
-    role.addRoleResource
-);
-authenticated.delete(
-    "/user/:userId/resource",
-    verifyResourceAccess,
-    verifyUserAccess,
-    role.removeRoleResource
-);
-authenticated.put(
-    "/org/:orgId/user/:userId/action",
-    verifyOrgAccess,
-    verifyUserAccess,
-    verifySuperuser,
-    role.addRoleAction
-);
-authenticated.delete(
-    "/org/:orgId/user/:userId/action",
-    verifyOrgAccess,
-    verifyUserAccess,
-    verifySuperuser,
-    role.removeRoleAction
-);
+// authenticated.put(
+//     "/user/:userId/site",
+//     verifySiteAccess,
+//     verifyUserAccess,
+//     role.addRoleSite
+// );
+// authenticated.delete(
+//     "/user/:userId/site",
+//     verifySiteAccess,
+//     verifyUserAccess,
+//     role.removeRoleSite
+// );
+// authenticated.put(
+//     "/user/:userId/resource",
+//     verifyResourceAccess,
+//     verifyUserAccess,
+//     role.addRoleResource
+// );
+// authenticated.delete(
+//     "/user/:userId/resource",
+//     verifyResourceAccess,
+//     verifyUserAccess,
+//     role.removeRoleResource
+// );
+// authenticated.put(
+//     "/org/:orgId/user/:userId/action",
+//     verifyOrgAccess,
+//     verifyUserAccess,
+//     verifySuperUser,
+//     role.addRoleAction
+// );
+// authenticated.delete(
+//     "/org/:orgId/user/:userId/action",
+//     verifyOrgAccess,
+//     verifyUserAccess,
+//     verifySuperUser,
+//     role.removeRoleAction
+// );
 
 
 // Auth routes
 // Auth routes
 export const authRouter = Router();
 export const authRouter = Router();

+ 3 - 3
server/routers/org/createOrg.ts

@@ -8,7 +8,7 @@ import HttpCode from '@server/types/HttpCode';
 import createHttpError from 'http-errors';
 import createHttpError from 'http-errors';
 import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions';
 import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions';
 import logger from '@server/logger';
 import logger from '@server/logger';
-import { createSuperuserRole } from '@server/db/ensureActions';
+import { createSuperUserRole } from '@server/db/ensureActions';
 import config, { APP_PATH } from "@server/config";
 import config, { APP_PATH } from "@server/config";
 import { fromError } from 'zod-validation-error';
 import { fromError } from 'zod-validation-error';
 
 
@@ -75,13 +75,13 @@ export async function createOrg(req: Request, res: Response, next: NextFunction)
             domain
             domain
         }).returning();
         }).returning();
 
 
-        const roleId = await createSuperuserRole(newOrg[0].orgId);
+        const roleId = await createSuperUserRole(newOrg[0].orgId);
 
 
         if (!roleId) {
         if (!roleId) {
             return next(
             return next(
                 createHttpError(
                 createHttpError(
                     HttpCode.INTERNAL_SERVER_ERROR,
                     HttpCode.INTERNAL_SERVER_ERROR,
-                    `Error creating superuser role`
+                    `Error creating Super User role`
                 )
                 )
             );
             );
         }
         }

+ 7 - 7
server/routers/resource/createResource.ts

@@ -87,27 +87,27 @@ export async function createResource(req: Request, res: Response, next: NextFunc
             subdomain,
             subdomain,
         }).returning();
         }).returning();
 
 
-        // find the superuser roleId and also add the resource to the superuser role
-        const superuserRole = await db.select()
+        // find the Super User roleId and also add the resource to the Super User role
+        const superUserRole = await db.select()
             .from(roles)
             .from(roles)
-            .where(and(eq(roles.isSuperuserRole, true), eq(roles.orgId, orgId)))
+            .where(and(eq(roles.isSuperUserRole, true), eq(roles.orgId, orgId)))
             .limit(1);
             .limit(1);
 
 
-        if (superuserRole.length === 0) {
+        if (superUserRole.length === 0) {
             return next(
             return next(
                 createHttpError(
                 createHttpError(
                     HttpCode.NOT_FOUND,
                     HttpCode.NOT_FOUND,
-                    `Superuser role not found`
+                    `Super User role not found`
                 )
                 )
             );
             );
         }
         }
 
 
         await db.insert(roleResources).values({
         await db.insert(roleResources).values({
-            roleId: superuserRole[0].roleId,
+            roleId: superUserRole[0].roleId,
             resourceId: newResource[0].resourceId,
             resourceId: newResource[0].resourceId,
         });
         });
 
 
-        if (req.userOrgRoleId != superuserRole[0].roleId) {
+        if (req.userOrgRoleId != superUserRole[0].roleId) {
             // make sure the user can access the resource
             // make sure the user can access the resource
             await db.insert(userResources).values({
             await db.insert(userResources).values({
                 userId: req.user?.userId!,
                 userId: req.user?.userId!,

+ 1 - 1
server/routers/resource/listResourceRoles.ts

@@ -51,7 +51,7 @@ export async function listResourceRoles(
                 roleId: roles.roleId,
                 roleId: roles.roleId,
                 name: roles.name,
                 name: roles.name,
                 description: roles.description,
                 description: roles.description,
-                isSuperuserRole: roles.isSuperuserRole,
+                isSuperUserRole: roles.isSuperUserRole,
             })
             })
             .from(roleResources)
             .from(roleResources)
             .innerJoin(roles, eq(roleResources.roleId, roles.roleId))
             .innerJoin(roles, eq(roleResources.roleId, roles.roleId))

+ 2 - 2
server/routers/role/deleteRole.ts

@@ -61,11 +61,11 @@ export async function deleteRole(
             );
             );
         }
         }
 
 
-        if (role[0].isSuperuserRole) {
+        if (role[0].isSuperUserRole) {
             return next(
             return next(
                 createHttpError(
                 createHttpError(
                     HttpCode.FORBIDDEN,
                     HttpCode.FORBIDDEN,
-                    `Cannot delete a superuser role`
+                    `Cannot delete a Super User role`
                 )
                 )
             );
             );
         }
         }

+ 1 - 1
server/routers/role/listRoles.ts

@@ -80,7 +80,7 @@ export async function listRoles(
             .select({
             .select({
                 roleId: roles.roleId,
                 roleId: roles.roleId,
                 orgId: roles.orgId,
                 orgId: roles.orgId,
-                isSuperuserRole: roles.isSuperuserRole,
+                isSuperUserRole: roles.isSuperUserRole,
                 name: roles.name,
                 name: roles.name,
                 description: roles.description,
                 description: roles.description,
                 orgName: orgs.name,
                 orgName: orgs.name,

+ 2 - 2
server/routers/role/updateRole.ts

@@ -81,11 +81,11 @@ export async function updateRole(
             );
             );
         }
         }
 
 
-        if (role[0].isSuperuserRole) {
+        if (role[0].isSuperUserRole) {
             return next(
             return next(
                 createHttpError(
                 createHttpError(
                     HttpCode.FORBIDDEN,
                     HttpCode.FORBIDDEN,
-                    `Cannot update a superuser role`
+                    `Cannot update a Super User role`
                 )
                 )
             );
             );
         }
         }

+ 7 - 7
server/routers/site/createSite.ts

@@ -107,25 +107,25 @@ export async function createSite(
                 subnet,
                 subnet,
             })
             })
             .returning();
             .returning();
-        // find the superuser roleId and also add the resource to the superuser role
-        const superuserRole = await db
+        // find the Super User roleId and also add the resource to the Super User role
+        const superUserRole = await db
             .select()
             .select()
             .from(roles)
             .from(roles)
-            .where(and(eq(roles.isSuperuserRole, true), eq(roles.orgId, orgId)))
+            .where(and(eq(roles.isSuperUserRole, true), eq(roles.orgId, orgId)))
             .limit(1);
             .limit(1);
 
 
-        if (superuserRole.length === 0) {
+        if (superUserRole.length === 0) {
             return next(
             return next(
-                createHttpError(HttpCode.NOT_FOUND, `Superuser role not found`)
+                createHttpError(HttpCode.NOT_FOUND, `Super User role not found`)
             );
             );
         }
         }
 
 
         await db.insert(roleSites).values({
         await db.insert(roleSites).values({
-            roleId: superuserRole[0].roleId,
+            roleId: superUserRole[0].roleId,
             siteId: newSite.siteId,
             siteId: newSite.siteId,
         });
         });
 
 
-        if (req.userOrgRoleId != superuserRole[0].roleId) {
+        if (req.userOrgRoleId != superUserRole[0].roleId) {
             // make sure the user can access the site
             // make sure the user can access the site
             db.insert(userSites).values({
             db.insert(userSites).values({
                 userId: req.user?.userId!,
                 userId: req.user?.userId!,

+ 1 - 1
server/routers/site/listSiteRoles.ts

@@ -51,7 +51,7 @@ export async function listSiteRoles(
                 roleId: roles.roleId,
                 roleId: roles.roleId,
                 name: roles.name,
                 name: roles.name,
                 description: roles.description,
                 description: roles.description,
-                isSuperuserRole: roles.isSuperuserRole,
+                isSuperUserRole: roles.isSuperUserRole,
             })
             })
             .from(roleSites)
             .from(roleSites)
             .innerJoin(roles, eq(roleSites.roleId, roles.roleId))
             .innerJoin(roles, eq(roleSites.roleId, roles.roleId))

+ 53 - 1
server/routers/user/inviteUser.ts

@@ -1,7 +1,7 @@
 import { Request, Response, NextFunction } from "express";
 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 { userInvites, userOrgs, users } from "@server/db/schema";
+import { orgs, userInvites, userOrgs, users } from "@server/db/schema";
 import { and, eq } from "drizzle-orm";
 import { and, eq } from "drizzle-orm";
 import response from "@server/utils/response";
 import response from "@server/utils/response";
 import HttpCode from "@server/types/HttpCode";
 import HttpCode from "@server/types/HttpCode";
@@ -13,6 +13,8 @@ import { createDate, TimeSpan } from "oslo";
 import config from "@server/config";
 import config from "@server/config";
 import { hashPassword } from "@server/auth/password";
 import { hashPassword } from "@server/auth/password";
 import { fromError } from "zod-validation-error";
 import { fromError } from "zod-validation-error";
+import { sendEmail } from "@server/emails";
+import SendInviteLink from "@server/emails/templates/SendInviteLink";
 
 
 const inviteUserParamsSchema = z.object({
 const inviteUserParamsSchema = z.object({
     orgId: z.string(),
     orgId: z.string(),
@@ -31,6 +33,8 @@ export type InviteUserResponse = {
     expiresAt: number;
     expiresAt: number;
 };
 };
 
 
+const inviteTracker: Record<string, { timestamps: number[] }> = {};
+
 export async function inviteUser(
 export async function inviteUser(
     req: Request,
     req: Request,
     res: Response,
     res: Response,
@@ -73,6 +77,39 @@ export async function inviteUser(
             );
             );
         }
         }
 
 
+        const currentTime = Date.now();
+        const oneHourAgo = currentTime - 3600000;
+
+        if (!inviteTracker[email]) {
+            inviteTracker[email] = { timestamps: [] };
+        }
+
+        inviteTracker[email].timestamps = inviteTracker[
+            email
+        ].timestamps.filter((timestamp) => timestamp > oneHourAgo);
+
+        if (inviteTracker[email].timestamps.length >= 3) {
+            return next(
+                createHttpError(
+                    HttpCode.TOO_MANY_REQUESTS,
+                    "User has already been invited 3 times in the last hour"
+                )
+            );
+        }
+
+        inviteTracker[email].timestamps.push(currentTime);
+
+        const org = await db
+            .select()
+            .from(orgs)
+            .where(eq(orgs.orgId, orgId))
+            .limit(1);
+        if (!org.length) {
+            return next(
+                createHttpError(HttpCode.NOT_FOUND, "Organization not found")
+            );
+        }
+
         const existingUser = await db
         const existingUser = await db
             .select()
             .select()
             .from(users)
             .from(users)
@@ -116,6 +153,21 @@ export async function inviteUser(
 
 
         const inviteLink = `${config.app.base_url}/invite?token=${inviteId}-${token}`;
         const inviteLink = `${config.app.base_url}/invite?token=${inviteId}-${token}`;
 
 
+        await sendEmail(
+            SendInviteLink({
+                email,
+                inviteLink,
+                expiresInDays: (validHours / 24).toString(),
+                orgName: orgId,
+                inviterName: req.user?.email,
+            }),
+            {
+                to: email,
+                from: config.email?.no_reply,
+                subject: "You're invited to join a Fossorial organization",
+            }
+        );
+
         return response<InviteUserResponse>(res, {
         return response<InviteUserResponse>(res, {
             data: {
             data: {
                 inviteLink,
                 inviteLink,

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

@@ -11,7 +11,7 @@ import logger from "@server/logger";
 import { fromError } from "zod-validation-error";
 import { fromError } from "zod-validation-error";
 
 
 const removeUserSchema = z.object({
 const removeUserSchema = z.object({
-    userId: z.string().uuid(),
+    userId: z.string(),
     orgId: z.string(),
     orgId: z.string(),
 });
 });
 
 
@@ -33,7 +33,6 @@ export async function removeUserOrg(
 
 
         const { userId, orgId } = parsedParams.data;
         const { userId, orgId } = parsedParams.data;
 
 
-        // Check if the user has permission to list sites
         const hasPermission = await checkUserActionPermission(
         const hasPermission = await checkUserActionPermission(
             ActionsEnum.removeUser,
             ActionsEnum.removeUser,
             req
             req
@@ -56,7 +55,7 @@ export async function removeUserOrg(
             data: null,
             data: null,
             success: true,
             success: true,
             error: false,
             error: false,
-            message: "User deleted successfully",
+            message: "User remove from org successfully",
             status: HttpCode.OK,
             status: HttpCode.OK,
         });
         });
     } catch (error) {
     } catch (error) {

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

@@ -79,7 +79,7 @@ export const columns: ColumnDef<SiteRow>[] = [
                     .then(() => {
                     .then(() => {
                         router.refresh();
                         router.refresh();
                     });
                     });
-            }
+            };
 
 
             return (
             return (
                 <DropdownMenu>
                 <DropdownMenu>
@@ -98,7 +98,12 @@ export const columns: ColumnDef<SiteRow>[] = [
                             </Link>
                             </Link>
                         </DropdownMenuItem>
                         </DropdownMenuItem>
                         <DropdownMenuItem>
                         <DropdownMenuItem>
-                            <button onClick={() => deleteSite(siteRow.id)} className="text-red-600 hover:text-red-800 hover:underline cursor-pointer">Delete</button>
+                            <button
+                                onClick={() => deleteSite(siteRow.id)}
+                                className="text-red-600 hover:text-red-800"
+                            >
+                                Delete
+                            </button>
                         </DropdownMenuItem>
                         </DropdownMenuItem>
                     </DropdownMenuContent>
                     </DropdownMenuContent>
                 </DropdownMenu>
                 </DropdownMenu>

+ 8 - 2
src/app/[orgId]/settings/users/components/InviteUserForm.tsx

@@ -122,7 +122,13 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
 
 
     return (
     return (
         <>
         <>
-            <Credenza open={open} onOpenChange={setOpen}>
+            <Credenza open={open} onOpenChange={(val) => {
+                setOpen(val);
+                setInviteLink(null);
+                setLoading(false);
+                setExpiresInDays(1);
+                form.reset();
+            }}>
                 <CredenzaContent>
                 <CredenzaContent>
                     <CredenzaHeader>
                     <CredenzaHeader>
                         <CredenzaTitle>Invite User</CredenzaTitle>
                         <CredenzaTitle>Invite User</CredenzaTitle>
@@ -257,7 +263,7 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
                             type="submit"
                             type="submit"
                             form="invite-user-form"
                             form="invite-user-form"
                             loading={loading}
                             loading={loading}
-                            disabled={inviteLink !== null}
+                            disabled={inviteLink !== null || loading}
                         >
                         >
                             Create Invitation
                             Create Invitation
                         </Button>
                         </Button>

+ 157 - 39
src/app/[orgId]/settings/users/components/UsersTable.tsx

@@ -12,60 +12,178 @@ import { ArrowUpDown, MoreHorizontal } from "lucide-react";
 import { UsersDataTable } from "./UsersDataTable";
 import { UsersDataTable } from "./UsersDataTable";
 import { useState } from "react";
 import { useState } from "react";
 import InviteUserForm from "./InviteUserForm";
 import InviteUserForm from "./InviteUserForm";
+import { Badge } from "@app/components/ui/badge";
+import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
+import { useUserContext } from "@app/hooks/useUserContext";
+import api from "@app/api";
+import { useOrgContext } from "@app/hooks/useOrgContext";
+import { useToast } from "@app/hooks/useToast";
 
 
 export type UserRow = {
 export type UserRow = {
     id: string;
     id: string;
     email: string;
     email: string;
+    status: string;
+    role: string;
 };
 };
 
 
-export const columns: ColumnDef<UserRow>[] = [
-    {
-        accessorKey: "email",
-        header: ({ column }) => {
-            return (
-                <Button
-                    variant="ghost"
-                    onClick={() =>
-                        column.toggleSorting(column.getIsSorted() === "asc")
-                    }
-                >
-                    Email
-                    <ArrowUpDown className="ml-2 h-4 w-4" />
-                </Button>
-            );
-        },
-    },
-    {
-        id: "actions",
-        cell: ({ row }) => {
-            const userRow = row.original;
-
-            return (
-                <DropdownMenu>
-                    <DropdownMenuTrigger asChild>
-                        <Button variant="ghost" className="h-8 w-8 p-0">
-                            <span className="sr-only">Open menu</span>
-                            <MoreHorizontal className="h-4 w-4" />
-                        </Button>
-                    </DropdownMenuTrigger>
-                    <DropdownMenuContent align="end">
-                        <DropdownMenuItem>Edit access</DropdownMenuItem>
-                    </DropdownMenuContent>
-                </DropdownMenu>
-            );
-        },
-    },
-];
-
 type UsersTableProps = {
 type UsersTableProps = {
     users: UserRow[];
     users: UserRow[];
 };
 };
 
 
 export default function UsersTable({ users }: UsersTableProps) {
 export default function UsersTable({ users }: UsersTableProps) {
     const [isInviteModalOpen, setIsInviteModalOpen] = useState(false);
     const [isInviteModalOpen, setIsInviteModalOpen] = useState(false);
+    const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+    const [userToRemove, setUserToRemove] = useState<UserRow | null>(null);
+
+    const user = useUserContext();
+    const { org } = useOrgContext();
+    const { toast } = useToast();
+
+    const columns: ColumnDef<UserRow>[] = [
+        {
+            accessorKey: "email",
+            header: ({ column }) => {
+                return (
+                    <Button
+                        variant="ghost"
+                        onClick={() =>
+                            column.toggleSorting(column.getIsSorted() === "asc")
+                        }
+                    >
+                        Email
+                        <ArrowUpDown className="ml-2 h-4 w-4" />
+                    </Button>
+                );
+            },
+        },
+        {
+            accessorKey: "status",
+            header: ({ column }) => {
+                return (
+                    <Button
+                        variant="ghost"
+                        onClick={() =>
+                            column.toggleSorting(column.getIsSorted() === "asc")
+                        }
+                    >
+                        Status
+                        <ArrowUpDown className="ml-2 h-4 w-4" />
+                    </Button>
+                );
+            },
+        },
+        {
+            accessorKey: "role",
+            header: ({ column }) => {
+                return (
+                    <Button
+                        variant="ghost"
+                        onClick={() =>
+                            column.toggleSorting(column.getIsSorted() === "asc")
+                        }
+                    >
+                        Role
+                        <ArrowUpDown className="ml-2 h-4 w-4" />
+                    </Button>
+                );
+            },
+        },
+        {
+            id: "actions",
+            cell: ({ row }) => {
+                const userRow = row.original;
+
+                return (
+                    <>
+                        <DropdownMenu>
+                            <DropdownMenuTrigger asChild>
+                                <Button variant="ghost" className="h-8 w-8 p-0">
+                                    <span className="sr-only">Open menu</span>
+                                    <MoreHorizontal className="h-4 w-4" />
+                                </Button>
+                            </DropdownMenuTrigger>
+                            <DropdownMenuContent align="end">
+                                <DropdownMenuItem>Manage user</DropdownMenuItem>
+                                {userRow.email !== user?.email && (
+                                    <DropdownMenuItem>
+                                        <button
+                                            className="text-red-600 hover:text-red-800"
+                                            onClick={() => {
+                                                setIsDeleteModalOpen(true);
+                                                setUserToRemove(userRow);
+                                            }}
+                                        >
+                                            Remove User
+                                        </button>
+                                    </DropdownMenuItem>
+                                )}
+                            </DropdownMenuContent>
+                        </DropdownMenu>
+                    </>
+                );
+            },
+        },
+    ];
+
+    async function removeUser() {
+        if (userToRemove) {
+            const res = await api
+                .delete(`/org/${org!.org.orgId}/user/${userToRemove.id}`)
+                .catch((e) => {
+                    toast({
+                        variant: "destructive",
+                        title: "Failed to remove user",
+                        description:
+                            e.message ??
+                            "An error occurred while removing the user.",
+                    });
+                });
+
+            if (res && res.status === 200) {
+                toast({
+                    variant: "default",
+                    title: "User removed",
+                    description: `The user ${userToRemove.email} has been removed from the organization.`,
+                });
+            }
+        }
+        setIsDeleteModalOpen(false);
+    }
 
 
     return (
     return (
         <>
         <>
+            <ConfirmDeleteDialog
+                open={isDeleteModalOpen}
+                setOpen={(val) => {
+                    setIsDeleteModalOpen(val);
+                    setUserToRemove(null);
+                }}
+                dialog={
+                    <div>
+                        <p className="mb-2">
+                            Are you sure you want to remove{" "}
+                            <b>{userToRemove?.email}</b> from the organization?
+                        </p>
+
+                        <p className="mb-2">
+                            Once removed, this user will no longer have access
+                            to the organization. You can always re-invite them
+                            later, but they will need to accept the invitation
+                            again.
+                        </p>
+
+                        <p>
+                            To confirm, please type the email address of the
+                            user below.
+                        </p>
+                    </div>
+                }
+                buttonText="Confirm remove user"
+                onConfirm={removeUser}
+                string={userToRemove?.email ?? ""}
+                title="Remove user from organization"
+            />
+
             <InviteUserForm
             <InviteUserForm
                 open={isInviteModalOpen}
                 open={isInviteModalOpen}
                 setOpen={setIsInviteModalOpen}
                 setOpen={setIsInviteModalOpen}

+ 12 - 3
src/app/[orgId]/settings/users/page.tsx

@@ -6,6 +6,8 @@ import UsersTable, { UserRow } from "./components/UsersTable";
 import { GetOrgResponse } from "@server/routers/org";
 import { GetOrgResponse } from "@server/routers/org";
 import { cache } from "react";
 import { cache } from "react";
 import OrgProvider from "@app/providers/OrgProvider";
 import OrgProvider from "@app/providers/OrgProvider";
+import UserProvider from "@app/providers/UserProvider";
+import { verifySession } from "@app/lib/auth/verifySession";
 
 
 type UsersPageProps = {
 type UsersPageProps = {
     params: Promise<{ orgId: string }>;
     params: Promise<{ orgId: string }>;
@@ -14,6 +16,9 @@ type UsersPageProps = {
 export default async function UsersPage(props: UsersPageProps) {
 export default async function UsersPage(props: UsersPageProps) {
     const params = await props.params;
     const params = await props.params;
 
 
+    const getUser = cache(verifySession);
+    const user = await getUser();
+
     let users: ListUsersResponse["users"] = [];
     let users: ListUsersResponse["users"] = [];
     const res = await internal
     const res = await internal
         .get<AxiosResponse<ListUsersResponse>>(
         .get<AxiosResponse<ListUsersResponse>>(
@@ -49,6 +54,8 @@ export default async function UsersPage(props: UsersPageProps) {
         return {
         return {
             id: user.id,
             id: user.id,
             email: user.email,
             email: user.email,
+            status: "Confirmed",
+            role: user.roleName || "",
         };
         };
     });
     });
 
 
@@ -64,9 +71,11 @@ export default async function UsersPage(props: UsersPageProps) {
                 </p>
                 </p>
             </div>
             </div>
 
 
-            <OrgProvider org={org}>
-                <UsersTable users={userRows} />
-            </OrgProvider>
+            <UserProvider user={user!}>
+                <OrgProvider org={org}>
+                    <UsersTable users={userRows} />
+                </OrgProvider>
+            </UserProvider>
         </>
         </>
     );
     );
 }
 }

+ 3 - 3
src/app/page.tsx

@@ -1,7 +1,7 @@
 import { internal } from "@app/api";
 import { internal } from "@app/api";
 import { authCookieHeader } from "@app/api/cookies";
 import { authCookieHeader } from "@app/api/cookies";
 import { verifySession } from "@app/lib/auth/verifySession";
 import { verifySession } from "@app/lib/auth/verifySession";
-import { LandingProvider } from "@app/providers/LandingProvider";
+import UserProvider from "@app/providers/UserProvider";
 import { ListOrgsResponse } from "@server/routers/org";
 import { ListOrgsResponse } from "@server/routers/org";
 import { AxiosResponse } from "axios";
 import { AxiosResponse } from "axios";
 import { ArrowUpRight } from "lucide-react";
 import { ArrowUpRight } from "lucide-react";
@@ -42,9 +42,9 @@ export default async function Page(props: {
 
 
     return (
     return (
         <>
         <>
-            <LandingProvider user={user}>
+            <UserProvider user={user}>
                 <p>Logged in as {user.email}</p>
                 <p>Logged in as {user.email}</p>
-            </LandingProvider>
+            </UserProvider>
 
 
             <div className="mt-4">
             <div className="mt-4">
                 {orgs.map((org) => (
                 {orgs.map((org) => (

+ 144 - 0
src/components/ConfirmDeleteDialog.tsx

@@ -0,0 +1,144 @@
+"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 {
+    InviteUserBody,
+    InviteUserResponse,
+    ListUsersResponse,
+} from "@server/routers/user";
+import { AxiosResponse } from "axios";
+import React, { useState } from "react";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+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";
+import { Description } from "@radix-ui/react-toast";
+
+type InviteUserFormProps = {
+    open: boolean;
+    setOpen: (open: boolean) => void;
+    string: string;
+    title: string;
+    dialog: React.ReactNode;
+    buttonText: string;
+    onConfirm: () => Promise<void>;
+};
+
+export default function InviteUserForm({
+    open,
+    setOpen,
+    string,
+    title,
+    onConfirm,
+    buttonText,
+    dialog,
+}: InviteUserFormProps) {
+    const [loading, setLoading] = useState(false);
+
+    const formSchema = z.object({
+        string: z.string().refine((val) => val === string, {
+            message: "Invalid confirmation",
+        }),
+    });
+
+    const form = useForm<z.infer<typeof formSchema>>({
+        resolver: zodResolver(formSchema),
+        defaultValues: {
+            string: "",
+        },
+    });
+
+    function reset() {
+        form.reset();
+        setLoading(false);
+    }
+
+    async function onSubmit(values: z.infer<typeof formSchema>) {
+        setLoading(true);
+        await onConfirm();
+        reset();
+    }
+
+    return (
+        <>
+            <Credenza
+                open={open}
+                onOpenChange={(val) => {
+                    setOpen(val);
+                    reset();
+                }}
+            >
+                <CredenzaContent>
+                    <CredenzaHeader>
+                        <CredenzaTitle>{title}</CredenzaTitle>
+                    </CredenzaHeader>
+                    <CredenzaBody>
+                        <div className="mb-4">{dialog}</div>
+                        <Form {...form}>
+                            <form
+                                onSubmit={form.handleSubmit(onSubmit)}
+                                className="space-y-4"
+                                id="confirm-delete-form"
+                            >
+                                <FormField
+                                    control={form.control}
+                                    name="string"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormControl>
+                                                <Input {...field} />
+                                            </FormControl>
+                                            <FormMessage />
+                                        </FormItem>
+                                    )}
+                                />
+                            </form>
+                        </Form>
+                    </CredenzaBody>
+                    <CredenzaFooter>
+                        <Button
+                            type="submit"
+                            form="confirm-delete-form"
+                            loading={loading}
+                            disabled={loading}
+                        >
+                            {buttonText}
+                        </Button>
+                        <CredenzaClose asChild>
+                            <Button variant="outline">Close</Button>
+                        </CredenzaClose>
+                    </CredenzaFooter>
+                </CredenzaContent>
+            </Credenza>
+        </>
+    );
+}

+ 30 - 27
src/components/ui/badge.tsx

@@ -1,36 +1,39 @@
-import * as React from "react"
-import { cva, type VariantProps } from "class-variance-authority"
+import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
 
 
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
 
 
 const badgeVariants = cva(
 const badgeVariants = cva(
-  "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
-  {
-    variants: {
-      variant: {
-        default:
-          "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
-        secondary:
-          "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
-        destructive:
-          "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
-        outline: "text-foreground",
-      },
-    },
-    defaultVariants: {
-      variant: "default",
-    },
-  }
-)
+    "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+    {
+        variants: {
+            variant: {
+                default:
+                    "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+                secondary:
+                    "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+                destructive:
+                    "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
+                outline: "text-foreground",
+                green: "border-transparent bg-green-300",
+                yellow: "border-transparent bg-yellow-300",
+                red: "border-transparent bg-red-300",
+            },
+        },
+        defaultVariants: {
+            variant: "default",
+        },
+    }
+);
 
 
 export interface BadgeProps
 export interface BadgeProps
-  extends React.HTMLAttributes<HTMLDivElement>,
-    VariantProps<typeof badgeVariants> {}
+    extends React.HTMLAttributes<HTMLDivElement>,
+        VariantProps<typeof badgeVariants> {}
 
 
 function Badge({ className, variant, ...props }: BadgeProps) {
 function Badge({ className, variant, ...props }: BadgeProps) {
-  return (
-    <div className={cn(badgeVariants({ variant }), className)} {...props} />
-  )
+    return (
+        <div className={cn(badgeVariants({ variant }), className)} {...props} />
+    );
 }
 }
 
 
-export { Badge, badgeVariants }
+export { Badge, badgeVariants };

+ 3 - 3
src/providers/LandingProvider.tsx → src/providers/UserProvider.tsx

@@ -4,13 +4,13 @@ import { UserContext } from "@app/contexts/userContext";
 import { GetUserResponse } from "@server/routers/user";
 import { GetUserResponse } from "@server/routers/user";
 import { ReactNode } from "react";
 import { ReactNode } from "react";
 
 
-type LandingProviderProps = {
+type UserProviderProps = {
     user: GetUserResponse;
     user: GetUserResponse;
     children: ReactNode;
     children: ReactNode;
 };
 };
 
 
-export function LandingProvider({ user, children }: LandingProviderProps) {
+export function UserProvider({ user, children }: UserProviderProps) {
     return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
     return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
 }
 }
 
 
-export default LandingProvider;
+export default UserProvider;