Skip to content

Commit 81adcc6

Browse files
fix/org-owner-membership-invite-role: fix: align organization owner role display and invite authorization
1 parent 0e0dd19 commit 81adcc6

4 files changed

Lines changed: 45 additions & 16 deletions

File tree

apps/web/actions/organization/send-invites.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@ import { getCurrentUser } from "@cap/database/auth/session";
55
import { sendEmail } from "@cap/database/emails/config";
66
import { OrganizationInvite } from "@cap/database/emails/organization-invite";
77
import { nanoId } from "@cap/database/helpers";
8-
import { organizationInvites, organizations } from "@cap/database/schema";
8+
import {
9+
organizationInvites,
10+
organizationMembers,
11+
organizations,
12+
} from "@cap/database/schema";
913
import { serverEnv } from "@cap/env";
1014
import type { Organisation } from "@cap/web-domain";
11-
import { eq } from "drizzle-orm";
15+
import { and, eq } from "drizzle-orm";
1216
import { revalidatePath } from "next/cache";
1317

1418
export async function sendOrganizationInvites(
@@ -30,10 +34,27 @@ export async function sendOrganizationInvites(
3034
throw new Error("Organization not found");
3135
}
3236

33-
if (organization[0]?.ownerId !== user.id) {
37+
const [ownerMembership] = await db()
38+
.select({ id: organizationMembers.id })
39+
.from(organizationMembers)
40+
.where(
41+
and(
42+
eq(organizationMembers.organizationId, organizationId),
43+
eq(organizationMembers.userId, user.id),
44+
eq(organizationMembers.role, "owner"),
45+
),
46+
)
47+
.limit(1);
48+
49+
if (!ownerMembership) {
3450
throw new Error("Only the owner can send organization invites");
3551
}
3652

53+
const [org] = organization;
54+
if (!org) {
55+
throw new Error("Organization not found");
56+
}
57+
3758
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3859
const validEmails = invitedEmails.filter((email) =>
3960
emailRegex.test(email.trim()),
@@ -53,11 +74,11 @@ export async function sendOrganizationInvites(
5374
const inviteUrl = `${serverEnv().WEB_URL}/invite/${inviteId}`;
5475
await sendEmail({
5576
email: email.trim(),
56-
subject: `Invitation to join ${organization[0].name} on Cap`,
77+
subject: `Invitation to join ${org.name} on Cap`,
5778
react: OrganizationInvite({
5879
email: email.trim(),
5980
url: inviteUrl,
60-
organizationName: organization[0].name,
81+
organizationName: org.name,
6182
}),
6283
});
6384
}

apps/web/app/(org)/dashboard/settings/organization/Organization.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ export const Organization = () => {
2525
const [loading, setLoading] = useState(false);
2626
const [billingLoading, setBillingLoading] = useState(false);
2727
const isOwner = user?.id === activeOrganization?.organization.ownerId;
28+
const canInviteUsers =
29+
activeOrganization?.members?.some(
30+
(member) => member.userId === user?.id && member.role === "owner",
31+
) ?? false;
2832
const [isInviteDialogOpen, setIsInviteDialogOpen] = useState(false);
2933
const ownerToastShown = useRef(false);
3034

@@ -71,6 +75,7 @@ export const Organization = () => {
7175

7276
<MembersCard
7377
isOwner={isOwner}
78+
canInviteUsers={canInviteUsers}
7479
loading={loading}
7580
handleManageBilling={() => handleManageBilling(setLoading)}
7681
showOwnerToast={showOwnerToast}
@@ -86,6 +91,7 @@ export const Organization = () => {
8691
<InviteDialog
8792
isOpen={isInviteDialogOpen}
8893
setIsOpen={setIsInviteDialogOpen}
94+
canInviteUsers={canInviteUsers}
8995
isOwner={isOwner}
9096
showOwnerToast={showOwnerToast}
9197
handleManageBilling={() => handleManageBilling(setLoading)}

apps/web/app/(org)/dashboard/settings/organization/components/InviteDialog.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { useDashboardContext } from "../../../Contexts";
2323
interface InviteDialogProps {
2424
isOpen: boolean;
2525
setIsOpen: (isOpen: boolean) => void;
26+
canInviteUsers: boolean;
2627
isOwner: boolean;
2728
showOwnerToast: () => void;
2829
handleManageBilling: () => Promise<void>;
@@ -31,6 +32,7 @@ interface InviteDialogProps {
3132
export const InviteDialog = ({
3233
isOpen,
3334
setIsOpen,
35+
canInviteUsers,
3436
isOwner,
3537
showOwnerToast,
3638
handleManageBilling,
@@ -41,9 +43,7 @@ export const InviteDialog = ({
4143
const [emailInput, setEmailInput] = useState("");
4244
const [upgradeLoading, setUpgradeLoading] = useState(false);
4345

44-
const { inviteQuota, remainingSeats } = calculateSeats(
45-
activeOrganization || {},
46-
);
46+
const { remainingSeats } = calculateSeats(activeOrganization || {});
4747

4848
const handleAddEmails = () => {
4949
const newEmails = emailInput
@@ -83,7 +83,7 @@ export const InviteDialog = ({
8383

8484
const sendInvites = useMutation({
8585
mutationFn: async () => {
86-
if (!isOwner) {
86+
if (!canInviteUsers) {
8787
showOwnerToast();
8888
throw new Error("Not authorized");
8989
}
@@ -162,7 +162,7 @@ export const InviteDialog = ({
162162
variant="destructive"
163163
size="xs"
164164
onClick={() => handleRemoveEmail(email)}
165-
disabled={!isOwner}
165+
disabled={!canInviteUsers}
166166
>
167167
Remove
168168
</Button>

apps/web/app/(org)/dashboard/settings/organization/components/MembersCard.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { calculateSeats } from "@/utils/organization";
2929

3030
interface MembersCardProps {
3131
isOwner: boolean;
32+
canInviteUsers: boolean;
3233
loading: boolean;
3334
handleManageBilling: () => Promise<void>;
3435
showOwnerToast: () => void;
@@ -37,6 +38,7 @@ interface MembersCardProps {
3738

3839
export const MembersCard = ({
3940
isOwner,
41+
canInviteUsers,
4042
loading,
4143
handleManageBilling,
4244
showOwnerToast,
@@ -115,8 +117,8 @@ export const MembersCard = ({
115117
}
116118
};
117119

118-
const isMemberOwner = (id: string) => {
119-
return id === activeOrganization?.organization.ownerId;
120+
const isMemberOwner = (role: string) => {
121+
return role === "owner";
120122
};
121123

122124
const pendingMemberTest = {
@@ -177,7 +179,7 @@ export const MembersCard = ({
177179
variant="dark"
178180
className="px-6 min-w-auto"
179181
onClick={() => {
180-
if (!isOwner) {
182+
if (!canInviteUsers) {
181183
showOwnerToast();
182184
} else if (remainingSeats <= 0) {
183185
toast.error(
@@ -187,7 +189,7 @@ export const MembersCard = ({
187189
setIsInviteDialogOpen(true);
188190
}
189191
}}
190-
disabled={!isOwner}
192+
disabled={!canInviteUsers}
191193
>
192194
+ Invite users
193195
</Button>
@@ -210,12 +212,12 @@ export const MembersCard = ({
210212
<TableCell>{member.user.name}</TableCell>
211213
<TableCell>{member.user.email}</TableCell>
212214
<TableCell>
213-
{isMemberOwner(member.user.id) ? "Owner" : "Member"}
215+
{isMemberOwner(member.role) ? "Owner" : "Member"}
214216
</TableCell>
215217
<TableCell>{format(member.createdAt, "MMM d, yyyy")}</TableCell>
216218
<TableCell>Active</TableCell>
217219
<TableCell>
218-
{!isMemberOwner(member.user.id) ? (
220+
{!isMemberOwner(member.role) ? (
219221
<Button
220222
type="button"
221223
size="xs"

0 commit comments

Comments
 (0)