Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/app/languine.lock
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,11 @@ files:
settings.team.invite.button.sent: f98552d4b87007da82818937e16becb1
settings.team.invite.toast.error: 4e7e65d5a93d3037b3dda6f2f485f809
settings.team.invite.toast.unexpected: 48640555cac27f997e813e57b2567719
settings.team.invite.error.title: b3ae1177e33927d873811dab64b96b5f
settings.team.invite.error.description: 3fe295d3d0645eb12d2540f51ee87322
settings.team.invite.error.default: 05fc634188165901fca91a73958314be
settings.team.invite.error.home: d78de1a1ae35d774b5f97ccc434d0697
settings.team.invite.error.signin: f2fdea440d768b85591e936a803c3631
settings.team.member_actions.actions: 06df33001c1d7187fdd81ea1f5b277aa
settings.team.member_actions.change_role: 092c25021695408aaa0a250ca5c6489c
settings.team.member_actions.remove_member: 5f76745afd9e5ee8676f8949544bfb1a
Expand Down
292 changes: 126 additions & 166 deletions apps/app/src/actions/organization/accept-invitation.ts
Original file line number Diff line number Diff line change
@@ -1,178 +1,138 @@
"use server";

import { db } from "@bubba/db";
import { authActionClient } from "../safe-action";
import { revalidatePath, revalidateTag } from "next/cache";
import { z } from "zod";
import { authActionClient } from "../safe-action";
import type { ActionResponse } from "../types";
import { createSafeActionClient } from "next-safe-action";
import { revalidatePath, revalidateTag } from "next/cache";

const inviteCodeSchema = z.object({
inviteCode: z.string(),
});

async function findMemberByInviteCode(inviteCode: string) {
const pendingInvitation = await db.organizationMember.findFirst({
where: {
accepted: false,
invitedEmail: { not: null },
inviteCode: inviteCode,
},
include: {
organization: {
select: {
id: true,
name: true,
},
},
},
});

return pendingInvitation;
async function validateInviteCode(inviteCode: string, invitedEmail: string) {
const pendingInvitation = await db.organizationMember.findFirst({
where: {
accepted: false,
invitedEmail,
inviteCode,
},
include: {
organization: {
select: {
id: true,
name: true,
},
},
},
});

return pendingInvitation;
}

const publicActionClient = createSafeActionClient();

export const acceptInvitation = publicActionClient
.schema(inviteCodeSchema)
.action(
async ({
parsedInput,
}): Promise<
ActionResponse<{ accepted: boolean; organizationId: string | null }>
> => {
const { inviteCode } = parsedInput;

try {
const invitation = await findMemberByInviteCode(inviteCode);

if (!invitation) {
return {
success: false,
error: "Invitation not found or has already been accepted",
};
}

return {
success: true,
data: {
accepted: false,
organizationId: invitation.organization.id,
},
};
} catch (error) {
console.error("Error validating invitation:", error);
return {
success: false,
error: "Failed to validate invitation",
};
}
},
);

const completeInvitationSchema = z.object({
inviteCode: z.string(),
userId: z.string(),
inviteCode: z.string(),
});

export const completeInvitation = authActionClient
.metadata({
name: "complete-invitation",
track: {
event: "complete_invitation",
channel: "organization",
},
})
.schema(completeInvitationSchema)
.action(
async ({
parsedInput,
ctx,
}): Promise<
ActionResponse<{ accepted: boolean; organizationId: string }>
> => {
const { inviteCode, userId } = parsedInput;

try {
const invitation = await findMemberByInviteCode(inviteCode);

if (!invitation) {
return {
success: false,
error: "Invitation not found or has already been accepted",
};
}

const existingMembership = await db.organizationMember.findFirst({
where: {
userId,
organizationId: invitation.organizationId,
},
});

if (existingMembership) {
const user = await db.user.findUnique({ where: { id: userId } });
if (!user?.organizationId) {
await db.user.update({
where: { id: userId },
data: {
organizationId: invitation.organizationId,
},
});

await db.organizationMember.update({
where: { id: existingMembership.id },
data: {
accepted: true,
},
});
}

return {
success: true,
data: {
accepted: true,
organizationId: invitation.organizationId,
},
};
}

await db.organizationMember.update({
where: {
id: invitation.id,
},
data: {
accepted: true,
userId,
invitedEmail: null,
},
});

await db.user.update({
where: {
id: userId,
},
data: {
organizationId: invitation.organizationId,
},
});

revalidatePath("/settings/members");
revalidateTag(`user_${userId}`);

return {
success: true,
data: {
accepted: true,
organizationId: invitation.organizationId,
},
};
} catch (error) {
console.error("Error accepting invitation:", error);
return {
success: false,
error: "Failed to accept invitation",
};
}
},
);
.metadata({
name: "complete-invitation",
track: {
event: "complete_invitation",
channel: "organization",
},
})
.schema(completeInvitationSchema)
.action(
async ({
parsedInput,
ctx,
}): Promise<
ActionResponse<{
accepted: boolean;
organizationId: string;
}>
> => {
const { inviteCode } = parsedInput;
const user = ctx.user;

if (!user || !user.email) {
throw new Error("Unauthorized");
}

try {
const invitation = await validateInviteCode(inviteCode, user.email);

if (!invitation) {
throw new Error("Invitation either used or expired");
}

const existingMembership = await db.organizationMember.findFirst({
where: {
userId: user.id,
organizationId: invitation.organizationId,
},
});

if (existingMembership) {
if (user.organizationId !== invitation.organizationId) {
await db.user.update({
where: { id: user.id },
data: {
organizationId: invitation.organizationId,
},
});

await db.organizationMember.update({
where: { id: existingMembership.id },
data: {
accepted: true,
invitedEmail: null,
inviteCode: null,
},
});
}

return {
success: true,
data: {
accepted: true,
organizationId: invitation.organizationId,
},
};
}

await db.organizationMember.update({
where: {
id: invitation.id,
},
data: {
accepted: true,
userId: user.id,
invitedEmail: null,
inviteCode: null,
},
});

await db.user.update({
where: {
id: user.id,
},
data: {
organizationId: invitation.organizationId,
},
});

revalidatePath(`/${invitation.organization.id}`);
revalidatePath(`/${invitation.organization.id}/settings/members`);
revalidateTag(`user_${user.id}`);

return {
success: true,
data: {
accepted: true,
organizationId: invitation.organizationId,
},
};
} catch (error) {
console.error("Error accepting invitation:", error);
throw new Error(error as string);
}
}
);
Loading