From fadfaf1f0b4a2da19b147710feef356d2181d948 Mon Sep 17 00:00:00 2001
From: Milo Schwartz
Date: Sun, 3 Nov 2024 17:28:12 -0500
Subject: [PATCH] ability to remove user from org
---
scripts/hydrate.ts | 6 +-
server/db/ensureActions.ts | 12 +-
server/db/schema.ts | 2 +-
server/emails/templates/SendInviteLink.tsx | 75 ++++++
server/routers/auth/index.ts | 2 +-
.../routers/auth/sendEmailVerificationCode.ts | 2 +-
server/routers/auth/verifyOrgAccess.ts | 42 +++-
server/routers/auth/verifySuperuser.ts | 8 +-
server/routers/auth/verifyUserAccess.ts | 59 +++--
server/routers/external.ts | 224 +++++++++---------
server/routers/org/createOrg.ts | 6 +-
server/routers/resource/createResource.ts | 14 +-
server/routers/resource/listResourceRoles.ts | 2 +-
server/routers/role/deleteRole.ts | 4 +-
server/routers/role/listRoles.ts | 2 +-
server/routers/role/updateRole.ts | 4 +-
server/routers/site/createSite.ts | 14 +-
server/routers/site/listSiteRoles.ts | 2 +-
server/routers/user/inviteUser.ts | 54 ++++-
server/routers/user/removeUserOrg.ts | 5 +-
.../settings/sites/components/SitesTable.tsx | 9 +-
.../users/components/InviteUserForm.tsx | 10 +-
.../settings/users/components/UsersTable.tsx | 196 ++++++++++++---
src/app/[orgId]/settings/users/page.tsx | 15 +-
src/app/page.tsx | 6 +-
src/components/ConfirmDeleteDialog.tsx | 144 +++++++++++
src/components/ui/badge.tsx | 57 ++---
.../{LandingProvider.tsx => UserProvider.tsx} | 6 +-
28 files changed, 718 insertions(+), 264 deletions(-)
create mode 100644 server/emails/templates/SendInviteLink.tsx
create mode 100644 src/components/ConfirmDeleteDialog.tsx
rename src/providers/{LandingProvider.tsx => UserProvider.tsx} (68%)
diff --git a/scripts/hydrate.ts b/scripts/hydrate.ts
index d1bdc8dd..456b52f1 100644
--- a/scripts/hydrate.ts
+++ b/scripts/hydrate.ts
@@ -7,7 +7,7 @@
// targets,
// } from "@server/db/schema";
// import db from "@server/db";
-// import { createSuperuserRole } from "@server/db/ensureActions";
+// import { createSuperUserRole } from "@server/db/ensureActions";
async function insertDummyData() {
// // Insert dummy orgs
@@ -21,7 +21,7 @@ async function insertDummyData() {
// .returning()
// .get();
- // await createSuperuserRole(org1.orgId!);
+ // await createSuperUserRole(org1.orgId!);
// const org2 = db
// .insert(orgs)
@@ -33,7 +33,7 @@ async function insertDummyData() {
// .returning()
// .get();
- // await createSuperuserRole(org2.orgId!);
+ // await createSuperUserRole(org2.orgId!);
// // Insert dummy exit nodes
// const exitNode1 = db
diff --git a/server/db/ensureActions.ts b/server/db/ensureActions.ts
index 81ea5c68..7d96ee55 100644
--- a/server/db/ensureActions.ts
+++ b/server/db/ensureActions.ts
@@ -15,7 +15,7 @@ export async function ensureActions() {
const defaultRoles = await db
.select()
.from(roles)
- .where(eq(roles.isSuperuserRole, true))
+ .where(eq(roles.isSuperUserRole, true))
.execute();
// 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
const [insertedRole] = await db
.insert(roles)
.values({
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 })
.execute();
@@ -56,7 +56,7 @@ export async function createSuperuserRole(orgId: string) {
const actionIds = await db.select().from(actions).execute();
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;
}
diff --git a/server/db/schema.ts b/server/db/schema.ts
index 4a24e488..493fa955 100644
--- a/server/db/schema.ts
+++ b/server/db/schema.ts
@@ -131,7 +131,7 @@ export const roles = sqliteTable("roles", {
orgId: text("orgId").references(() => orgs.orgId, {
onDelete: "cascade",
}),
- isSuperuserRole: integer("isSuperuserRole", { mode: "boolean" }),
+ isSuperUserRole: integer("isSuperUserRole", { mode: "boolean" }),
name: text("name").notNull(),
description: text("description"),
});
diff --git a/server/emails/templates/SendInviteLink.tsx b/server/emails/templates/SendInviteLink.tsx
new file mode 100644
index 00000000..64f19bba
--- /dev/null
+++ b/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 (
+
+
+ {previewText}
+
+
+
+
+ You're invite to join a Fossorial organization
+
+
+ Hi {email || "there"},
+
+
+ You’ve been invited to join the organization{" "}
+ {orgName}
+ {inviterName ? ` by ${inviterName}.` : ""}. Please
+ access the link below to accept the invite.
+
+
+ This invite will expire in{" "}
+ {expiresInDays} days.
+
+
+
+
+
+ Best regards,
+
+ Fossorial
+
+
+
+
+
+ );
+};
+
+export default SendInviteLink;
diff --git a/server/routers/auth/index.ts b/server/routers/auth/index.ts
index 7d7b36eb..c7a95cfb 100644
--- a/server/routers/auth/index.ts
+++ b/server/routers/auth/index.ts
@@ -11,7 +11,7 @@ export * from "./verifyResourceAccess";
export * from "./verifyTargetAccess";
export * from "./verifyRoleAccess";
export * from "./verifyUserAccess";
-export * from "./verifySuperuser";
+export * from "./verifySuperUser";
export * from "./verifyEmail";
export * from "./requestEmailVerificationCode";
export * from "./changePassword";
diff --git a/server/routers/auth/sendEmailVerificationCode.ts b/server/routers/auth/sendEmailVerificationCode.ts
index 6a9ecd1c..a8e2f087 100644
--- a/server/routers/auth/sendEmailVerificationCode.ts
+++ b/server/routers/auth/sendEmailVerificationCode.ts
@@ -4,7 +4,7 @@ import db from "@server/db";
import { users, emailVerificationCodes } from "@server/db/schema";
import { eq } from "drizzle-orm";
import { sendEmail } from "@server/emails";
-import VerifyEmail from "@server/emails/templates/verifyEmailCode";
+import VerifyEmail from "@server/emails/templates/VerifyEmailCode";
import config from "@server/config";
export async function sendEmailVerificationCode(
diff --git a/server/routers/auth/verifyOrgAccess.ts b/server/routers/auth/verifyOrgAccess.ts
index e67dc623..1d04e4ca 100644
--- a/server/routers/auth/verifyOrgAccess.ts
+++ b/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 orgId = req.params.orgId;
if (!userId) {
- return next(createHttpError(HttpCode.UNAUTHORIZED, 'User not authenticated'));
+ return next(
+ createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
+ );
}
if (!orgId) {
- return next(createHttpError(HttpCode.BAD_REQUEST, 'Invalid organization ID'));
+ return next(
+ createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
+ );
}
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)))
.then((result) => {
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 {
// User has access, attach the user's role to the request for potential future use
req.userOrgRoleId = result[0].roleId;
@@ -32,6 +45,11 @@ export function verifyOrgAccess(req: Request, res: Response, next: NextFunction)
}
})
.catch((error) => {
- next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, 'Error verifying organization access'));
+ next(
+ createHttpError(
+ HttpCode.INTERNAL_SERVER_ERROR,
+ "Error verifying organization access"
+ )
+ );
});
}
diff --git a/server/routers/auth/verifySuperuser.ts b/server/routers/auth/verifySuperuser.ts
index 46b26c1d..5455d4a4 100644
--- a/server/routers/auth/verifySuperuser.ts
+++ b/server/routers/auth/verifySuperuser.ts
@@ -6,7 +6,7 @@ import createHttpError from 'http-errors';
import HttpCode from '@server/types/HttpCode';
import logger from '@server/logger';
-export async function verifySuperuser(req: Request, res: Response, next: NextFunction) {
+export async function verifySuperUser(req: Request, res: Response, next: NextFunction) {
const userId = req.user?.userId; // Assuming you have user information in the request
const orgId = req.userOrgId;
@@ -30,14 +30,14 @@ export async function verifySuperuser(req: Request, res: Response, next: NextFun
}
// 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()
.from(roles)
.where(eq(roles.roleId, userOrgRole[0].roleId))
.limit(1);
- if (userRole.length === 0 || !userRole[0].isSuperuserRole) {
- return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have superuser access'));
+ if (userRole.length === 0 || !userRole[0].isSuperUserRole) {
+ return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have Super User access'));
}
return next();
diff --git a/server/routers/auth/verifyUserAccess.ts b/server/routers/auth/verifyUserAccess.ts
index 53606c48..c9d40793 100644
--- a/server/routers/auth/verifyUserAccess.ts
+++ b/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';
+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) {
+export async function verifyUserAccess(
+ req: Request,
+ res: Response,
+ next: NextFunction
+) {
const userId = req.user!.userId; // Assuming you have user information in the request
const reqUserId = req.params.userId || req.body.userId || req.query.userId;
if (!userId) {
- return next(createHttpError(HttpCode.UNAUTHORIZED, 'User not authenticated'));
+ return next(
+ createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
+ );
}
if (!reqUserId) {
- return next(createHttpError(HttpCode.BAD_REQUEST, 'Invalid user ID'));
+ return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid user ID"));
}
try {
-
- const userOrg = await db.select()
+ const userOrg = await db
+ .select()
.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);
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) {
- 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"
+ )
+ );
}
}
diff --git a/server/routers/external.ts b/server/routers/external.ts
index d57327f9..0aa38896 100644
--- a/server/routers/external.ts
+++ b/server/routers/external.ts
@@ -19,7 +19,7 @@ import {
verifyResourceAccess,
verifyTargetAccess,
verifyRoleAccess,
- verifySuperuser,
+ verifySuperUser,
verifyUserInRole,
verifyUserAccess,
} 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("/org/:orgId", verifyOrgAccess, org.getOrg);
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.get("/org/:orgId/sites", verifyOrgAccess, site.listSites);
@@ -52,7 +52,7 @@ authenticated.get(
site.pickSiteDefaults
);
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.delete("/site/:siteId", verifySiteAccess, site.deleteSite);
@@ -75,11 +75,11 @@ authenticated.post(
); // maybe make this /invite/create instead
authenticated.post("/invite/accept", user.acceptInvite);
-authenticated.get(
- "/resource/:resourceId/roles",
- verifyResourceAccess,
- resource.listResourceRoles
-);
+// authenticated.get(
+// "/resource/:resourceId/roles",
+// verifyResourceAccess,
+// resource.listResourceRoles
+// );
authenticated.get(
"/resource/:resourceId",
verifyResourceAccess,
@@ -121,85 +121,85 @@ authenticated.delete(
// authenticated.put(
// "/org/:orgId/role",
// verifyOrgAccess,
-// verifySuperuser,
+// verifySuperUser,
// 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(
// "/role/:roleId",
// verifyRoleAccess,
-// verifySuperuser,
+// verifySuperUser,
// role.updateRole
// );
// authenticated.delete(
// "/role/:roleId",
// verifyRoleAccess,
-// verifySuperuser,
+// verifySuperUser,
// role.deleteRole
// );
-authenticated.put(
- "/role/:roleId/site",
- verifyRoleAccess,
- verifyUserInRole,
- role.addRoleSite
-);
-authenticated.delete(
- "/role/:roleId/site",
- verifyRoleAccess,
- verifyUserInRole,
- role.removeRoleSite
-);
-authenticated.get(
- "/role/:roleId/sites",
- verifyRoleAccess,
- verifyUserInRole,
- role.listRoleSites
-);
-authenticated.put(
- "/role/:roleId/resource",
- verifyRoleAccess,
- verifyUserInRole,
- role.addRoleResource
-);
-authenticated.delete(
- "/role/:roleId/resource",
- verifyRoleAccess,
- verifyUserInRole,
- role.removeRoleResource
-);
-authenticated.get(
- "/role/:roleId/resources",
- verifyRoleAccess,
- verifyUserInRole,
- role.listRoleResources
-);
-authenticated.put(
- "/role/:roleId/action",
- verifyRoleAccess,
- verifyUserInRole,
- role.addRoleAction
-);
-authenticated.delete(
- "/role/:roleId/action",
- verifyRoleAccess,
- verifyUserInRole,
- verifySuperuser,
- role.removeRoleAction
-);
-authenticated.get(
- "/role/:roleId/actions",
- verifyRoleAccess,
- verifyUserInRole,
- verifySuperuser,
- role.listRoleActions
-);
+// authenticated.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);
@@ -211,44 +211,44 @@ authenticated.delete(
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
export const authRouter = Router();
diff --git a/server/routers/org/createOrg.ts b/server/routers/org/createOrg.ts
index 9f4854dc..23a79620 100644
--- a/server/routers/org/createOrg.ts
+++ b/server/routers/org/createOrg.ts
@@ -8,7 +8,7 @@ import HttpCode from '@server/types/HttpCode';
import createHttpError from 'http-errors';
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions';
import logger from '@server/logger';
-import { createSuperuserRole } from '@server/db/ensureActions';
+import { createSuperUserRole } from '@server/db/ensureActions';
import config, { APP_PATH } from "@server/config";
import { fromError } from 'zod-validation-error';
@@ -75,13 +75,13 @@ export async function createOrg(req: Request, res: Response, next: NextFunction)
domain
}).returning();
- const roleId = await createSuperuserRole(newOrg[0].orgId);
+ const roleId = await createSuperUserRole(newOrg[0].orgId);
if (!roleId) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
- `Error creating superuser role`
+ `Error creating Super User role`
)
);
}
diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts
index 77f28820..c58d7343 100644
--- a/server/routers/resource/createResource.ts
+++ b/server/routers/resource/createResource.ts
@@ -87,27 +87,27 @@ export async function createResource(req: Request, res: Response, next: NextFunc
subdomain,
}).returning();
- // find the superuser roleId and also add the resource to the superuser role
- const superuserRole = await db.select()
+ // find the Super User roleId and also add the resource to the Super User role
+ const superUserRole = await db.select()
.from(roles)
- .where(and(eq(roles.isSuperuserRole, true), eq(roles.orgId, orgId)))
+ .where(and(eq(roles.isSuperUserRole, true), eq(roles.orgId, orgId)))
.limit(1);
- if (superuserRole.length === 0) {
+ if (superUserRole.length === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
- `Superuser role not found`
+ `Super User role not found`
)
);
}
await db.insert(roleResources).values({
- roleId: superuserRole[0].roleId,
+ roleId: superUserRole[0].roleId,
resourceId: newResource[0].resourceId,
});
- if (req.userOrgRoleId != superuserRole[0].roleId) {
+ if (req.userOrgRoleId != superUserRole[0].roleId) {
// make sure the user can access the resource
await db.insert(userResources).values({
userId: req.user?.userId!,
diff --git a/server/routers/resource/listResourceRoles.ts b/server/routers/resource/listResourceRoles.ts
index aa624733..c3d880f3 100644
--- a/server/routers/resource/listResourceRoles.ts
+++ b/server/routers/resource/listResourceRoles.ts
@@ -51,7 +51,7 @@ export async function listResourceRoles(
roleId: roles.roleId,
name: roles.name,
description: roles.description,
- isSuperuserRole: roles.isSuperuserRole,
+ isSuperUserRole: roles.isSuperUserRole,
})
.from(roleResources)
.innerJoin(roles, eq(roleResources.roleId, roles.roleId))
diff --git a/server/routers/role/deleteRole.ts b/server/routers/role/deleteRole.ts
index 62138e53..24a650f0 100644
--- a/server/routers/role/deleteRole.ts
+++ b/server/routers/role/deleteRole.ts
@@ -61,11 +61,11 @@ export async function deleteRole(
);
}
- if (role[0].isSuperuserRole) {
+ if (role[0].isSuperUserRole) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
- `Cannot delete a superuser role`
+ `Cannot delete a Super User role`
)
);
}
diff --git a/server/routers/role/listRoles.ts b/server/routers/role/listRoles.ts
index 1b6f911f..f73f8af0 100644
--- a/server/routers/role/listRoles.ts
+++ b/server/routers/role/listRoles.ts
@@ -80,7 +80,7 @@ export async function listRoles(
.select({
roleId: roles.roleId,
orgId: roles.orgId,
- isSuperuserRole: roles.isSuperuserRole,
+ isSuperUserRole: roles.isSuperUserRole,
name: roles.name,
description: roles.description,
orgName: orgs.name,
diff --git a/server/routers/role/updateRole.ts b/server/routers/role/updateRole.ts
index 8d05db76..04ce2478 100644
--- a/server/routers/role/updateRole.ts
+++ b/server/routers/role/updateRole.ts
@@ -81,11 +81,11 @@ export async function updateRole(
);
}
- if (role[0].isSuperuserRole) {
+ if (role[0].isSuperUserRole) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
- `Cannot update a superuser role`
+ `Cannot update a Super User role`
)
);
}
diff --git a/server/routers/site/createSite.ts b/server/routers/site/createSite.ts
index c50018ec..b35793d9 100644
--- a/server/routers/site/createSite.ts
+++ b/server/routers/site/createSite.ts
@@ -107,25 +107,25 @@ export async function createSite(
subnet,
})
.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()
.from(roles)
- .where(and(eq(roles.isSuperuserRole, true), eq(roles.orgId, orgId)))
+ .where(and(eq(roles.isSuperUserRole, true), eq(roles.orgId, orgId)))
.limit(1);
- if (superuserRole.length === 0) {
+ if (superUserRole.length === 0) {
return next(
- createHttpError(HttpCode.NOT_FOUND, `Superuser role not found`)
+ createHttpError(HttpCode.NOT_FOUND, `Super User role not found`)
);
}
await db.insert(roleSites).values({
- roleId: superuserRole[0].roleId,
+ roleId: superUserRole[0].roleId,
siteId: newSite.siteId,
});
- if (req.userOrgRoleId != superuserRole[0].roleId) {
+ if (req.userOrgRoleId != superUserRole[0].roleId) {
// make sure the user can access the site
db.insert(userSites).values({
userId: req.user?.userId!,
diff --git a/server/routers/site/listSiteRoles.ts b/server/routers/site/listSiteRoles.ts
index 8c725908..1c255124 100644
--- a/server/routers/site/listSiteRoles.ts
+++ b/server/routers/site/listSiteRoles.ts
@@ -51,7 +51,7 @@ export async function listSiteRoles(
roleId: roles.roleId,
name: roles.name,
description: roles.description,
- isSuperuserRole: roles.isSuperuserRole,
+ isSuperUserRole: roles.isSuperUserRole,
})
.from(roleSites)
.innerJoin(roles, eq(roleSites.roleId, roles.roleId))
diff --git a/server/routers/user/inviteUser.ts b/server/routers/user/inviteUser.ts
index bcbff537..a4d69772 100644
--- a/server/routers/user/inviteUser.ts
+++ b/server/routers/user/inviteUser.ts
@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
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 response from "@server/utils/response";
import HttpCode from "@server/types/HttpCode";
@@ -13,6 +13,8 @@ import { createDate, TimeSpan } from "oslo";
import config from "@server/config";
import { hashPassword } from "@server/auth/password";
import { fromError } from "zod-validation-error";
+import { sendEmail } from "@server/emails";
+import SendInviteLink from "@server/emails/templates/SendInviteLink";
const inviteUserParamsSchema = z.object({
orgId: z.string(),
@@ -31,6 +33,8 @@ export type InviteUserResponse = {
expiresAt: number;
};
+const inviteTracker: Record = {};
+
export async function inviteUser(
req: Request,
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
.select()
.from(users)
@@ -116,6 +153,21 @@ export async function inviteUser(
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(res, {
data: {
inviteLink,
diff --git a/server/routers/user/removeUserOrg.ts b/server/routers/user/removeUserOrg.ts
index f1f297f9..652acc95 100644
--- a/server/routers/user/removeUserOrg.ts
+++ b/server/routers/user/removeUserOrg.ts
@@ -11,7 +11,7 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const removeUserSchema = z.object({
- userId: z.string().uuid(),
+ userId: z.string(),
orgId: z.string(),
});
@@ -33,7 +33,6 @@ export async function removeUserOrg(
const { userId, orgId } = parsedParams.data;
- // Check if the user has permission to list sites
const hasPermission = await checkUserActionPermission(
ActionsEnum.removeUser,
req
@@ -56,7 +55,7 @@ export async function removeUserOrg(
data: null,
success: true,
error: false,
- message: "User deleted successfully",
+ message: "User remove from org successfully",
status: HttpCode.OK,
});
} catch (error) {
diff --git a/src/app/[orgId]/settings/sites/components/SitesTable.tsx b/src/app/[orgId]/settings/sites/components/SitesTable.tsx
index 93466e3d..655d90cc 100644
--- a/src/app/[orgId]/settings/sites/components/SitesTable.tsx
+++ b/src/app/[orgId]/settings/sites/components/SitesTable.tsx
@@ -79,7 +79,7 @@ export const columns: ColumnDef[] = [
.then(() => {
router.refresh();
});
- }
+ };
return (
@@ -98,7 +98,12 @@ export const columns: ColumnDef[] = [
-
+
diff --git a/src/app/[orgId]/settings/users/components/InviteUserForm.tsx b/src/app/[orgId]/settings/users/components/InviteUserForm.tsx
index 2364233d..f3cf8073 100644
--- a/src/app/[orgId]/settings/users/components/InviteUserForm.tsx
+++ b/src/app/[orgId]/settings/users/components/InviteUserForm.tsx
@@ -122,7 +122,13 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
return (
<>
-
+ {
+ setOpen(val);
+ setInviteLink(null);
+ setLoading(false);
+ setExpiresInDays(1);
+ form.reset();
+ }}>
Invite User
@@ -257,7 +263,7 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
type="submit"
form="invite-user-form"
loading={loading}
- disabled={inviteLink !== null}
+ disabled={inviteLink !== null || loading}
>
Create Invitation
diff --git a/src/app/[orgId]/settings/users/components/UsersTable.tsx b/src/app/[orgId]/settings/users/components/UsersTable.tsx
index 54560985..a35e8f67 100644
--- a/src/app/[orgId]/settings/users/components/UsersTable.tsx
+++ b/src/app/[orgId]/settings/users/components/UsersTable.tsx
@@ -12,60 +12,178 @@ import { ArrowUpDown, MoreHorizontal } from "lucide-react";
import { UsersDataTable } from "./UsersDataTable";
import { useState } from "react";
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 = {
id: string;
email: string;
+ status: string;
+ role: string;
};
-export const columns: ColumnDef[] = [
- {
- accessorKey: "email",
- header: ({ column }) => {
- return (
-
- );
- },
- },
- {
- id: "actions",
- cell: ({ row }) => {
- const userRow = row.original;
-
- return (
-
-
-
-
-
- Edit access
-
-
- );
- },
- },
-];
-
type UsersTableProps = {
users: UserRow[];
};
export default function UsersTable({ users }: UsersTableProps) {
const [isInviteModalOpen, setIsInviteModalOpen] = useState(false);
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+ const [userToRemove, setUserToRemove] = useState(null);
+
+ const user = useUserContext();
+ const { org } = useOrgContext();
+ const { toast } = useToast();
+
+ const columns: ColumnDef[] = [
+ {
+ accessorKey: "email",
+ header: ({ column }) => {
+ return (
+
+ );
+ },
+ },
+ {
+ accessorKey: "status",
+ header: ({ column }) => {
+ return (
+
+ );
+ },
+ },
+ {
+ accessorKey: "role",
+ header: ({ column }) => {
+ return (
+
+ );
+ },
+ },
+ {
+ id: "actions",
+ cell: ({ row }) => {
+ const userRow = row.original;
+
+ return (
+ <>
+
+
+
+
+
+ Manage user
+ {userRow.email !== user?.email && (
+
+
+
+ )}
+
+
+ >
+ );
+ },
+ },
+ ];
+
+ 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 (
<>
+ {
+ setIsDeleteModalOpen(val);
+ setUserToRemove(null);
+ }}
+ dialog={
+
+
+ Are you sure you want to remove{" "}
+ {userToRemove?.email} from the organization?
+
+
+
+ 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.
+
+
+
+ To confirm, please type the email address of the
+ user below.
+