Skip to content

Commit

Permalink
Merge pull request #413 from captableinc/rbac
Browse files Browse the repository at this point in the history
feat: add rbac
  • Loading branch information
dahal committed Jul 10, 2024
2 parents 2d26136 + 1b5ab0d commit f3f1ed9
Show file tree
Hide file tree
Showing 67 changed files with 2,683 additions and 289 deletions.
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"format": "biome format --write ./src && prisma format",
"email:preview": "email preview ./src/emails",
"copy:pdfjs": "node scripts/copy-pdfjs-worker.cjs",
"knip": "knip"
"knip": "knip",
"test": "vitest"
},
"dependencies": {
"@ark-ui/react": "^2.2.3",
Expand Down Expand Up @@ -89,7 +90,7 @@
"lodash-es": "^4.17.21",
"mime": "^4.0.3",
"nanoid": "^5.0.4",
"next": "^14.2.3",
"next": "^14.2.4",
"next-auth": "^4.24.7",
"next-nprogress-bar": "^2.3.11",
"nodemailer": "^6.9.14",
Expand All @@ -106,7 +107,6 @@
"react-hook-form": "^7.51.5",
"react-number-format": "^5.3.4",
"react-pdf": "^8.0.2",
"server-only": "^0.0.1",
"sharp": "^0.33.3",
"sonner": "^1.4.41",
"stripe": "^15.8.0",
Expand Down Expand Up @@ -144,7 +144,8 @@
"prisma": "^5.13.0",
"tailwindcss": "^3.4.3",
"tsx": "^4.7.0",
"typescript": "^5.4.5"
"typescript": "^5.4.5",
"vitest": "^1.6.0"
},
"ct3aMetadata": {
"initVersion": "7.25.1"
Expand Down
868 changes: 767 additions & 101 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions prisma/migrations/20240709002523_add_rbac/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-- CreateEnum
CREATE TYPE "Roles" AS ENUM ('ADMIN', 'CUSTOM');

-- AlterTable
ALTER TABLE "Member" ADD COLUMN "customRoleId" TEXT,
ADD COLUMN "role" "Roles" DEFAULT 'ADMIN';

-- CreateTable
CREATE TABLE "CustomRole" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"companyId" TEXT NOT NULL,
"permissions" JSONB[],

CONSTRAINT "CustomRole_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE INDEX "CustomRole_companyId_idx" ON "CustomRole"("companyId");

-- CreateIndex
CREATE INDEX "Member_customRoleId_idx" ON "Member"("customRoleId");
24 changes: 24 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ model Company {
dataRooms DataRoom[]
eSignAudits EsignAudit[]
billingCustomers BillingCustomer[]
Role CustomRole[]
@@unique([publicId])
}
Expand All @@ -166,11 +167,17 @@ enum MemberStatusEnum {
PENDING
}

enum Roles {
ADMIN
CUSTOM
}

model Member {
id String @id @default(cuid())
title String?
status MemberStatusEnum @default(PENDING)
isOnboarded Boolean @default(false)
role Roles? @default(ADMIN)
workEmail String?
lastAccessed DateTime @default(now())
createdAt DateTime @default(now())
Expand All @@ -182,6 +189,9 @@ model Member {
companyId String
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
customRoleId String?
customRole CustomRole? @relation(fields: [customRoleId], references: [id])
documentReceived EsignRecipient[]
documents Document[]
templates Template[]
Expand All @@ -193,6 +203,20 @@ model Member {
@@index([companyId])
@@index([status])
@@index([userId])
@@index([customRoleId])
}

model CustomRole {
id String @id @default(cuid())
name String
companyId String
company Company @relation(fields: [companyId], references: [id])
permissions Json[]
member Member[]
@@index([companyId])
}

enum StakeholderTypeEnum {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import FileIcon from "@/components/common/file-icon";
import FilePreview from "@/components/file/preview";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { withServerSession } from "@/server/auth";
import { withServerComponentSession } from "@/server/auth";
import { db } from "@/server/db";
import { getPresignedGetUrl } from "@/server/file-uploads";
import { RiArrowLeftSLine } from "@remixicon/react";
Expand All @@ -15,7 +15,7 @@ const DocumentPreview = async ({
}: {
params: { publicId: string; bucketId: string };
}) => {
const session = await withServerSession();
const session = await withServerComponentSession();
const companyId = session?.user?.companyId;
const document = await db.document.findFirst({
where: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import EmptyState from "@/components/common/empty-state";
import { Button } from "@/components/ui/button";
import { getServerAuthSession } from "@/server/auth";
import { getServerComponentAuthSession } from "@/server/auth";
import { db } from "@/server/db";
import { RiAddFill, RiFolderCheckFill } from "@remixicon/react";
import { Fragment } from "react";
Expand All @@ -28,7 +28,7 @@ const getDataRooms = (companyId: string) => {
};

const DataRoomPage = async () => {
const session = await getServerAuthSession();
const session = await getServerComponentAuthSession();

if (!session || !session.user) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import { PdfCanvas } from "@/components/template/pdf-canvas";
import { TemplateFieldForm } from "@/components/template/template-field-form";
import { Badge } from "@/components/ui/badge";
import { TemplateFieldProvider } from "@/providers/template-field-provider";
import { withServerSession } from "@/server/auth";
import { withServerComponentSession } from "@/server/auth";
import { api } from "@/trpc/server";

const EsignTemplateDetailPage = async ({
params: { templatePublicId },
}: {
params: { templatePublicId: string };
}) => {
const session = await withServerSession();
const session = await withServerComponentSession();

const { name, status, url, fields, recipients } =
await api.template.get.query({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import EmptyState from "@/components/common/empty-state";
import { PageLayout } from "@/components/dashboard/page-layout";
import { Card } from "@/components/ui/card";
import { withServerSession } from "@/server/auth";
import { withServerComponentSession } from "@/server/auth";
import { api } from "@/trpc/server";
import { RiUploadCloudLine } from "@remixicon/react";
import type { Metadata } from "next";
Expand All @@ -13,7 +13,7 @@ export const metadata: Metadata = {
};

const EsignDocumentPage = async () => {
const session = await withServerSession();
const session = await withServerComponentSession();
const { documents } = await api.template.all.query();

if (documents.length === 0) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import EmptyState from "@/components/common/empty-state";
import { PageLayout } from "@/components/dashboard/page-layout";
import { Card } from "@/components/ui/card";
import { withServerSession } from "@/server/auth";
import { withServerComponentSession } from "@/server/auth";
import { api } from "@/trpc/server";
import { RiUploadCloudLine } from "@remixicon/react";
import type { Metadata } from "next";
Expand All @@ -14,7 +14,7 @@ export const metadata: Metadata = {

const DocumentsPage = async () => {
const documents = await api.document.getAll.query();
const session = await withServerSession();
const session = await withServerComponentSession();

if (documents.length === 0) {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import EmptyState from "@/components/common/empty-state";
import { PageLayout } from "@/components/dashboard/page-layout";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { withServerSession } from "@/server/auth";
import { withServerComponentSession } from "@/server/auth";
import { api } from "@/trpc/server";
import { RiAddFill, RiUploadCloudLine } from "@remixicon/react";
import type { Metadata } from "next";
Expand All @@ -15,7 +15,7 @@ export const metadata: Metadata = {

const DocumentsPage = async () => {
const documents = await api.document.getAll.query();
const session = await withServerSession();
const session = await withServerComponentSession();

if (documents.length === 0) {
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import EmptyState from "@/components/common/empty-state";
import Tldr from "@/components/common/tldr";
import { Card } from "@/components/ui/card";
import { withServerSession } from "@/server/auth";
import { withServerComponentSession } from "@/server/auth";
import { db } from "@/server/db";
import type { EquityPlanMutationType } from "@/trpc/routers/equity-plan/schema";
import type { ShareClassMutationType } from "@/trpc/routers/share-class/schema";
Expand All @@ -27,7 +27,7 @@ const getShareClasses = async (companyId: string) => {
};

const EquityPlanPage = async () => {
const session = await withServerSession();
const session = await withServerComponentSession();
const companyId = session?.user?.companyId;
let equityPlans: EquityPlanMutationType[] = [];

Expand Down
17 changes: 12 additions & 5 deletions src/app/(authenticated)/(dashboard)/[publicId]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { NavBar } from "@/components/dashboard/navbar";
import { SideBar } from "@/components/dashboard/sidebar";
import { ModalProvider } from "@/components/modals";
import { withServerSession } from "@/server/auth";
import { withServerComponentSession } from "@/server/auth";
import { getCompanyList } from "@/server/company";
import { redirect } from "next/navigation";
import "@/styles/hint.css";
import { RBAC } from "@/lib/rbac";
import { getServerPermissions } from "@/lib/rbac/access-control";
import { RolesProvider } from "@/providers/roles-provider";

type DashboardLayoutProps = {
children: React.ReactNode;
Expand All @@ -15,16 +18,20 @@ const DashboardLayout = async ({
children,
params: { publicId },
}: DashboardLayoutProps) => {
const { user } = await withServerSession();
const { user } = await withServerComponentSession();

if (user.companyPublicId !== publicId) {
redirect(`/${user.companyPublicId}`);
}

const companies = await getCompanyList(user.id);
const [companies, permissionsData] = await Promise.all([
getCompanyList(user.id),
getServerPermissions(),
]);

const permissions = RBAC.normalizePermissionsMap(permissionsData.permissions);
return (
<>
<RolesProvider data={{ permissions }}>
<div className="flex min-h-screen bg-gray-50">
<aside className="sticky top-0 hidden min-h-full w-64 flex-shrink-0 flex-col lg:flex lg:border-r">
<SideBar companies={companies} publicId={publicId} />
Expand All @@ -37,7 +44,7 @@ const DashboardLayout = async ({
</div>
</div>
<ModalProvider />
</>
</RolesProvider>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import EmptyState from "@/components/common/empty-state";
import { Card } from "@/components/ui/card";
import { withServerSession } from "@/server/auth";
import { withServerComponentSession } from "@/server/auth";
import { db } from "@/server/db";
import { RiTerminalBoxFill } from "@remixicon/react";
import type { Metadata } from "next";
Expand All @@ -12,7 +12,7 @@ export const metadata: Metadata = {
title: "API Keys",
};
const ApiSettingsPage = async () => {
const session = await withServerSession();
const session = await withServerComponentSession();
const { user } = session;
const apiKeys = await db.apiKey.findMany({
where: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { PageLayout } from "@/components/dashboard/page-layout";
import { RoleCreateUpdateModalAction } from "@/components/modals/role-create-update-modal";
import { RoleTable } from "@/components/rbac/role-table";
import { Card } from "@/components/ui/card";
import { serverAccessControl } from "@/lib/rbac/access-control";
import { api } from "@/trpc/server";

export default async function RolesPage() {
const { allow } = await serverAccessControl();

const data = await allow(api.rbac.listRoles.query(), ["roles", "read"]);

const canCreate = allow(true, ["roles", "create"]);

return (
<div className="flex flex-col gap-y-3">
<PageLayout
title="Roles"
description="Create and manage roles for your company."
action={<RoleCreateUpdateModalAction disabled={!canCreate} />}
/>

{data ? <RoleTable roles={data.rolesList} /> : null}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { SecurityList } from "@/components/security/SecurityList";
import { SettingsHeader } from "@/components/security/SettingHeader";
import { getServerAuthSession } from "@/server/auth";
import type { Metadata } from "next";
import { redirect } from "next/navigation";

Expand Down Expand Up @@ -30,10 +29,6 @@ const SecurityLists = [
];

export default async function SecurityPage() {
const session = await getServerAuthSession();
if (!session?.user) {
redirect("/login");
}
return (
<>
<SettingsHeader
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,21 @@ import {
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import type { RouterOutputs } from "@/trpc/shared";
import { RiAccountCircleFill, RiAddLine } from "@remixicon/react";

export const AddTeamMemberDropdownMenu = () => {
type Roles = RouterOutputs["rbac"]["listRoles"]["rolesList"];

interface AddTeamMemberDropdownMenuProps {
roles: Roles;
}

export const AddTeamMemberDropdownMenu = ({
roles,
}: AddTeamMemberDropdownMenuProps) => {
return (
<DropdownMenu>
<DropdownMenuTrigger>
<DropdownMenuTrigger asChild>
<Button className="w-full md:w-auto" size="sm">
<RiAddLine className="inline-block h-5 w-5" />
Team member
Expand All @@ -35,7 +44,9 @@ export const AddTeamMemberDropdownMenu = () => {
loginEmail: "",
title: "",
workEmail: "",
roleId: "",
},
roles,
});
}}
>
Expand Down
Loading

0 comments on commit f3f1ed9

Please sign in to comment.