migrate organizations from clerk to in-house

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-06-16 19:32:53 +02:00
parent 84ac68fe63
commit c7dbc2f7c4
27 changed files with 523 additions and 294 deletions

View File

@@ -1,6 +1,6 @@
import { clerkClient } from '@clerk/fastify';
import { getProjectById } from '@openpanel/db';
import { db, getProjectById } from '@openpanel/db';
import { cacheable } from '@openpanel/redis';
export const getProjectAccessCached = cacheable(getProjectAccess, 60 * 60);
@@ -13,20 +13,19 @@ export async function getProjectAccess({
}) {
try {
// Check if user has access to the project
const [project, organizations] = await Promise.all([
getProjectById(projectId),
clerkClient.users.getOrganizationMembershipList({
userId,
}),
]);
if (!project) {
const project = await getProjectById(projectId);
if (!project?.organizationSlug) {
return false;
}
return !!organizations.data.find(
(org) => org.organization.slug === project.organizationSlug
);
const member = await db.member.findFirst({
where: {
organizationId: project.organizationSlug,
userId,
},
});
return member;
} catch (err) {
return false;
}
@@ -43,11 +42,10 @@ export async function getOrganizationAccess({
userId: string;
organizationId: string;
}) {
const organizations = await clerkClient.users.getOrganizationMembershipList({
userId,
return db.member.findFirst({
where: {
userId,
organizationId,
},
});
return !!organizations.data.find(
(org) => org.organization.id === organizationId
);
}

View File

@@ -1,9 +1,8 @@
import { randomUUID } from 'crypto';
import { clerkClient } from '@clerk/fastify';
import type { z } from 'zod';
import { hashPassword, slug, stripTrailingSlash } from '@openpanel/common';
import { db, getId } from '@openpanel/db';
import { db, getId, getOrganizationBySlug } from '@openpanel/db';
import type { ProjectType } from '@openpanel/db';
import { zOnboardingProject } from '@openpanel/validation';
@@ -14,16 +13,16 @@ async function createOrGetOrganization(
userId: string
) {
if (input.organizationSlug) {
return await clerkClient.organizations.getOrganization({
slug: input.organizationSlug,
});
return await getOrganizationBySlug(input.organizationSlug);
}
if (input.organization) {
return await clerkClient.organizations.createOrganization({
name: input.organization,
slug: slug(input.organization),
createdBy: userId,
return db.organization.create({
data: {
id: slug(input.organization),
name: input.organization,
createdByUserId: userId,
},
});
}
@@ -44,7 +43,7 @@ export const onboardingRouter = createTRPCRouter({
ctx.session.userId
);
if (!organization?.slug) {
if (!organization?.id) {
throw new Error('Organization slug is missing');
}
@@ -52,7 +51,7 @@ export const onboardingRouter = createTRPCRouter({
data: {
id: await getId('project', input.project),
name: input.project,
organizationSlug: organization.slug,
organizationSlug: organization.id,
types,
},
});
@@ -61,7 +60,7 @@ export const onboardingRouter = createTRPCRouter({
const client = await db.client.create({
data: {
name: `${project.name} Client`,
organizationSlug: organization.slug,
organizationSlug: organization.id,
projectId: project.id,
type: 'write',
cors: input.domain ? stripTrailingSlash(input.domain) : null,

View File

@@ -1,4 +1,5 @@
import { clerkClient } from '@clerk/fastify';
import { pathOr } from 'ramda';
import { z } from 'zod';
import { db, getOrganizationBySlug } from '@openpanel/db';
@@ -7,18 +8,6 @@ import { zInviteUser } from '@openpanel/validation';
import { createTRPCRouter, protectedProcedure } from '../trpc';
export const organizationRouter = createTRPCRouter({
list: protectedProcedure.query(() => {
return clerkClient.organizations.getOrganizationList();
}),
get: protectedProcedure
.input(
z.object({
id: z.string(),
})
)
.query(({ input }) => {
return getOrganizationBySlug(input.id);
}),
update: protectedProcedure
.input(
z.object({
@@ -27,42 +16,63 @@ export const organizationRouter = createTRPCRouter({
})
)
.mutation(({ input }) => {
return clerkClient.organizations.updateOrganization(input.id, {
name: input.name,
return db.organization.update({
where: {
id: input.id,
},
data: {
name: input.name,
},
});
}),
inviteUser: protectedProcedure
.input(zInviteUser)
.mutation(async ({ input, ctx }) => {
const organization = await getOrganizationBySlug(input.organizationSlug);
if (!organization) {
throw new Error('Organization not found');
}
return clerkClient.organizations.createOrganizationInvitation({
organizationId: organization.id,
const ticket = await clerkClient.invitations.createInvitation({
emailAddress: input.email,
role: input.role,
inviterUserId: ctx.session.userId,
publicMetadata: {
access: input.access,
notify: true,
});
return db.member.create({
data: {
email: input.email,
organizationId: input.organizationSlug,
role: input.role,
invitedById: ctx.session.userId,
meta: {
access: input.access,
invitationId: ticket.id,
},
},
});
}),
revokeInvite: protectedProcedure
.input(
z.object({
organizationId: z.string(),
invitationId: z.string(),
memberId: z.string(),
})
)
.mutation(async ({ input, ctx }) => {
return clerkClient.organizations.revokeOrganizationInvitation({
organizationId: input.organizationId,
invitationId: input.invitationId,
requestingUserId: ctx.session.userId,
.mutation(async ({ input }) => {
const member = await db.member.findUniqueOrThrow({
where: {
id: input.memberId,
},
});
const invitationId = pathOr<string | undefined>(
undefined,
['meta', 'invitationId'],
member
);
if (invitationId) {
await clerkClient.invitations.revokeInvitation(invitationId);
}
return db.member.delete({
where: {
id: input.memberId,
},
});
}),
@@ -77,25 +87,21 @@ export const organizationRouter = createTRPCRouter({
if (ctx.session.userId === input.userId) {
throw new Error('You cannot remove yourself from the organization');
}
const organization = await clerkClient.organizations.getOrganization({
organizationId: input.organizationId,
});
if (!organization?.slug) {
throw new Error('Organization not found');
}
await db.projectAccess.deleteMany({
where: {
userId: input.userId,
organizationSlug: organization.slug,
},
});
return clerkClient.organizations.deleteOrganizationMembership({
organizationId: input.organizationId,
userId: input.userId,
});
await db.$transaction([
db.member.deleteMany({
where: {
userId: input.userId,
organizationId: input.organizationId,
},
}),
db.projectAccess.deleteMany({
where: {
userId: input.userId,
organizationSlug: input.organizationId,
},
}),
]);
}),
updateMemberAccess: protectedProcedure

View File

@@ -1,7 +1,8 @@
import { clerkClient } from '@clerk/fastify';
import { SeventySevenClient } from '@seventy-seven/sdk';
import { z } from 'zod';
import { getUserById } from '@openpanel/db';
import { createTRPCRouter, protectedProcedure } from '../trpc';
const API_KEY = process.env.SEVENTY_SEVEN_API_KEY!;
@@ -21,14 +22,16 @@ export const ticketRouter = createTRPCRouter({
throw new Error('Ticket system not configured');
}
const user = await clerkClient.users.getUser(ctx.session.userId);
const user = await getUserById(ctx.session.userId);
return client.createTicket({
subject: input.subject,
body: input.body,
meta: input.meta,
senderEmail: user.primaryEmailAddress?.emailAddress || 'none',
senderFullName: user.fullName || 'none',
senderEmail: user?.email || 'none',
senderFullName: user?.firstName
? [user?.firstName, user?.lastName].filter(Boolean).join(' ')
: 'none',
});
}),
});

View File

@@ -1,7 +1,7 @@
import { clerkClient } from '@clerk/fastify';
import { z } from 'zod';
import { transformUser } from '@openpanel/db';
import { db } from '@openpanel/db';
import { createTRPCRouter, protectedProcedure } from '../trpc';
@@ -13,12 +13,23 @@ export const userRouter = createTRPCRouter({
lastName: z.string(),
})
)
.mutation(({ input, ctx }) => {
return clerkClient.users
.updateUser(ctx.session.userId, {
.mutation(async ({ input, ctx }) => {
const [updatedUser] = await Promise.all([
db.user.update({
where: {
id: ctx.session.userId,
},
data: {
firstName: input.firstName,
lastName: input.lastName,
},
}),
clerkClient.users.updateUser(ctx.session.userId, {
firstName: input.firstName,
lastName: input.lastName,
})
.then(transformUser);
}),
]);
return updatedUser;
}),
});