Skip to content

Commit 3f838fd

Browse files
committed
fix(web): member management catches up with the signed removeOwner signature
removeOwner became removeOwner(account, accountId, wallet) in Task 4 (signed edge-function call) but removeMember/leaveOrg in supabase-member-management.ts still called it with the old 2-arg shape and leaveOrg still deleted account_owners rows directly with the anon key. Both now take the signing SigningAccount and route leave through the org-membership edge function's "leave" action; the org/manage page threads useActiveAccount() through to both call sites.
1 parent 995fb45 commit 3f838fd

2 files changed

Lines changed: 34 additions & 20 deletions

File tree

apps/web/src/app/app/org/manage/page.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,8 @@ export default function OrgManagePage() {
150150
}
151151

152152
const handleRemoveMember = async (wallet: string, name: string) => {
153-
if (!accountId || !confirm(`${name} wirklich entfernen?`)) return
154-
await removeMemberDB(accountId, wallet)
153+
if (!accountId || !thirdwebAccount || !confirm(`${name} wirklich entfernen?`)) return
154+
await removeMemberDB(thirdwebAccount, accountId, wallet)
155155
await load()
156156
}
157157

@@ -163,9 +163,9 @@ export default function OrgManagePage() {
163163
}
164164

165165
const handleLeave = async () => {
166-
if (!accountId || !walletAddress) return
166+
if (!accountId || !thirdwebAccount) return
167167
if (!confirm(`${activeAccount?.name || "Organisation"} wirklich verlassen?`)) return
168-
await leaveOrgDB(accountId, walletAddress)
168+
await leaveOrgDB(thirdwebAccount, accountId)
169169
await refreshAccounts()
170170
router.push("/app")
171171
}

apps/web/src/lib/supabase-member-management.ts

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import { supabase } from "./supabase";
66
import { fetchAccountOwners, removeOwner } from "./supabase-accounts";
7+
import { callOrgMembership, type SigningAccount } from "./org-membership/client";
78
import type { MemberWithProfile } from "@/types/account";
89

910
/** Fetch all members of an account with their user profiles. */
@@ -51,38 +52,51 @@ export async function fetchMembersWithProfiles(
5152
return enriched;
5253
}
5354

54-
/** Remove a member from an org (owner-only action). */
55+
/**
56+
* Remove a member from an org (owner/admin action, or self-removal). Signed
57+
* by `account`; threads through to removeOwner, which calls the
58+
* org-membership edge function — no client-side pre-check needed, the edge
59+
* function enforces the owner/admin gate and the last-owner invariant.
60+
*/
5561
export async function removeMember(
62+
account: SigningAccount,
5663
accountId: string,
5764
walletAddress: string
5865
): Promise<void> {
59-
await removeOwner(accountId, walletAddress);
66+
await removeOwner(account, accountId, walletAddress);
6067
}
6168

62-
/** Leave an org voluntarily. Blocks if sole owner. */
69+
/**
70+
* Leave an org voluntarily. Signed by `account` (the leaving wallet is
71+
* derived server-side from the signature, never passed in the payload). The
72+
* local owner-count check is a fast client-side UX shortcut only — the
73+
* server enforces the real last-owner invariant via `delete_owner_guarded`
74+
* and returns LAST_OWNER if this check was stale.
75+
*/
6376
export async function leaveOrg(
64-
accountId: string,
65-
walletAddress: string
77+
account: SigningAccount,
78+
accountId: string
6679
): Promise<void> {
80+
const walletAddress = account.address.toLowerCase();
6781
const owners = await fetchAccountOwners(accountId);
6882
const ownerCount = owners.filter((o) => o.role === "owner").length;
6983
const myRole = owners.find(
70-
(o) => o.wallet_address === walletAddress.toLowerCase()
84+
(o) => o.wallet_address.toLowerCase() === walletAddress
7185
)?.role;
7286

87+
const lastOwnerMessage =
88+
"Du bist der einzige Inhaber. Übertrage die Inhaberschaft, bevor du die Organisation verlässt.";
89+
7390
if (myRole === "owner" && ownerCount <= 1) {
74-
throw new Error(
75-
"Du bist der einzige Inhaber. Übertrage die Inhaberschaft, bevor du die Organisation verlässt."
76-
);
91+
throw new Error(lastOwnerMessage);
7792
}
7893

79-
const { error } = await supabase
80-
.from("account_owners")
81-
.delete()
82-
.eq("account_id", accountId)
83-
.eq("wallet_address", walletAddress.toLowerCase());
84-
85-
if (error) throw error;
94+
const res = await callOrgMembership(account, "leave", { accountId });
95+
if (!res.ok) {
96+
if (res.code === "LAST_OWNER") throw new Error(lastOwnerMessage);
97+
console.error("leaveOrg error:", res.code, res.message);
98+
throw new Error(res.message || res.code || "leaveOrg failed");
99+
}
86100
}
87101

88102
/** Search users by name for the invite flow (excludes existing members). */

0 commit comments

Comments
 (0)