From 9e6aed38840954dca3ab8c8f73d0adea466fb323 Mon Sep 17 00:00:00 2001 From: JoaquinBN <47780609+JoaquinBN@users.noreply.github.com> Date: Sat, 10 Jan 2026 00:16:16 +0700 Subject: [PATCH 1/5] Restrict contribution submissions to users with required roles (#304) Non-validators cannot submit validator contributions, and non-builders cannot submit builder contributions. Backend validation enforces role checks with 403 responses. Frontend disables contribution tabs and shows links to unlock journeys. --- backend/contributions/views.py | 19 +++++++ .../components/ContributionSelection.svelte | 50 ++++++++++++++++++- frontend/src/routes/SubmitContribution.svelte | 3 ++ 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/backend/contributions/views.py b/backend/contributions/views.py index 5e7ec6d..2ed6461 100644 --- a/backend/contributions/views.py +++ b/backend/contributions/views.py @@ -657,6 +657,25 @@ def create(self, request, *args, **kwargs): status=status.HTTP_400_BAD_REQUEST ) + # Validate category restrictions based on user role + contribution_type_id = data.get('contribution_type') + if contribution_type_id: + try: + contribution_type = ContributionType.objects.select_related('category').get(id=contribution_type_id) + if contribution_type.category: + if contribution_type.category.slug == 'builder' and not hasattr(request.user, 'builder'): + return Response( + {'error': 'You must complete the Builder Welcome journey before submitting builder contributions.'}, + status=status.HTTP_403_FORBIDDEN + ) + if contribution_type.category.slug == 'validator' and not hasattr(request.user, 'validator'): + return Response( + {'error': 'You must complete the Validator Waitlist journey before submitting validator contributions.'}, + status=status.HTTP_403_FORBIDDEN + ) + except ContributionType.DoesNotExist: + pass + serializer = self.get_serializer(data=data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) diff --git a/frontend/src/lib/components/ContributionSelection.svelte b/frontend/src/lib/components/ContributionSelection.svelte index 243618f..ba31e43 100644 --- a/frontend/src/lib/components/ContributionSelection.svelte +++ b/frontend/src/lib/components/ContributionSelection.svelte @@ -15,9 +15,14 @@ providedContributionTypes = null, // Allow passing types from parent disabled = false, // Disable selection when locked (e.g., mission) selectedMission = $bindable(null), // Currently selected mission + isValidator = true, + isBuilder = true, onSelectionChange = () => {} } = $props(); + let validatorTabDisabled = $derived(!isValidator && !stewardMode); + let builderTabDisabled = $derived(!isBuilder && !stewardMode); + let contributionTypes = $state([]); let missions = $state([]); // All missions let filteredTypes = $state([]); @@ -268,8 +273,10 @@ type="button" class="category-btn" class:active={selectedCategory === 'validator'} + class:disabled={validatorTabDisabled} style={selectedCategory === 'validator' ? 'background: #e0f2fe; color: #0369a1;' : ''} - onclick={() => selectCategory('validator')} + onclick={() => !validatorTabDisabled && selectCategory('validator')} + disabled={validatorTabDisabled} > Validator @@ -277,13 +284,27 @@ type="button" class="category-btn" class:active={selectedCategory === 'builder'} + class:disabled={builderTabDisabled} style={selectedCategory === 'builder' ? 'background: #ffedd5; color: #c2410c;' : ''} - onclick={() => selectCategory('builder')} + onclick={() => !builderTabDisabled && selectCategory('builder')} + disabled={builderTabDisabled} > Builder + {#if validatorTabDisabled || builderTabDisabled} +
+ {#if validatorTabDisabled && builderTabDisabled} + Complete a journey to submit contributions: Validator Waitlist or Builder Welcome + {:else if validatorTabDisabled} + Complete the Validator Waitlist journey to submit validator contributions. + {:else} + Complete the Builder Welcome journey to submit builder contributions. + {/if} +
+ {/if} +