Skip to content

Commit ecd70f4

Browse files
committed
fix(web): invites ride the signed path — creation, acceptance and reads stop trusting the client
Rewires all 8 exports of supabase-invites.ts to Task 4's org-membership edge function contract: create/revoke/accept/decline/list_invites/ has_pending_invite go through callOrgMembership(account, action, payload) with a signed message, and invitedBy/the leaving wallet are derived server-side from the verified signer rather than trusted from the client. fetchInviteByToken moves to the anon-callable get_invite_by_token RPC (bearer-token lookup, no wallet param). Reads keep a read-only fallback to the direct invite_tokens query (isolated to three helpers, clearly commented) for the deploy window before the RPC/edge function are live in production; writes never fall back. hasPendingInvite's new contract only answers for the calling wallet (the edge function won't check an arbitrary wallet — that would leak other users' invite status), so org/manage's duplicate-invite precheck is dropped; duplicates are already legal in the schema. Also threads useActiveAccount() through every invite call site (invite/[token]/page.tsx, org/manage/page.tsx), and drops the administrator-only 'stadt'/'fraktion' sub_types from the org/create picker now that create_account rejects them for self-service callers (folded in from the Task 4 review).
1 parent 3f838fd commit ecd70f4

4 files changed

Lines changed: 210 additions & 190 deletions

File tree

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@ import {
2323
type OrgSubType,
2424
} from "@/types/account";
2525

26+
// Self-service sub_types only — the org-membership edge function's
27+
// create_account action rejects 'stadt'/'fraktion' (administrator-issued
28+
// only, via the service role), so offering them here would just produce a
29+
// BAD_REQUEST for anyone who picks them.
2630
const ORG_SUB_TYPES: OrgSubType[] = [
2731
"unternehmen",
2832
"restaurant",
2933
"verein",
30-
"stadt",
31-
"fraktion",
3234
"journalist",
3335
];
3436

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

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import {
2222
revokeInvite as revokeInviteDB,
2323
createInAppInvite,
2424
createLinkInvite,
25-
hasPendingInvite,
2625
} from "@/lib/supabase-invites"
2726
import type {
2827
MemberWithProfile,
@@ -84,16 +83,16 @@ export default function OrgManagePage() {
8483
const canLeave = canLeaveOrg(currentRole, ownerCount)
8584

8685
const load = useCallback(async () => {
87-
if (!accountId || !walletAddress) return
86+
if (!accountId || !walletAddress || !thirdwebAccount) return
8887
const [membersData, invitesData, role] = await Promise.all([
8988
fetchMembersWithProfiles(accountId),
90-
fetchPendingInvites(accountId),
89+
fetchPendingInvites(thirdwebAccount, accountId),
9190
getAccountRole(accountId, walletAddress),
9291
])
9392
setMembers(membersData)
9493
setPendingInvites(invitesData)
9594
setCurrentRole(role)
96-
}, [accountId, walletAddress])
95+
}, [accountId, walletAddress, thirdwebAccount])
9796

9897
useEffect(() => {
9998
setIsLoading(true)
@@ -112,12 +111,15 @@ export default function OrgManagePage() {
112111
}
113112

114113
const handleSendInvite = async () => {
115-
if (!selectedUser || !walletAddress || !accountId) return
114+
if (!selectedUser || !thirdwebAccount || !accountId) return
116115
setIsSending(true)
117116
try {
118-
const exists = await hasPendingInvite(accountId, selectedUser.wallet_address)
119-
if (exists) { alert("Dieser Benutzer hat bereits eine ausstehende Einladung."); return }
120-
await createInAppInvite(accountId, selectedUser.wallet_address, inviteRole, walletAddress)
117+
// hasPendingInvite now answers only for the calling wallet (the edge
118+
// function doesn't accept an arbitrary wallet to check — that would
119+
// leak other wallets' invite status), so there's no client-side
120+
// precheck for the invitee anymore. Duplicate pending invites for the
121+
// same person are allowed by the schema; create_invite just proceeds.
122+
await createInAppInvite(thirdwebAccount, accountId, selectedUser.wallet_address, inviteRole)
121123
setShowInvite(false)
122124
setSelectedUser(null)
123125
setSearchQuery("")
@@ -130,10 +132,10 @@ export default function OrgManagePage() {
130132
}
131133

132134
const handleCreateLink = async () => {
133-
if (!walletAddress || !accountId) return
135+
if (!thirdwebAccount || !accountId) return
134136
setIsSending(true)
135137
try {
136-
const invite = await createLinkInvite(accountId, inviteRole, walletAddress, expiryDays)
138+
const invite = await createLinkInvite(thirdwebAccount, accountId, inviteRole, expiryDays)
137139
setGeneratedLink(`https://roebel.app/invite/${invite.token}`)
138140
await load()
139141
} catch (e: any) {
@@ -144,8 +146,8 @@ export default function OrgManagePage() {
144146
}
145147

146148
const handleRevoke = async (inviteId: string) => {
147-
if (!confirm("Einladung wirklich widerrufen?")) return
148-
await revokeInviteDB(inviteId)
149+
if (!thirdwebAccount || !confirm("Einladung wirklich widerrufen?")) return
150+
await revokeInviteDB(thirdwebAccount, inviteId)
149151
await load()
150152
}
151153

apps/web/src/app/invite/[token]/page.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,10 @@ export default function InviteTokenPage() {
6161
}, [token, walletAddress])
6262

6363
const handleAccept = async () => {
64-
if (!invite || !walletAddress) return
64+
if (!invite || !thirdwebAccount) return
6565
setIsAccepting(true)
6666
try {
67-
await acceptInvite(invite.id, walletAddress)
67+
await acceptInvite(thirdwebAccount, invite.id)
6868
setResolved("accepted")
6969
} catch (err: any) {
7070
setError(err?.message || "Fehler beim Annehmen")
@@ -74,10 +74,10 @@ export default function InviteTokenPage() {
7474
}
7575

7676
const handleDecline = async () => {
77-
if (!invite) return
77+
if (!invite || !thirdwebAccount) return
7878
setIsDeclining(true)
7979
try {
80-
await declineInvite(invite.id)
80+
await declineInvite(thirdwebAccount, invite.id)
8181
setResolved("declined")
8282
} catch (err: any) {
8383
setError(err?.message || "Fehler beim Ablehnen")

0 commit comments

Comments
 (0)