fix(proposals): block unverified members from governance writes (L2)

createProposal/openProposal/withdrawProposal/castVote gated only with
requireMember, which admits status='unverified' — so a member pending approval
in a require_approval space could propose and cast counted votes before vetting.
Add the isPrivilegeEligible gate (same as requireMemberWithRole) to all four.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Moheeb Zara
2026-08-01 21:31:51 -07:00
co-authored by Claude Opus 4.8
parent 7fc08c5165
commit c7a6cd3853
2 changed files with 157 additions and 1 deletions
+21 -1
View File
@@ -8,7 +8,7 @@ import {
logActivity,
parseInput,
} from '@/lib/auth-helpers'
import { ADMIN_ROLES } from '@/lib/permissions'
import { ADMIN_ROLES, isPrivilegeEligible } from '@/lib/permissions'
import {
createProposalSchema,
openProposalSchema,
@@ -41,6 +41,11 @@ export async function createProposal(formData: {
const auth = await requireMember(supabase)
if (!auth.ok) return { error: auth.error }
const { member } = auth
// require_approval gate: an 'unverified' member (pending admin approval)
// must not create or participate in governance. Mirrors requireMemberWithRole.
if (!isPrivilegeEligible(member.status)) {
return { error: 'Your membership is pending approval.' }
}
const status = v.data.open_immediately ? 'open' : 'draft'
@@ -83,6 +88,11 @@ export async function openProposal(proposalId: string, voting_closes_at?: string
const auth = await requireMember(supabase)
if (!auth.ok) return { error: auth.error }
const { member } = auth
// require_approval gate: an 'unverified' member (pending admin approval)
// must not create or participate in governance. Mirrors requireMemberWithRole.
if (!isPrivilegeEligible(member.status)) {
return { error: 'Your membership is pending approval.' }
}
const updates: Record<string, unknown> = { status: 'open' }
if (v.data.voting_closes_at) updates.voting_closes_at = v.data.voting_closes_at
@@ -115,6 +125,11 @@ export async function withdrawProposal(proposalId: string) {
const auth = await requireMember(supabase)
if (!auth.ok) return { error: auth.error }
const { member } = auth
// require_approval gate: an 'unverified' member (pending admin approval)
// must not create or participate in governance. Mirrors requireMemberWithRole.
if (!isPrivilegeEligible(member.status)) {
return { error: 'Your membership is pending approval.' }
}
const { error } = await supabase
.from('proposals')
@@ -150,6 +165,11 @@ export async function castVote(formData: {
const auth = await requireMember(supabase)
if (!auth.ok) return { error: auth.error }
const { member } = auth
// require_approval gate: an 'unverified' member (pending admin approval)
// must not create or participate in governance. Mirrors requireMemberWithRole.
if (!isPrivilegeEligible(member.status)) {
return { error: 'Your membership is pending approval.' }
}
// Defense in depth: the votes RLS policies already enforce same-space,
// open status and the voting window, but this is a sensitive mutation, so