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}
+
@@ -495,6 +516,31 @@
position: relative;
}
+ .category-btn.disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+
+ .category-btn.disabled:hover {
+ background: transparent;
+ }
+
+ .category-locked-message {
+ padding: 0.75rem 1rem;
+ background: #fef3c7;
+ border: 1px solid #f59e0b;
+ border-radius: 0.375rem;
+ margin-top: 0.75rem;
+ font-size: 0.875rem;
+ color: #92400e;
+ }
+
+ .category-locked-message a {
+ color: #d97706;
+ font-weight: 500;
+ text-decoration: underline;
+ }
+
.contribution-type-selector {
position: relative;
margin-top: 0;
diff --git a/frontend/src/routes/SubmitContribution.svelte b/frontend/src/routes/SubmitContribution.svelte
index 7372e52..7438b84 100644
--- a/frontend/src/routes/SubmitContribution.svelte
+++ b/frontend/src/routes/SubmitContribution.svelte
@@ -1,6 +1,7 @@
+
+
+
+
+ {#if isOpen && results.length > 0}
+
+ {#each results as user, index}
+
+ {/each}
+
+ {/if}
+
+
+
diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js
index d120d96..c3b8867 100644
--- a/frontend/src/lib/api.js
+++ b/frontend/src/lib/api.js
@@ -62,7 +62,8 @@ export const usersAPI = {
getDeploymentStatus: () => api.get('/users/deployment_status/'),
getActiveValidators: () => api.get('/users/validators/'),
getReferrals: () => api.get('/users/referrals/'),
- getReferralPoints: () => api.get('/users/referral_points/')
+ getReferralPoints: () => api.get('/users/referral_points/'),
+ searchUsers: (query) => api.get('/users/search/', { params: { q: query } })
};
// API endpoints for contributions
From 0a2850d389d4a20b580369798105ea927eff2261 Mon Sep 17 00:00:00 2001
From: JoaquinBN
Date: Sat, 10 Jan 2026 12:33:48 +0100
Subject: [PATCH 5/5] Add CRON_SYNC_TOKEN environment secret to App Runner
deployment
Add CRON_SYNC_TOKEN as a runtime environment secret that references the AWS SSM Parameter Store. Also remove hardcoded CPU and Memory configuration from the deployment script to allow managing instance sizes directly from the AWS console.
---
backend/.env.example | 6 +++++-
backend/deploy-apprunner.sh | 10 ++++------
2 files changed, 9 insertions(+), 7 deletions(-)
diff --git a/backend/.env.example b/backend/.env.example
index d0fd0e7..a1fa147 100644
--- a/backend/.env.example
+++ b/backend/.env.example
@@ -78,4 +78,8 @@ GITHUB_ENCRYPTION_KEY=your_encryption_key_here
# For production: Get real keys at https://www.google.com/recaptcha/admin
# Select "reCAPTCHA v2" > "I'm not a robot" Checkbox
RECAPTCHA_PUBLIC_KEY=6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI
-RECAPTCHA_PRIVATE_KEY=6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe
\ No newline at end of file
+RECAPTCHA_PRIVATE_KEY=6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe
+
+# Cron Sync Token
+# Used for authenticating cron job requests to sync validators
+CRON_SYNC_TOKEN=your_cron_sync_token_here
\ No newline at end of file
diff --git a/backend/deploy-apprunner.sh b/backend/deploy-apprunner.sh
index 555c601..b9e6295 100755
--- a/backend/deploy-apprunner.sh
+++ b/backend/deploy-apprunner.sh
@@ -211,7 +211,8 @@ if aws apprunner describe-service --service-arn arn:aws:apprunner:$REGION:$ACCOU
"GITHUB_ENCRYPTION_KEY": "$SSM_PREFIX/prod/github_encryption_key",
"GITHUB_REPO_TO_STAR": "$SSM_PREFIX/prod/github_repo_to_star",
"RECAPTCHA_PUBLIC_KEY": "$SSM_PREFIX/prod/recaptcha_public_key",
- "RECAPTCHA_PRIVATE_KEY": "$SSM_PREFIX/prod/recaptcha_private_key"
+ "RECAPTCHA_PRIVATE_KEY": "$SSM_PREFIX/prod/recaptcha_private_key",
+ "CRON_SYNC_TOKEN": "$SSM_PREFIX/prod/cron_sync_token"
},
"StartCommand": "./startup.sh gunicorn --bind 0.0.0.0:8000 --timeout 180 --workers 2 tally.wsgi:application"
},
@@ -223,8 +224,6 @@ if aws apprunner describe-service --service-arn arn:aws:apprunner:$REGION:$ACCOU
}
},
"InstanceConfiguration": {
- "Cpu": "0.25 vCPU",
- "Memory": "0.5 GB",
"InstanceRoleArn": "arn:aws:iam::$ACCOUNT_ID:role/AppRunnerInstanceRole"
},
"HealthCheckConfiguration": {
@@ -293,7 +292,8 @@ else
"GITHUB_ENCRYPTION_KEY": "$SSM_PREFIX/prod/github_encryption_key",
"GITHUB_REPO_TO_STAR": "$SSM_PREFIX/prod/github_repo_to_star",
"RECAPTCHA_PUBLIC_KEY": "$SSM_PREFIX/prod/recaptcha_public_key",
- "RECAPTCHA_PRIVATE_KEY": "$SSM_PREFIX/prod/recaptcha_private_key"
+ "RECAPTCHA_PRIVATE_KEY": "$SSM_PREFIX/prod/recaptcha_private_key",
+ "CRON_SYNC_TOKEN": "$SSM_PREFIX/prod/cron_sync_token"
},
"StartCommand": "./startup.sh gunicorn --bind 0.0.0.0:8000 --timeout 180 --workers 2 tally.wsgi:application"
},
@@ -305,8 +305,6 @@ else
}
},
"InstanceConfiguration": {
- "Cpu": "0.25 vCPU",
- "Memory": "0.5 GB",
"InstanceRoleArn": "arn:aws:iam::$ACCOUNT_ID:role/AppRunnerInstanceRole"
},
"HealthCheckConfiguration": {