dashboard: restrict access to organization users

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-03-26 21:13:11 +01:00
parent 45e9b1d702
commit d0079c8dc3
33 changed files with 856 additions and 225 deletions

View File

@@ -0,0 +1,17 @@
-- CreateEnum
CREATE TYPE "AccessLevel" AS ENUM ('read', 'write', 'admin');
-- CreateTable
CREATE TABLE "project_access" (
"id" TEXT NOT NULL,
"project_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"level" "AccessLevel" NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "project_access_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "project_access" ADD CONSTRAINT "project_access_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,12 @@
/*
Warnings:
- The primary key for the `project_access` table will be changed. If it partially fails, the table could be left without primary key constraint.
- The `id` column on the `project_access` table would be dropped and recreated. This will lead to data loss if there is data in the column.
*/
-- AlterTable
ALTER TABLE "project_access" DROP CONSTRAINT "project_access_pkey",
DROP COLUMN "id",
ADD COLUMN "id" UUID NOT NULL DEFAULT gen_random_uuid(),
ADD CONSTRAINT "project_access_pkey" PRIMARY KEY ("id");

View File

@@ -0,0 +1,8 @@
/*
Warnings:
- Added the required column `organization_slug` to the `project_access` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "project_access" ADD COLUMN "organization_slug" TEXT NOT NULL;

View File

@@ -27,9 +27,30 @@ model Project {
EventMeta EventMeta[]
Reference Reference[]
access ProjectAccess[]
@@map("projects")
}
enum AccessLevel {
read
write
admin
}
model ProjectAccess {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
project_id String
project Project @relation(fields: [project_id], references: [id])
organization_slug String
user_id String
level AccessLevel
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@map("project_access")
}
model Event {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String

View File

@@ -2,21 +2,31 @@ import { auth, clerkClient } from '@clerk/nextjs';
import type {
Organization,
OrganizationInvitation,
OrganizationMembership,
} from '@clerk/nextjs/dist/types/server';
import type { ProjectAccess } from '../prisma-client';
import { db } from '../prisma-client';
export type IServiceOrganization = Awaited<
ReturnType<typeof getCurrentOrganizations>
>[number];
export type IServiceInvites = Awaited<ReturnType<typeof getInvites>>;
export type IServiceOrganization = ReturnType<typeof transformOrganization>;
export type IServiceInvite = ReturnType<typeof transformInvite>;
export type IServiceMember = ReturnType<typeof transformMember>;
export type IServiceProjectAccess = ReturnType<typeof transformAccess>;
export function transformOrganization(org: Organization) {
return {
id: org.id,
name: org.name,
slug: org.slug,
slug: org.slug!,
};
}
export function transformAccess(access: ProjectAccess) {
return {
projectId: access.project_id,
userId: access.user_id,
level: access.level,
organizationSlug: access.organization_slug,
};
}
@@ -50,18 +60,77 @@ export async function getOrganizationByProjectId(projectId: string) {
export function transformInvite(invite: OrganizationInvitation) {
return {
id: invite.id,
organizationId: invite.organizationId,
email: invite.emailAddress,
role: invite.role,
status: invite.status,
createdAt: invite.createdAt,
updatedAt: invite.updatedAt,
publicMetadata: invite.publicMetadata,
};
}
export async function getInvites(organizationId: string) {
export async function getInvites(organizationSlug: string) {
const org = await getOrganizationBySlug(organizationSlug);
if (!org) return [];
return await clerkClient.organizations
.getOrganizationInvitationList({
organizationId,
organizationId: org.id,
})
.then((invites) => invites.map(transformInvite));
}
export function transformMember(
item: OrganizationMembership & {
access: IServiceProjectAccess[];
}
) {
return {
memberId: item.id,
id: item.publicUserData?.userId,
name:
[item.publicUserData?.firstName, item.publicUserData?.lastName]
.filter(Boolean)
.join(' ') || 'Unknown',
role: item.role,
createdAt: item.createdAt,
updatedAt: item.updatedAt,
publicMetadata: item.publicMetadata,
organization: transformOrganization(item.organization),
access: item.access,
};
}
export async function getMembers(organizationSlug: string) {
const org = await getOrganizationBySlug(organizationSlug);
if (!org) return [];
const [members, access] = await Promise.all([
clerkClient.organizations.getOrganizationMembershipList({
organizationId: org.id,
}),
db.projectAccess.findMany({
where: {
organization_slug: organizationSlug,
},
}),
]);
return members
.map((member) => {
const projectAccess = access.filter(
(item) => item.user_id === member.publicUserData?.userId
);
return {
...member,
access: projectAccess.map(transformAccess),
};
})
.map(transformMember);
}
export async function getMember(organizationSlug: string, userId: string) {
const org = await getOrganizationBySlug(organizationSlug);
if (!org) return null;
const members = await getMembers(org.id);
return members.find((member) => member.id === userId) ?? null;
}

View File

@@ -1,3 +1,6 @@
import { auth } from '@clerk/nextjs';
import { project } from 'ramda';
import type { Project } from '../prisma-client';
import { db } from '../prisma-client';
@@ -33,3 +36,31 @@ export async function getProjectsByOrganizationSlug(slug: string) {
return res.map(transformProject);
}
export async function getCurrentProjects(slug: string) {
const session = auth();
if (!session.userId) {
return [];
}
const access = await db.projectAccess.findMany({
where: {
organization_slug: slug,
user_id: session.userId,
},
});
const res = await db.project.findMany({
where: {
organization_slug: slug,
},
});
if (access.length === 0) {
return res.map(transformProject);
}
return res
.filter((project) => access.some((a) => a.project_id === project.id))
.map(transformProject);
}

View File

@@ -80,7 +80,8 @@ export const zChartInput = z.object({
export const zInviteUser = z.object({
email: z.string().email(),
organizationSlug: z.string(),
role: z.enum(['admin', 'org:member']),
role: z.enum(['org:admin', 'org:member']),
access: z.array(z.string()),
});
export const zShareOverview = z.object({