migrate organizations from clerk to in-house
This commit is contained in:
@@ -18,25 +18,21 @@ export default function LayoutOrganizationSelector({
|
||||
const router = useRouter();
|
||||
|
||||
const organization = organizations.find(
|
||||
(item) => item.slug === params.organizationSlug
|
||||
(item) => item.id === params.organizationSlug
|
||||
);
|
||||
|
||||
if (!organization) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
className="w-full"
|
||||
placeholder="Select organization"
|
||||
icon={Building}
|
||||
value={organization.slug}
|
||||
value={organization?.id}
|
||||
items={
|
||||
organizations
|
||||
.filter((item) => item.slug)
|
||||
.filter((item) => item.id)
|
||||
.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.slug,
|
||||
value: item.id,
|
||||
})) ?? []
|
||||
}
|
||||
onChange={(value) => {
|
||||
|
||||
@@ -27,7 +27,7 @@ export default async function AppLayout({
|
||||
getDashboardsByProjectId(projectId),
|
||||
]);
|
||||
|
||||
if (!organizations.find((item) => item.slug === organizationSlug)) {
|
||||
if (!organizations.find((item) => item.id === organizationSlug)) {
|
||||
return (
|
||||
<FullPageEmptyState title="Not found" className="min-h-screen">
|
||||
The organization you were looking for could not be found.
|
||||
|
||||
@@ -53,6 +53,9 @@ export default function CreateInvite({ projects }: Props) {
|
||||
closeSheet();
|
||||
router.refresh();
|
||||
},
|
||||
onError() {
|
||||
toast.error('Failed to invite user');
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { Dot } from '@/components/dot';
|
||||
import { TooltipComplete } from '@/components/tooltip-complete';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -20,9 +19,9 @@ import {
|
||||
} from '@/components/ui/table';
|
||||
import { Widget, WidgetHead } from '@/components/widget';
|
||||
import { api } from '@/trpc/client';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { MoreHorizontalIcon } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { pathOr } from 'ramda';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import type { IServiceInvite, IServiceProject } from '@openpanel/db';
|
||||
@@ -44,10 +43,9 @@ const Invites = ({ invites, projects }: Props) => {
|
||||
<Table className="mini">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Mail</TableHead>
|
||||
<TableHead>Role</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Access</TableHead>
|
||||
<TableHead>More</TableHead>
|
||||
</TableRow>
|
||||
@@ -66,18 +64,9 @@ interface ItemProps extends IServiceInvite {
|
||||
projects: IServiceProject[];
|
||||
}
|
||||
|
||||
function Item({
|
||||
id,
|
||||
email,
|
||||
role,
|
||||
createdAt,
|
||||
projects,
|
||||
publicMetadata,
|
||||
status,
|
||||
organizationId,
|
||||
}: ItemProps) {
|
||||
function Item({ id, email, role, createdAt, projects, meta }: ItemProps) {
|
||||
const router = useRouter();
|
||||
const access = (publicMetadata?.access ?? []) as string[];
|
||||
const access = pathOr<string[]>([], ['access'], meta);
|
||||
const revoke = api.organization.revokeInvite.useMutation({
|
||||
onSuccess() {
|
||||
toast.success(`Invite for ${email} revoked`);
|
||||
@@ -96,17 +85,6 @@ function Item({
|
||||
{new Date(createdAt).toLocaleDateString()}
|
||||
</TooltipComplete>
|
||||
</TableCell>
|
||||
<TableCell className="flex items-center gap-2 capitalize">
|
||||
<Dot
|
||||
className={cn(
|
||||
status === 'accepted' && 'bg-emerald-600',
|
||||
status === 'revoked' && 'bg-red-600',
|
||||
status === 'pending' && 'bg-orange-600'
|
||||
)}
|
||||
animated={status === 'pending'}
|
||||
/>
|
||||
{status}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{access.map((id) => {
|
||||
const project = projects.find((p) => p.id === id);
|
||||
@@ -136,7 +114,7 @@ function Item({
|
||||
<DropdownMenuItem
|
||||
className="text-destructive"
|
||||
onClick={() => {
|
||||
revoke.mutate({ organizationId, invitationId: id });
|
||||
revoke.mutate({ memberId: id });
|
||||
}}
|
||||
>
|
||||
Revoke invite
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { TooltipComplete } from '@/components/tooltip-complete';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
|
||||
import {
|
||||
@@ -62,10 +63,10 @@ interface ItemProps extends IServiceMember {
|
||||
|
||||
function Item({
|
||||
id,
|
||||
name,
|
||||
user,
|
||||
role,
|
||||
organizationId,
|
||||
createdAt,
|
||||
organization,
|
||||
projects,
|
||||
access: prevAccess,
|
||||
}: ItemProps) {
|
||||
@@ -73,22 +74,35 @@ function Item({
|
||||
const mutation = api.organization.updateMemberAccess.useMutation();
|
||||
const revoke = api.organization.removeMember.useMutation({
|
||||
onSuccess() {
|
||||
toast.success(`${name} has been removed from the organization`);
|
||||
toast.success(
|
||||
`${user?.firstName} has been removed from the organization`
|
||||
);
|
||||
router.refresh();
|
||||
},
|
||||
onError() {
|
||||
toast.error(`Failed to remove ${name} from the organization`);
|
||||
toast.error(`Failed to remove ${user?.firstName} from the organization`);
|
||||
},
|
||||
});
|
||||
const [access, setAccess] = useState<string[]>(
|
||||
prevAccess.map((item) => item.projectId)
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow key={id}>
|
||||
<TableCell className="font-medium">{name}</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
<div>{[user?.firstName, user?.lastName].filter(Boolean).join(' ')}</div>
|
||||
<div className="text-sm text-muted-foreground">{user?.email}</div>
|
||||
</TableCell>
|
||||
<TableCell>{role}</TableCell>
|
||||
<TableCell>{new Date(createdAt).toLocaleString()}</TableCell>
|
||||
<TableCell>
|
||||
<TooltipComplete content={new Date(createdAt).toLocaleString()}>
|
||||
{new Date(createdAt).toLocaleDateString()}
|
||||
</TooltipComplete>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ComboboxAdvanced
|
||||
placeholder="Restrict access to projects"
|
||||
@@ -96,8 +110,8 @@ function Item({
|
||||
onChange={(newAccess) => {
|
||||
setAccess(newAccess);
|
||||
mutation.mutate({
|
||||
userId: id!,
|
||||
organizationSlug: organization.slug,
|
||||
userId: user.id,
|
||||
organizationSlug: organizationId,
|
||||
access: newAccess as string[],
|
||||
});
|
||||
}}
|
||||
@@ -116,7 +130,7 @@ function Item({
|
||||
<DropdownMenuItem
|
||||
className="text-destructive"
|
||||
onClick={() => {
|
||||
revoke.mutate({ organizationId: organization.id, userId: id! });
|
||||
revoke.mutate({ organizationId: organizationId, userId: id });
|
||||
}}
|
||||
>
|
||||
Remove member
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import PageLayout from '@/app/(app)/[organizationSlug]/[projectId]/page-layout';
|
||||
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
||||
import { auth, clerkClient } from '@clerk/nextjs/server';
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
import { ShieldAlertIcon } from 'lucide-react';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getOrganizationBySlug } from '@openpanel/db';
|
||||
import { db } from '@openpanel/db';
|
||||
|
||||
import EditOrganization from './edit-organization';
|
||||
import InvitesServer from './invites';
|
||||
@@ -19,25 +19,30 @@ interface PageProps {
|
||||
export default async function Page({
|
||||
params: { organizationSlug },
|
||||
}: PageProps) {
|
||||
const organization = await getOrganizationBySlug(organizationSlug);
|
||||
const session = auth();
|
||||
const memberships = await clerkClient.users.getOrganizationMembershipList({
|
||||
userId: session.userId!,
|
||||
const organization = await db.organization.findUnique({
|
||||
where: {
|
||||
id: organizationSlug,
|
||||
members: {
|
||||
some: {
|
||||
userId: session.userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
members: {
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const member = memberships.data.find(
|
||||
(membership) => membership.organization.id === organization.id
|
||||
);
|
||||
|
||||
if (!member) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const hasAccess = member.role === 'org:admin';
|
||||
const hasAccess = organization.members[0]?.role === 'org:admin';
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -20,9 +20,7 @@ export default async function Page({
|
||||
getCurrentProjects(organizationSlug),
|
||||
]);
|
||||
|
||||
const organization = organizations.find(
|
||||
(org) => org.slug === organizationSlug
|
||||
);
|
||||
const organization = organizations.find((org) => org.id === organizationSlug);
|
||||
|
||||
if (!organization) {
|
||||
return (
|
||||
|
||||
@@ -6,7 +6,7 @@ export default async function Page() {
|
||||
const organizations = await getCurrentOrganizations();
|
||||
|
||||
if (organizations.length > 0) {
|
||||
return redirect(`/${organizations[0]?.slug}`);
|
||||
return redirect(`/${organizations[0]?.id}`);
|
||||
}
|
||||
|
||||
return redirect('/onboarding');
|
||||
|
||||
@@ -12,7 +12,7 @@ type Props = {
|
||||
|
||||
const Connect = async ({ params: { projectId } }: Props) => {
|
||||
const orgs = await getCurrentOrganizations();
|
||||
const organizationSlug = orgs[0]?.slug;
|
||||
const organizationSlug = orgs[0]?.id;
|
||||
if (!organizationSlug) {
|
||||
throw new Error('No organization found');
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ type Props = {
|
||||
|
||||
const Verify = async ({ params: { projectId } }: Props) => {
|
||||
const orgs = await getCurrentOrganizations();
|
||||
const organizationSlug = orgs[0]?.slug;
|
||||
const organizationSlug = orgs[0]?.id;
|
||||
if (!organizationSlug) {
|
||||
throw new Error('No organization found');
|
||||
}
|
||||
|
||||
@@ -112,10 +112,10 @@ const Tracking = ({
|
||||
value={field.value}
|
||||
items={
|
||||
organizations
|
||||
.filter((item) => item.slug)
|
||||
.filter((item) => item.id)
|
||||
.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.slug,
|
||||
value: item.id,
|
||||
})) ?? []
|
||||
}
|
||||
onChange={field.onChange}
|
||||
|
||||
@@ -1,10 +1,64 @@
|
||||
import type { WebhookEvent } from '@clerk/nextjs/server';
|
||||
import { pathOr } from 'ramda';
|
||||
|
||||
import { AccessLevel, db } from '@openpanel/db';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const payload: WebhookEvent = await request.json();
|
||||
|
||||
if (payload.type === 'user.created') {
|
||||
const email = payload.data.email_addresses[0]?.email_address;
|
||||
|
||||
if (!email) {
|
||||
return Response.json(
|
||||
{ message: 'No email address found' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const user = await db.user.create({
|
||||
data: {
|
||||
id: payload.data.id,
|
||||
email,
|
||||
firstName: payload.data.first_name,
|
||||
lastName: payload.data.last_name,
|
||||
},
|
||||
});
|
||||
|
||||
const memberships = await db.member.findMany({
|
||||
where: {
|
||||
email,
|
||||
userId: null,
|
||||
},
|
||||
});
|
||||
|
||||
for (const membership of memberships) {
|
||||
const access = pathOr<string[]>([], ['meta', 'access'], membership);
|
||||
db.$transaction([
|
||||
// Update the member to link it to the user
|
||||
// This will remove the item from invitations
|
||||
db.member.update({
|
||||
where: {
|
||||
id: membership.id,
|
||||
},
|
||||
data: {
|
||||
userId: user.id,
|
||||
},
|
||||
}),
|
||||
db.projectAccess.createMany({
|
||||
data: access
|
||||
.filter((a) => typeof a === 'string')
|
||||
.map((projectId) => ({
|
||||
organizationSlug: membership.organizationId,
|
||||
projectId: projectId,
|
||||
userId: user.id,
|
||||
level: AccessLevel.read,
|
||||
})),
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.type === 'organizationMembership.created') {
|
||||
const access = payload.data.public_metadata.access;
|
||||
if (Array.isArray(access)) {
|
||||
@@ -12,7 +66,7 @@ export async function POST(request: Request) {
|
||||
data: access
|
||||
.filter((a): a is string => typeof a === 'string')
|
||||
.map((projectId) => ({
|
||||
organizationSlug: payload.data.organization.slug!,
|
||||
organizationSlug: payload.data.organization.slug,
|
||||
projectId: projectId,
|
||||
userId: payload.data.public_user_data.user_id,
|
||||
level: AccessLevel.read,
|
||||
@@ -20,10 +74,37 @@ export async function POST(request: Request) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.type === 'user.deleted') {
|
||||
db.$transaction([
|
||||
db.user.update({
|
||||
where: {
|
||||
id: payload.data.id,
|
||||
},
|
||||
data: {
|
||||
deletedAt: new Date(),
|
||||
firstName: null,
|
||||
lastName: null,
|
||||
email: `deleted+${payload.data.id}@openpanel.dev`,
|
||||
},
|
||||
}),
|
||||
db.projectAccess.deleteMany({
|
||||
where: {
|
||||
userId: payload.data.id,
|
||||
},
|
||||
}),
|
||||
db.member.deleteMany({
|
||||
where: {
|
||||
userId: payload.data.id,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
if (payload.type === 'organizationMembership.deleted') {
|
||||
await db.projectAccess.deleteMany({
|
||||
where: {
|
||||
organizationSlug: payload.data.organization.slug!,
|
||||
organizationSlug: payload.data.organization.slug,
|
||||
userId: payload.data.public_user_data.user_id,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "projects" ADD COLUMN "organizationId" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "organizations" (
|
||||
"id" TEXT NOT NULL DEFAULT gen_random_uuid(),
|
||||
"name" TEXT NOT NULL,
|
||||
"createdByUserId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "organizations_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "users" (
|
||||
"id" TEXT NOT NULL DEFAULT gen_random_uuid(),
|
||||
"email" TEXT NOT NULL,
|
||||
"first_name" TEXT,
|
||||
"last_name" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "members" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"role" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"userId" TEXT,
|
||||
"organizationId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "members_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "organizations" ADD CONSTRAINT "organizations_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "members" ADD CONSTRAINT "members_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "members" ADD CONSTRAINT "members_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "projects" ADD CONSTRAINT "projects_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `first_name` on the `users` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `last_name` on the `users` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "users" DROP COLUMN "first_name",
|
||||
DROP COLUMN "last_name",
|
||||
ADD COLUMN "firstName" TEXT,
|
||||
ADD COLUMN "lastName" TEXT;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "members" ADD COLUMN "invitedById" TEXT,
|
||||
ADD COLUMN "meta" JSONB;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "members" ADD CONSTRAINT "members_invitedById_fkey" FOREIGN KEY ("invitedById") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "users" ADD COLUMN "deletedAt" TIMESTAMP(3);
|
||||
@@ -0,0 +1,23 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "clients" ADD COLUMN "organizationId" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "dashboards" ADD COLUMN "organizationId" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "project_access" ADD COLUMN "organizationId" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "shares" ADD COLUMN "organizationId" TEXT;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "project_access" ADD CONSTRAINT "project_access_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "clients" ADD CONSTRAINT "clients_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "dashboards" ADD CONSTRAINT "dashboards_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "shares" ADD CONSTRAINT "shares_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "project_access" ADD CONSTRAINT "project_access_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -17,10 +17,63 @@ enum ProjectType {
|
||||
backend
|
||||
}
|
||||
|
||||
model Organization {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()"))
|
||||
name String
|
||||
projects Project[]
|
||||
members Member[]
|
||||
createdByUserId String?
|
||||
createdBy User? @relation(fields: [createdByUserId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
ProjectAccess ProjectAccess[]
|
||||
Client Client[]
|
||||
Dashboard Dashboard[]
|
||||
ShareOverview ShareOverview[]
|
||||
|
||||
@@map("organizations")
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()"))
|
||||
email String @unique
|
||||
firstName String?
|
||||
lastName String?
|
||||
createdOrganizations Organization[]
|
||||
membership Member[]
|
||||
sentInvites Member[] @relation("invitedBy")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
deletedAt DateTime?
|
||||
ProjectAccess ProjectAccess[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model Member {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
role String
|
||||
email String
|
||||
// userId is nullable because we want to allow invites to be sent to emails that are not registered
|
||||
userId String?
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
invitedById String?
|
||||
invitedBy User? @relation("invitedBy", fields: [invitedById], references: [id])
|
||||
organizationId String
|
||||
organization Organization @relation(fields: [organizationId], references: [id])
|
||||
meta Json?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
|
||||
@@map("members")
|
||||
}
|
||||
|
||||
model Project {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()"))
|
||||
name String
|
||||
organizationSlug String
|
||||
organization Organization? @relation(fields: [organizationId], references: [id])
|
||||
organizationId String?
|
||||
eventsCount Int @default(0)
|
||||
types ProjectType[] @default([])
|
||||
|
||||
@@ -47,14 +100,17 @@ enum AccessLevel {
|
||||
}
|
||||
|
||||
model ProjectAccess {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
projectId String
|
||||
project Project @relation(fields: [projectId], references: [id])
|
||||
project Project @relation(fields: [projectId], references: [id])
|
||||
organizationSlug String
|
||||
organization Organization? @relation(fields: [organizationId], references: [id])
|
||||
organizationId String?
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
level AccessLevel
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
|
||||
@@map("project_access")
|
||||
}
|
||||
@@ -105,13 +161,15 @@ enum ClientType {
|
||||
}
|
||||
|
||||
model Client {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
name String
|
||||
secret String?
|
||||
type ClientType @default(write)
|
||||
type ClientType @default(write)
|
||||
projectId String?
|
||||
project Project? @relation(fields: [projectId], references: [id])
|
||||
project Project? @relation(fields: [projectId], references: [id])
|
||||
organizationSlug String
|
||||
organization Organization? @relation(fields: [organizationId], references: [id])
|
||||
organizationId String?
|
||||
cors String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
@@ -139,11 +197,13 @@ enum ChartType {
|
||||
}
|
||||
|
||||
model Dashboard {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()"))
|
||||
id String @id @default(dbgenerated("gen_random_uuid()"))
|
||||
name String
|
||||
organizationSlug String
|
||||
organization Organization? @relation(fields: [organizationId], references: [id])
|
||||
organizationId String?
|
||||
projectId String
|
||||
project Project @relation(fields: [projectId], references: [id])
|
||||
project Project @relation(fields: [projectId], references: [id])
|
||||
reports Report[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
@@ -195,14 +255,16 @@ model Waitlist {
|
||||
}
|
||||
|
||||
model ShareOverview {
|
||||
id String @unique
|
||||
projectId String @unique
|
||||
project Project @relation(fields: [projectId], references: [id])
|
||||
id String @unique
|
||||
projectId String @unique
|
||||
project Project @relation(fields: [projectId], references: [id])
|
||||
organizationSlug String
|
||||
public Boolean @default(false)
|
||||
organization Organization? @relation(fields: [organizationId], references: [id])
|
||||
organizationId String?
|
||||
public Boolean @default(false)
|
||||
password String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
|
||||
@@map("shares")
|
||||
}
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
import type {
|
||||
Organization,
|
||||
OrganizationInvitation,
|
||||
OrganizationMembership,
|
||||
} from '@clerk/nextjs/dist/types/server';
|
||||
import { auth, clerkClient } from '@clerk/nextjs/server';
|
||||
import { sort } from 'ramda';
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
|
||||
import type { ProjectAccess } from '../prisma-client';
|
||||
import type { Organization, Prisma, ProjectAccess } from '../prisma-client';
|
||||
import { db } from '../prisma-client';
|
||||
|
||||
export type IServiceOrganization = ReturnType<typeof transformOrganization>;
|
||||
export type IServiceInvite = ReturnType<typeof transformInvite>;
|
||||
export type IServiceMember = ReturnType<typeof transformMember>;
|
||||
export type IServiceInvite = Prisma.MemberGetPayload<{
|
||||
include: { user: true };
|
||||
}>;
|
||||
export type IServiceMember = Prisma.MemberGetPayload<{
|
||||
include: { user: true };
|
||||
}> & { access: ProjectAccess[] };
|
||||
export type IServiceProjectAccess = ProjectAccess;
|
||||
|
||||
export function transformOrganization(org: Organization) {
|
||||
return {
|
||||
id: org.id,
|
||||
slug: org.id,
|
||||
name: org.name,
|
||||
slug: org.slug!,
|
||||
createdAt: org.createdAt,
|
||||
};
|
||||
}
|
||||
@@ -26,20 +24,28 @@ export function transformOrganization(org: Organization) {
|
||||
export async function getCurrentOrganizations() {
|
||||
const session = auth();
|
||||
if (!session.userId) return [];
|
||||
const organizations = await clerkClient.users.getOrganizationMembershipList({
|
||||
userId: session.userId,
|
||||
const organizations = await db.organization.findMany({
|
||||
where: {
|
||||
members: {
|
||||
some: {
|
||||
userId: session.userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
return sort(
|
||||
(a, b) => a.createdAt - b.createdAt,
|
||||
organizations.data.map((item) => transformOrganization(item.organization))
|
||||
);
|
||||
|
||||
return organizations.map(transformOrganization);
|
||||
}
|
||||
|
||||
export function getOrganizationBySlug(slug: string) {
|
||||
return clerkClient.organizations
|
||||
.getOrganization({ slug })
|
||||
.then(transformOrganization)
|
||||
.catch(() => null);
|
||||
return db.organization.findUnique({
|
||||
where: {
|
||||
id: slug,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getOrganizationByProjectId(projectId: string) {
|
||||
@@ -47,63 +53,42 @@ export async function getOrganizationByProjectId(projectId: string) {
|
||||
where: {
|
||||
id: projectId,
|
||||
},
|
||||
include: {
|
||||
organization: true,
|
||||
},
|
||||
});
|
||||
|
||||
return clerkClient.organizations.getOrganization({
|
||||
slug: project.organizationSlug,
|
||||
});
|
||||
}
|
||||
if (!project.organization) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
return transformOrganization(project.organization);
|
||||
}
|
||||
|
||||
export async function getInvites(organizationSlug: string) {
|
||||
const org = await getOrganizationBySlug(organizationSlug);
|
||||
if (!org) return [];
|
||||
return await clerkClient.organizations
|
||||
.getOrganizationInvitationList({
|
||||
organizationId: org.id,
|
||||
})
|
||||
.then((invites) => invites.data.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,
|
||||
};
|
||||
return db.member.findMany({
|
||||
where: {
|
||||
organizationId: organizationSlug,
|
||||
userId: null,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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.member.findMany({
|
||||
where: {
|
||||
organizationId: organizationSlug,
|
||||
userId: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
}),
|
||||
db.projectAccess.findMany({
|
||||
where: {
|
||||
@@ -112,22 +97,17 @@ export async function getMembers(organizationSlug: string) {
|
||||
}),
|
||||
]);
|
||||
|
||||
return members.data
|
||||
.map((member) => {
|
||||
const projectAccess = access.filter(
|
||||
(item) => item.userId === member.publicUserData?.userId
|
||||
);
|
||||
return {
|
||||
...member,
|
||||
access: projectAccess,
|
||||
};
|
||||
})
|
||||
.map(transformMember);
|
||||
return members.map((member) => ({
|
||||
...member,
|
||||
access: access.filter((a) => a.userId === member.userId),
|
||||
}));
|
||||
}
|
||||
|
||||
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;
|
||||
return db.member.findFirst({
|
||||
where: {
|
||||
organizationId: organizationSlug,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -58,15 +58,34 @@ export async function getCurrentProjects(organizationSlug: string) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return db.project.findMany({
|
||||
where: {
|
||||
organizationSlug,
|
||||
},
|
||||
include: {
|
||||
access: true,
|
||||
},
|
||||
orderBy: {
|
||||
eventsCount: 'desc',
|
||||
},
|
||||
});
|
||||
const [projects, members, access] = await Promise.all([
|
||||
db.project.findMany({
|
||||
where: {
|
||||
organizationSlug,
|
||||
},
|
||||
}),
|
||||
db.member.findMany({
|
||||
where: {
|
||||
userId: session.userId,
|
||||
organizationId: organizationSlug,
|
||||
},
|
||||
}),
|
||||
db.projectAccess.findMany({
|
||||
where: {
|
||||
userId: session.userId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
if (members.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (access.length > 0) {
|
||||
return projects.filter((project) =>
|
||||
access.some((a) => a.projectId === project.id)
|
||||
);
|
||||
}
|
||||
|
||||
return projects;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,7 @@
|
||||
import type { User } from '@clerk/nextjs/dist/types/server';
|
||||
import { auth, clerkClient } from '@clerk/nextjs/server';
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
|
||||
import { db } from '../prisma-client';
|
||||
|
||||
export function transformUser(user: User) {
|
||||
return {
|
||||
name: `${user.firstName} ${user.lastName}`,
|
||||
email: user.emailAddresses[0]?.emailAddress ?? '',
|
||||
id: user.id,
|
||||
lastName: user.lastName ?? '',
|
||||
firstName: user.firstName ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
export async function getCurrentUser() {
|
||||
const session = auth();
|
||||
if (!session.userId) {
|
||||
@@ -22,20 +11,9 @@ export async function getCurrentUser() {
|
||||
}
|
||||
|
||||
export async function getUserById(id: string) {
|
||||
return clerkClient.users.getUser(id).then(transformUser);
|
||||
}
|
||||
|
||||
export async function isWaitlistUserAccepted() {
|
||||
const user = await getCurrentUser();
|
||||
const waitlist = await db.waitlist.findFirst({
|
||||
return db.user.findUnique({
|
||||
where: {
|
||||
email: user?.email,
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!waitlist) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return waitlist.accepted;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user