From fd705aecc590213158c5965ad09e55cd2e2c1c3e Mon Sep 17 00:00:00 2001 From: Mostapha El Sabah Date: Tue, 14 Jan 2025 22:29:24 -0500 Subject: [PATCH] feat: Add tenant table/logic (#50) --- eslint.config.js | 9 +++++ .../migration.sql | 39 +++++++++++++++++++ prisma/schema.prisma | 19 ++++++++- src/lib/server/auth/index.ts | 26 ++++++++++++- src/lib/server/pothos/schema/users.ts | 2 + 5 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 prisma/migrations/20250115025622_add_tenant_table/migration.sql diff --git a/eslint.config.js b/eslint.config.js index 4c9cdec..5b2744d 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -41,6 +41,15 @@ export default ts.config( caughtErrorsIgnorePattern: '^_', }, ], + 'no-restricted-syntax': [ + 'error', + { + selector: + 'CallExpression:matches([callee.object.object.name="prisma"], [callee.object.object.name="prismaTransactionClient"], [callee.object.object.name="transactionClient"]):matches([callee.property.name="findFirst"], [callee.property.name="findMany"], [callee.property.name="updateMany"], [callee.property.name="deleteMany"], [callee.property.name="count"], [callee.property.name="aggregate"], [callee.property.name="groupBy"]):not(:has(ObjectExpression > Property[key.name="where"] > ObjectExpression > Property[key.name="tenantId"]))', + message: + 'Please filter on the current tenant when using findFirst, findMany, updateMany, deleteMany, count, aggregate or groupBy.', + }, + ], }, } ); diff --git a/prisma/migrations/20250115025622_add_tenant_table/migration.sql b/prisma/migrations/20250115025622_add_tenant_table/migration.sql new file mode 100644 index 0000000..6dd7285 --- /dev/null +++ b/prisma/migrations/20250115025622_add_tenant_table/migration.sql @@ -0,0 +1,39 @@ +/* + Warnings: + + - The primary key for the `User` table will be changed. If it partially fails, the table could be left without primary key constraint. + - A unique constraint covering the columns `[clerkId,tenantId]` on the table `User` will be added. If there are existing duplicate values, this will fail. + - Added the required column `tenantId` to the `User` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropIndex +DROP INDEX "User_clerkId_key"; + +-- AlterTable +ALTER TABLE "User" DROP CONSTRAINT "User_pkey", +ADD COLUMN "tenantId" TEXT NOT NULL, +ADD CONSTRAINT "User_pkey" PRIMARY KEY ("id", "tenantId"); + +-- CreateTable +CREATE TABLE "Tenant" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "clerkOrganizationId" TEXT NOT NULL, + + CONSTRAINT "Tenant_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Tenant_slug_key" ON "Tenant"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "Tenant_clerkOrganizationId_key" ON "Tenant"("clerkOrganizationId"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_clerkId_tenantId_key" ON "User"("clerkId", "tenantId"); + +-- AddForeignKey +ALTER TABLE "User" ADD CONSTRAINT "User_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3033568..fb780a0 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -15,12 +15,27 @@ datasource db { } model User { - id String @id @default(uuid()) - clerkId String @unique + id String @default(uuid()) + clerkId String + tenant Tenant @relation(fields: [tenantId], references: [id]) + tenantId String email String? name String createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt + + @@id([id, tenantId]) + @@unique([clerkId, tenantId]) +} + +model Tenant { + id String @id @default(uuid()) + name String + slug String @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + clerkOrganizationId String @unique + users User[] } diff --git a/src/lib/server/auth/index.ts b/src/lib/server/auth/index.ts index 8bc10b9..e628e58 100644 --- a/src/lib/server/auth/index.ts +++ b/src/lib/server/auth/index.ts @@ -14,7 +14,7 @@ export async function validateSession({ locals }: ServerLoadEvent) { return redirect(307, '/login'); } - if (!locals.auth.orgId && locals.auth.sessionId) { + if ((!locals.auth.orgId && locals.auth.sessionId) || !locals.auth.orgId) { // Sign out the user if they are not associated with an organization await clerkSessionClient.sessions.revokeSession(locals.auth.sessionId); return redirect(307, '/login'); @@ -22,9 +22,32 @@ export async function validateSession({ locals }: ServerLoadEvent) { const clerkUser = await clerkClient.users.getUser(locals.auth.userId); + const tenantClerkId = locals.auth.orgId; + + let tenant = await prisma.tenant.findUnique({ + where: { + clerkOrganizationId: tenantClerkId, + }, + }); + + if (!tenant) { + const organization = await clerkClient.organizations.getOrganization({ + organizationId: tenantClerkId, + }); + + tenant = await prisma.tenant.create({ + data: { + clerkOrganizationId: tenantClerkId, + name: organization.name, + slug: organization.slug ?? `tenant-${tenantClerkId}`, + }, + }); + } + let user = await prisma.user.findFirst({ where: { clerkId: clerkUser.id, + tenantId: tenant.id, }, }); @@ -41,6 +64,7 @@ export async function validateSession({ locals }: ServerLoadEvent) { clerkId: clerkUser.id, email: clerkUser.emailAddresses[0].emailAddress, name: clerkUser.fullName ?? '', + tenantId: tenant.id, }, }); diff --git a/src/lib/server/pothos/schema/users.ts b/src/lib/server/pothos/schema/users.ts index 24db8da..055bb40 100644 --- a/src/lib/server/pothos/schema/users.ts +++ b/src/lib/server/pothos/schema/users.ts @@ -19,6 +19,8 @@ builder.queryFields((t) => ({ users: t.prismaField({ type: [User], resolve: async () => { + // TODO: Fix this when we add a tenant context + // eslint-disable-next-line no-restricted-syntax return await prisma.user.findMany(); }, }),