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
11 changes: 10 additions & 1 deletion apps/app/src/actions/organization/accept-invitation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ export const completeInvitation = authActionClientWithoutOrg
where: {
userId: user.id,
organizationId: invitation.organizationId,
deactivated: false,
},
});

Expand All @@ -90,6 +89,16 @@ export const completeInvitation = authActionClientWithoutOrg
},
});

if (existingMembership.deactivated) {
await db.member.update({
where: { id: existingMembership.id },
data: {
deactivated: false,
role: invitation.role,
},
});
}

// Server redirect to the organization's root
redirect(`/${invitation.organizationId}/`);
}
Expand Down
26 changes: 26 additions & 0 deletions apps/app/src/actions/tasks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use server';

import { addYears } from 'date-fns';
import { createSafeActionClient } from 'next-safe-action';
import { cookies } from 'next/headers';
import { z } from 'zod';

const schema = z.object({
view: z.enum(['categories', 'list']),
orgId: z.string(),
});

export const updateTaskViewPreference = createSafeActionClient()
.inputSchema(schema)
.action(async ({ parsedInput }) => {
const cookieStore = await cookies();

cookieStore.set({
name: `task-view-preference-${parsedInput.orgId}`,
value: parsedInput.view,
expires: addYears(new Date(), 1),
});

return { success: true };
});

Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
'use server';

import { auth } from '@/utils/auth';
import { db } from '@db';
import { headers } from 'next/headers';

export const checkMemberStatus = async ({
email,
organizationId,
}: {
email: string;
organizationId: string;
}) => {
try {
const session = await auth.api.getSession({ headers: await headers() });
if (!session?.session) {
throw new Error('Authentication required.');
}

const currentUserId = session.session.userId;
const currentUserMember = await db.member.findFirst({
where: {
organizationId: organizationId,
userId: currentUserId,
deactivated: false,
},
});

if (
!currentUserMember ||
(!currentUserMember.role.includes('admin') && !currentUserMember.role.includes('owner'))
) {
throw new Error("You don't have permission to reactivate members.");
}

// Find the user by email
const user = await db.user.findFirst({
where: {
email: {
equals: email,
mode: 'insensitive',
},
},
});

if (!user) {
// User doesn't exist yet
return { success: true, memberExists: false, isActive: false, reactivated: false };
}

// Check if there's a member for this user and organization (active or deactivated)
const existingMember = await db.member.findFirst({
where: {
userId: user.id,
organizationId,
},
});

if (!existingMember) {
// Member doesn't exist
return { success: true, memberExists: false, isActive: false, reactivated: false };
}

if (existingMember.deactivated) {
return {
success: true,
memberExists: true,
isActive: true,
reactivated: true,
memberId: existingMember.id,
};
}

// Member exists and is already active
return {
success: true,
memberExists: true,
isActive: true,
reactivated: false,
memberId: existingMember.id,
};
} catch (error) {
console.error('Error checking member status:', error);
throw error;
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
'use server';

import { auth } from '@/utils/auth';
import { sendInviteMemberEmail } from '@comp/email/lib/invite-member';
import { db } from '@db';
import { headers } from 'next/headers';

export const sendInvitationEmailToExistingMember = async ({
email,
organizationId,
roles,
}: {
email: string;
organizationId: string;
roles: string[];
}) => {
try {
const session = await auth.api.getSession({ headers: await headers() });
if (!session?.session) {
throw new Error('Authentication required.');
}

const currentUserId = session.session.userId;
const currentUserMember = await db.member.findFirst({
where: {
organizationId: organizationId,
userId: currentUserId,
deactivated: false,
},
});

if (
!currentUserMember ||
(!currentUserMember.role.includes('admin') && !currentUserMember.role.includes('owner'))
) {
throw new Error("You don't have permission to send invitations.");
}

// Get organization name
const organization = await db.organization.findUnique({
where: { id: organizationId },
select: { name: true },
});

if (!organization) {
throw new Error('Organization not found.');
}

// Generate invitation using Better Auth
// Note: This might fail if member already exists, so we'll create invitation manually
const invitation = await db.invitation.create({
data: {
email: email.toLowerCase(),
organizationId,
role: roles.length === 1 ? roles[0] : roles.join(','),
status: 'pending',
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
inviterId: currentUserId,
},
});

// Generate invite link
const isLocalhost = process.env.NODE_ENV === 'development';
const protocol = isLocalhost ? 'http' : 'https';

const betterAuthUrl = process.env.NEXT_PUBLIC_BETTER_AUTH_URL;
const isDevEnv = betterAuthUrl?.includes('dev.trycomp.ai');
const isProdEnv = betterAuthUrl?.includes('app.trycomp.ai');

const domain = isDevEnv ? 'dev.trycomp.ai' : isProdEnv ? 'app.trycomp.ai' : 'localhost:3000';
const inviteLink = `${protocol}://${domain}/invite/${invitation.id}`;

// Send the invitation email
await sendInviteMemberEmail({
inviteeEmail: email.toLowerCase(),
inviteLink,
organizationName: organization.name,
});

return { success: true };
} catch (error) {
console.error('Error sending invitation email:', error);
throw error;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import {
import { Input } from '@comp/ui/input';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@comp/ui/tabs';
import { addEmployeeWithoutInvite } from '../actions/addEmployeeWithoutInvite';
import { checkMemberStatus } from '../actions/checkMemberStatus';
import { sendInvitationEmailToExistingMember } from '../actions/sendInvitationEmail';
import { MultiRoleCombobox } from './MultiRoleCombobox';

// --- Constants for Roles ---
Expand Down Expand Up @@ -169,11 +171,26 @@ export function InviteMembersModal({
roles: invite.roles,
});
} else {
// Use authClient to send the invitation
await authClient.organization.inviteMember({
// Check member status and reactivate if needed
const memberStatus = await checkMemberStatus({
email: invite.email.toLowerCase(),
role: invite.roles.length === 1 ? invite.roles[0] : invite.roles,
organizationId,
});

if (memberStatus.memberExists && memberStatus.isActive) {
// Member already exists and is active - send invitation email manually
await sendInvitationEmailToExistingMember({
email: invite.email.toLowerCase(),
organizationId,
roles: invite.roles,
});
} else {
// Member doesn't exist - use authClient to send the invitation
await authClient.organization.inviteMember({
email: invite.email.toLowerCase(),
role: invite.roles.length === 1 ? invite.roles[0] : invite.roles,
});
}
}
successCount++;
} catch (error) {
Expand Down Expand Up @@ -331,10 +348,26 @@ export function InviteMembersModal({
roles: validRoles,
});
} else {
await authClient.organization.inviteMember({
// Check member status and reactivate if needed
const memberStatus = await checkMemberStatus({
email: email.toLowerCase(),
role: validRoles,
organizationId,
});

if (memberStatus.memberExists && memberStatus.isActive) {
// Member already exists and is active - send invitation email manually
await sendInvitationEmailToExistingMember({
email: email.toLowerCase(),
organizationId,
roles: validRoles,
});
} else {
// Member doesn't exist - use authClient to send the invitation
await authClient.organization.inviteMember({
email: email.toLowerCase(),
role: validRoles,
});
}
}
successCount++;
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
ref={ref}
type="text"
placeholder={placeholder}
className={`h-9 w-[280px] border border-input bg-background pl-10 pr-4 text-sm text-foreground placeholder:text-muted-foreground transition-all focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 rounded-md ${className}`}
className={`h-9 w-full border border-input bg-background pl-10 pr-4 text-sm text-foreground placeholder:text-muted-foreground transition-all focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 rounded-md ${className}`}
{...props}
/>
</div>
Expand Down
Loading
Loading