Skip to content

Commit 3ea5c1f

Browse files
fix(app): invite the deactivated user again as admin (#1844)
Co-authored-by: chasprowebdev <[email protected]>
1 parent 2083c5e commit 3ea5c1f

File tree

4 files changed

+219
-6
lines changed

4 files changed

+219
-6
lines changed

apps/app/src/actions/organization/accept-invitation.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ export const completeInvitation = authActionClientWithoutOrg
6969
where: {
7070
userId: user.id,
7171
organizationId: invitation.organizationId,
72-
deactivated: false,
7372
},
7473
});
7574

@@ -90,6 +89,16 @@ export const completeInvitation = authActionClientWithoutOrg
9089
},
9190
});
9291

92+
if (existingMembership.deactivated) {
93+
await db.member.update({
94+
where: { id: existingMembership.id },
95+
data: {
96+
deactivated: false,
97+
role: invitation.role,
98+
},
99+
});
100+
}
101+
93102
// Server redirect to the organization's root
94103
redirect(`/${invitation.organizationId}/`);
95104
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
'use server';
2+
3+
import { auth } from '@/utils/auth';
4+
import { db } from '@db';
5+
import { headers } from 'next/headers';
6+
7+
export const checkMemberStatus = async ({
8+
email,
9+
organizationId,
10+
}: {
11+
email: string;
12+
organizationId: string;
13+
}) => {
14+
try {
15+
const session = await auth.api.getSession({ headers: await headers() });
16+
if (!session?.session) {
17+
throw new Error('Authentication required.');
18+
}
19+
20+
const currentUserId = session.session.userId;
21+
const currentUserMember = await db.member.findFirst({
22+
where: {
23+
organizationId: organizationId,
24+
userId: currentUserId,
25+
deactivated: false,
26+
},
27+
});
28+
29+
if (
30+
!currentUserMember ||
31+
(!currentUserMember.role.includes('admin') && !currentUserMember.role.includes('owner'))
32+
) {
33+
throw new Error("You don't have permission to reactivate members.");
34+
}
35+
36+
// Find the user by email
37+
const user = await db.user.findFirst({
38+
where: {
39+
email: {
40+
equals: email,
41+
mode: 'insensitive',
42+
},
43+
},
44+
});
45+
46+
if (!user) {
47+
// User doesn't exist yet
48+
return { success: true, memberExists: false, isActive: false, reactivated: false };
49+
}
50+
51+
// Check if there's a member for this user and organization (active or deactivated)
52+
const existingMember = await db.member.findFirst({
53+
where: {
54+
userId: user.id,
55+
organizationId,
56+
},
57+
});
58+
59+
if (!existingMember) {
60+
// Member doesn't exist
61+
return { success: true, memberExists: false, isActive: false, reactivated: false };
62+
}
63+
64+
if (existingMember.deactivated) {
65+
return {
66+
success: true,
67+
memberExists: true,
68+
isActive: true,
69+
reactivated: true,
70+
memberId: existingMember.id,
71+
};
72+
}
73+
74+
// Member exists and is already active
75+
return {
76+
success: true,
77+
memberExists: true,
78+
isActive: true,
79+
reactivated: false,
80+
memberId: existingMember.id,
81+
};
82+
} catch (error) {
83+
console.error('Error checking member status:', error);
84+
throw error;
85+
}
86+
};
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
'use server';
2+
3+
import { auth } from '@/utils/auth';
4+
import { sendInviteMemberEmail } from '@comp/email/lib/invite-member';
5+
import { db } from '@db';
6+
import { headers } from 'next/headers';
7+
8+
export const sendInvitationEmailToExistingMember = async ({
9+
email,
10+
organizationId,
11+
roles,
12+
}: {
13+
email: string;
14+
organizationId: string;
15+
roles: string[];
16+
}) => {
17+
try {
18+
const session = await auth.api.getSession({ headers: await headers() });
19+
if (!session?.session) {
20+
throw new Error('Authentication required.');
21+
}
22+
23+
const currentUserId = session.session.userId;
24+
const currentUserMember = await db.member.findFirst({
25+
where: {
26+
organizationId: organizationId,
27+
userId: currentUserId,
28+
deactivated: false,
29+
},
30+
});
31+
32+
if (
33+
!currentUserMember ||
34+
(!currentUserMember.role.includes('admin') && !currentUserMember.role.includes('owner'))
35+
) {
36+
throw new Error("You don't have permission to send invitations.");
37+
}
38+
39+
// Get organization name
40+
const organization = await db.organization.findUnique({
41+
where: { id: organizationId },
42+
select: { name: true },
43+
});
44+
45+
if (!organization) {
46+
throw new Error('Organization not found.');
47+
}
48+
49+
// Generate invitation using Better Auth
50+
// Note: This might fail if member already exists, so we'll create invitation manually
51+
const invitation = await db.invitation.create({
52+
data: {
53+
email: email.toLowerCase(),
54+
organizationId,
55+
role: roles.length === 1 ? roles[0] : roles.join(','),
56+
status: 'pending',
57+
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
58+
inviterId: currentUserId,
59+
},
60+
});
61+
62+
// Generate invite link
63+
const isLocalhost = process.env.NODE_ENV === 'development';
64+
const protocol = isLocalhost ? 'http' : 'https';
65+
66+
const betterAuthUrl = process.env.NEXT_PUBLIC_BETTER_AUTH_URL;
67+
const isDevEnv = betterAuthUrl?.includes('dev.trycomp.ai');
68+
const isProdEnv = betterAuthUrl?.includes('app.trycomp.ai');
69+
70+
const domain = isDevEnv ? 'dev.trycomp.ai' : isProdEnv ? 'app.trycomp.ai' : 'localhost:3000';
71+
const inviteLink = `${protocol}://${domain}/invite/${invitation.id}`;
72+
73+
// Send the invitation email
74+
await sendInviteMemberEmail({
75+
inviteeEmail: email.toLowerCase(),
76+
inviteLink,
77+
organizationName: organization.name,
78+
});
79+
80+
return { success: true };
81+
} catch (error) {
82+
console.error('Error sending invitation email:', error);
83+
throw error;
84+
}
85+
};

apps/app/src/app/(app)/[orgId]/people/all/components/InviteMembersModal.tsx

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import {
3232
import { Input } from '@comp/ui/input';
3333
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@comp/ui/tabs';
3434
import { addEmployeeWithoutInvite } from '../actions/addEmployeeWithoutInvite';
35+
import { checkMemberStatus } from '../actions/checkMemberStatus';
36+
import { sendInvitationEmailToExistingMember } from '../actions/sendInvitationEmail';
3537
import { MultiRoleCombobox } from './MultiRoleCombobox';
3638

3739
// --- Constants for Roles ---
@@ -169,11 +171,26 @@ export function InviteMembersModal({
169171
roles: invite.roles,
170172
});
171173
} else {
172-
// Use authClient to send the invitation
173-
await authClient.organization.inviteMember({
174+
// Check member status and reactivate if needed
175+
const memberStatus = await checkMemberStatus({
174176
email: invite.email.toLowerCase(),
175-
role: invite.roles.length === 1 ? invite.roles[0] : invite.roles,
177+
organizationId,
176178
});
179+
180+
if (memberStatus.memberExists && memberStatus.isActive) {
181+
// Member already exists and is active - send invitation email manually
182+
await sendInvitationEmailToExistingMember({
183+
email: invite.email.toLowerCase(),
184+
organizationId,
185+
roles: invite.roles,
186+
});
187+
} else {
188+
// Member doesn't exist - use authClient to send the invitation
189+
await authClient.organization.inviteMember({
190+
email: invite.email.toLowerCase(),
191+
role: invite.roles.length === 1 ? invite.roles[0] : invite.roles,
192+
});
193+
}
177194
}
178195
successCount++;
179196
} catch (error) {
@@ -331,10 +348,26 @@ export function InviteMembersModal({
331348
roles: validRoles,
332349
});
333350
} else {
334-
await authClient.organization.inviteMember({
351+
// Check member status and reactivate if needed
352+
const memberStatus = await checkMemberStatus({
335353
email: email.toLowerCase(),
336-
role: validRoles,
354+
organizationId,
337355
});
356+
357+
if (memberStatus.memberExists && memberStatus.isActive) {
358+
// Member already exists and is active - send invitation email manually
359+
await sendInvitationEmailToExistingMember({
360+
email: email.toLowerCase(),
361+
organizationId,
362+
roles: validRoles,
363+
});
364+
} else {
365+
// Member doesn't exist - use authClient to send the invitation
366+
await authClient.organization.inviteMember({
367+
email: email.toLowerCase(),
368+
role: validRoles,
369+
});
370+
}
338371
}
339372
successCount++;
340373
} catch (error) {

0 commit comments

Comments
 (0)