fix(auth): unverified members cannot exercise roles; single-membership join (D2)

require_approval was silently void: redeeming an admin-role invite into
a require_approval space yielded role=admin,status=unverified and
requireMemberWithRole never checked status -> instant admin pre-approval.
- PRIVILEGE_STATUSES/isPrivilegeEligible (current|late only; excludes
  unverified/inactive). requireMemberWithRole now rejects non-privilege-
  eligible status with 'pending approval' before the role check; Member
  carries status (getAuthMember selects it). requireMember stays
  permissive so unverified members still see their own /me.
- joinSpace rejects a second active membership (single-space model;
  prevents the .single() self-DoS).
Deferred (HANDOFF): SECURITY DEFINER helpers user_has_role_in_space/
user_has_permission still ignore status (RLS defense-in-depth, own
careful pass). 524 tests; build clean.
This commit is contained in:
Moheeb Zara
2026-05-18 18:30:20 -07:00
parent a513ad0fab
commit 2239e7a92d
4 changed files with 56 additions and 3 deletions
+18
View File
@@ -4,6 +4,8 @@ import {
ADMIN_ROLES,
TREASURER_ROLES,
ACTIVE_STATUSES,
PRIVILEGE_STATUSES,
isPrivilegeEligible,
hasRole,
type Role,
} from '@/lib/permissions'
@@ -50,6 +52,22 @@ describe('ACTIVE_STATUSES', () => {
})
})
describe('isPrivilegeEligible / PRIVILEGE_STATUSES', () => {
it('only current + late may exercise a role/permission', () => {
expect([...PRIVILEGE_STATUSES].sort()).toEqual(['current', 'late'].sort())
expect(isPrivilegeEligible('current')).toBe(true)
expect(isPrivilegeEligible('late')).toBe(true)
})
it('unverified (pending approval) is NOT privilege-eligible', () => {
expect(isPrivilegeEligible('unverified')).toBe(false)
})
it('inactive / null / undefined are NOT privilege-eligible', () => {
expect(isPrivilegeEligible('inactive')).toBe(false)
expect(isPrivilegeEligible(null)).toBe(false)
expect(isPrivilegeEligible(undefined)).toBe(false)
})
})
describe('hasRole', () => {
it('returns true when role is in the allowed list', () => {
expect(hasRole('admin', ADMIN_ROLES)).toBe(true)