diff --git a/Dockerfile b/Dockerfile index 96a08551..035c9e5e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ RUN pip install -r requirements.txt COPY . . RUN mkdir db RUN mkdir -p static -RUN mkdir media +RUN mkdir media -p RUN chmod +x start_dev.sh EXPOSE 5678 diff --git a/brightIDfaucet/settings.py b/brightIDfaucet/settings.py index 7e1e82fb..5a18aeec 100644 --- a/brightIDfaucet/settings.py +++ b/brightIDfaucet/settings.py @@ -72,6 +72,8 @@ def str2bool(v): MEMCACHED_PASSWORD = os.environ.get("MEMCACHEDCLOUD_PASSWORD") DEPLOYMENT_ENV = os.environ.get("DEPLOYMENT_ENV") +CLOUDFLARE_TURNSTILE_SECRET_KEY = os.environ.get("CLOUDFLARE_TURNSTILE_SECRET_KEY") + assert DEPLOYMENT_ENV in ["dev", "main"] @@ -259,4 +261,4 @@ def before_send(event, hint): "djangorestframework_camel_case.parser.CamelCaseJSONParser", ), } -CELERY_BROKER_URL = REDIS_URL \ No newline at end of file +CELERY_BROKER_URL = REDIS_URL diff --git a/core/constraints/__init__.py b/core/constraints/__init__.py index edb72907..648d9485 100644 --- a/core/constraints/__init__.py +++ b/core/constraints/__init__.py @@ -63,6 +63,7 @@ IsFollowingTwitterBatch, IsFollowinTwitterUser, ) +from core.constraints.captcha import HasVerifiedCloudflareCaptcha def get_constraint(constraint_label: str) -> ConstraintVerification: diff --git a/core/constraints/abstract.py b/core/constraints/abstract.py index 5292c94b..e0f70f9b 100644 --- a/core/constraints/abstract.py +++ b/core/constraints/abstract.py @@ -56,9 +56,10 @@ class ConstraintVerification(ABC): app_name = ConstraintApp.GENERAL.value __response_text = "" - def __init__(self, user_profile) -> None: + def __init__(self, user_profile, context=None) -> None: self.user_profile = user_profile self._param_values = {} + self.context = context def get_info(self, *args, **kwargs): pass diff --git a/core/constraints/captcha.py b/core/constraints/captcha.py new file mode 100644 index 00000000..feac1eb9 --- /dev/null +++ b/core/constraints/captcha.py @@ -0,0 +1,32 @@ +from core.constraints.abstract import ConstraintApp, ConstraintVerification +from core.thirdpartyapp.cloudflare import CloudflareUtil + + +import logging + +from core.utils import RequestContextExtractor + + +logger = logging.getLogger(__name__) + + +class HasVerifiedCloudflareCaptcha(ConstraintVerification): + _param_keys = [] + app_name = ConstraintApp.GENERAL.value + + def is_observed(self, *args, **kwargs) -> bool: + + if self.context is None or self.context.get("requset") is None: + return False + + cloudflare = CloudflareUtil() + + request_context: RequestContextExtractor = RequestContextExtractor( + self.context["requset"] + ) + + turnstile_token = request_context.data.get("cf-turnstile-response") + + return turnstile_token is not None and cloudflare.is_verified( + turnstile_token, request_context.ip + ) diff --git a/core/models.py b/core/models.py index 99b7070c..547fbb57 100644 --- a/core/models.py +++ b/core/models.py @@ -6,6 +6,7 @@ from django.contrib.postgres.fields import ArrayField from django.db import models from django.utils.translation import gettext_lazy as _ +from core.constraints.captcha import HasVerifiedCloudflareCaptcha from encrypted_model_fields.fields import EncryptedCharField from rest_framework.exceptions import ValidationError from solders.keypair import Keypair @@ -155,6 +156,7 @@ class Type(models.TextChoices): GLMStakingVerification, IsFollowingTwitterBatch, IsFollowingFarcasterBatch, + HasVerifiedCloudflareCaptcha, ] name = models.CharField( diff --git a/core/thirdpartyapp/cloudflare.py b/core/thirdpartyapp/cloudflare.py new file mode 100644 index 00000000..4687928f --- /dev/null +++ b/core/thirdpartyapp/cloudflare.py @@ -0,0 +1,24 @@ +import logging +from django.conf import settings +import requests + + +logger = logging.getLogger(__name__) + + +class CloudflareUtil: + secret_key = settings.CLOUDFLARE_TURNSTILE_SECRET_KEY + api_url = "https://challenges.cloudflare.com/turnstile/v0" + + def is_verified(self, token: str, ip: str) -> bool: + try: + res = requests.post( + f"{self.api_url}/siteverify", + data={"secret": self.secret_key, "response": token, "remoteip": ip}, + ) + + return res.ok and res.json()["success"] + except Exception as e: + logger.info(f"Error occurred during cloudflare verification {str(e)}") + + return False diff --git a/core/utils.py b/core/utils.py index 268d7070..0375c00a 100644 --- a/core/utils.py +++ b/core/utils.py @@ -5,6 +5,7 @@ import uuid from contextlib import contextmanager +from django.http import HttpRequest import pytz import web3.exceptions from django.core.cache import cache @@ -377,3 +378,23 @@ def save(self, file: UploadedFile): str(self.upload_to or "") + file_name + file_extension, file ) return MEDIA_ROOT + "/" + path + + + + +class RequestContextExtractor: + def __init__(self, request) -> None: + self.headers = request.headers + self.ip = RequestContextExtractor.get_client_ip(request.META.get('HTTP_X_FORWARDED_FOR') or request.META['REMOTE_ADDR']) + self.data = {**request.query_params, **request.data} + + @staticmethod + def get_client_ip(x_forwarded_for): + if x_forwarded_for: + ip_list = [ip.strip() for ip in x_forwarded_for.split(',')] + for ip in ip_list: + if ip and not ip.startswith(('10.', '172.16.', '192.168.')): + return ip + return None + + \ No newline at end of file diff --git a/faucet/migrations/0019_brightuser__last_verified_datetime.py b/faucet/migrations/0019_brightuser__last_verified_datetime.py index 6f4e85f7..f2aac0b1 100644 --- a/faucet/migrations/0019_brightuser__last_verified_datetime.py +++ b/faucet/migrations/0019_brightuser__last_verified_datetime.py @@ -2,7 +2,6 @@ import datetime from django.db import migrations, models -from django.utils.timezone import utc class Migration(migrations.Migration): @@ -15,6 +14,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='brightuser', name='_last_verified_datetime', - field=models.DateTimeField(default=datetime.datetime(1970, 1, 1, 0, 0, tzinfo=utc)), + field=models.DateTimeField(default=datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)), ), ] diff --git a/prizetap/migrations/0076_alter_constraint_name.py b/prizetap/migrations/0076_alter_constraint_name.py new file mode 100644 index 00000000..45504ca3 --- /dev/null +++ b/prizetap/migrations/0076_alter_constraint_name.py @@ -0,0 +1,89 @@ +# Generated by Django 4.0.4 on 2024-08-25 09:12 + +from django.db import migrations, models + + +def create_prizetap_constraint(apps, schema_editor): + Constraint = apps.get_model("prizetap", "Constraint") + + Constraint.objects.create( + name="core.HasVerifiedCloudflareCaptcha", + description="HasVerifiedCloudflareCaptcha", + title="Passed Cloudflare Captcha", + type="VER", + ) + + + +class Migration(migrations.Migration): + + dependencies = [ + ("prizetap", "0075_alter_constraint_name"), + ] + + operations = [ + migrations.AlterField( + model_name="constraint", + name="name", + field=models.CharField( + choices=[ + ("core.BrightIDMeetVerification", "BrightIDMeetVerification"), + ("core.BrightIDAuraVerification", "BrightIDAuraVerification"), + ("core.HasNFTVerification", "HasNFTVerification"), + ("core.HasTokenVerification", "HasTokenVerification"), + ( + "core.HasTokenTransferVerification", + "HasTokenTransferVerification", + ), + ("core.AllowListVerification", "AllowListVerification"), + ("core.HasENSVerification", "HasENSVerification"), + ("core.HasLensProfile", "HasLensProfile"), + ("core.IsFollowingLensUser", "IsFollowingLensUser"), + ("core.BeFollowedByLensUser", "BeFollowedByLensUser"), + ("core.DidMirrorOnLensPublication", "DidMirrorOnLensPublication"), + ("core.DidCollectLensPublication", "DidCollectLensPublication"), + ("core.HasMinimumLensPost", "HasMinimumLensPost"), + ("core.HasMinimumLensFollower", "HasMinimumLensFollower"), + ("core.BeFollowedByFarcasterUser", "BeFollowedByFarcasterUser"), + ("core.HasMinimumFarcasterFollower", "HasMinimumFarcasterFollower"), + ("core.DidLikedFarcasterCast", "DidLikedFarcasterCast"), + ("core.DidRecastFarcasterCast", "DidRecastFarcasterCast"), + ("core.IsFollowingFarcasterUser", "IsFollowingFarcasterUser"), + ("core.HasFarcasterProfile", "HasFarcasterProfile"), + ("core.BeAttestedBy", "BeAttestedBy"), + ("core.Attest", "Attest"), + ("core.HasDonatedOnGitcoin", "HasDonatedOnGitcoin"), + ("core.HasMinimumHumanityScore", "HasMinimumHumanityScore"), + ("core.HasGitcoinPassportProfile", "HasGitcoinPassportProfile"), + ("core.IsFollowingFarcasterChannel", "IsFollowingFarcasterChannel"), + ("core.BridgeEthToArb", "BridgeEthToArb"), + ("core.IsFollowinTwitterUser", "IsFollowinTwitterUser"), + ("core.BeFollowedByTwitterUser", "BeFollowedByTwitterUser"), + ("core.DidRetweetTweet", "DidRetweetTweet"), + ("core.DidQuoteTweet", "DidQuoteTweet"), + ("core.HasMuonNode", "HasMuonNode"), + ("core.DelegateArb", "DelegateArb"), + ("core.DelegateOP", "DelegateOP"), + ("core.DidDelegateArbToAddress", "DidDelegateArbToAddress"), + ("core.DidDelegateOPToAddress", "DidDelegateOPToAddress"), + ("core.GLMStakingVerification", "GLMStakingVerification"), + ("core.IsFollowingTwitterBatch", "IsFollowingTwitterBatch"), + ("core.IsFollowingFarcasterBatch", "IsFollowingFarcasterBatch"), + ( + "core.HasVerifiedCloudflareCaptcha", + "HasVerifiedCloudflareCaptcha", + ), + ("prizetap.HaveUnitapPass", "HaveUnitapPass"), + ("prizetap.NotHaveUnitapPass", "NotHaveUnitapPass"), + ("faucet.OptimismDonationConstraint", "OptimismDonationConstraint"), + ( + "faucet.OptimismClaimingGasConstraint", + "OptimismClaimingGasConstraint", + ), + ], + max_length=255, + unique=True, + ), + ), + migrations.RunPython(create_prizetap_constraint), + ] diff --git a/prizetap/validators.py b/prizetap/validators.py index 648ce834..3d8e37f9 100644 --- a/prizetap/validators.py +++ b/prizetap/validators.py @@ -15,6 +15,7 @@ def __init__(self, *args, **kwargs): self.user_profile: UserProfile = kwargs["user_profile"] self.raffle: Raffle = kwargs["raffle"] self.raffle_data: dict = kwargs.get("raffle_data", dict()) + self.request = kwargs.get("requset") def can_enroll_in_raffle(self): if not self.raffle.is_claimable: @@ -29,7 +30,7 @@ def check_user_constraints(self, raise_exception=True): result = dict() for c in self.raffle.constraints.all(): constraint: ConstraintVerification = get_constraint(c.name)( - self.user_profile + self.user_profile, context={"request": self.request} ) constraint.response = c.response try: diff --git a/prizetap/views.py b/prizetap/views.py index d28cb869..cdcb4d27 100644 --- a/prizetap/views.py +++ b/prizetap/views.py @@ -82,7 +82,10 @@ def post(self, request, pk): ) validator = RaffleEnrollmentValidator( - user_profile=user_profile, raffle=raffle, raffle_data=raffle_data + user_profile=user_profile, + raffle=raffle, + raffle_data=raffle_data, + request=request, ) validator.is_valid(self.request.data) @@ -192,7 +195,10 @@ def get(self, request, raffle_pk): reversed_constraints = raffle.reversed_constraints_list response_constraints = [] validator = RaffleEnrollmentValidator( - user_profile=user_profile, raffle=raffle, raffle_data=raffle_data + user_profile=user_profile, + raffle=raffle, + raffle_data=raffle_data, + request=request, ) validated_constraints = validator.check_user_constraints(raise_exception=False) diff --git a/start_dev.sh b/start_dev.sh index 63d53e96..8d8604fd 100755 --- a/start_dev.sh +++ b/start_dev.sh @@ -1,5 +1,5 @@ #!/bin/bash python manage.py collectstatic --noinput python manage.py migrate -python manage.py runserver 0.0.0.0:5678 & +python manage.py runserver 0.0.0.0:5678 celery -A brightIDfaucet worker -B \ No newline at end of file diff --git a/tokenTap/migrations/0062_alter_constraint_name.py b/tokenTap/migrations/0062_alter_constraint_name.py new file mode 100644 index 00000000..1ba8a107 --- /dev/null +++ b/tokenTap/migrations/0062_alter_constraint_name.py @@ -0,0 +1,31 @@ +# Generated by Django 4.0.4 on 2024-08-25 09:12 + +from django.db import migrations, models + + + +def create_tokentap_constraint(apps, schema_editor): + Constraint = apps.get_model("tokenTap", "Constraint") + + Constraint.objects.create( + name="core.HasVerifiedCloudflareCaptcha", + description="HasVerifiedCloudflareCaptcha", + title="Passed Cloudflare Captcha", + type="VER", + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('tokenTap', '0061_alter_constraint_name'), + ] + + operations = [ + migrations.AlterField( + model_name='constraint', + name='name', + field=models.CharField(choices=[('core.BrightIDMeetVerification', 'BrightIDMeetVerification'), ('core.BrightIDAuraVerification', 'BrightIDAuraVerification'), ('core.HasNFTVerification', 'HasNFTVerification'), ('core.HasTokenVerification', 'HasTokenVerification'), ('core.HasTokenTransferVerification', 'HasTokenTransferVerification'), ('core.AllowListVerification', 'AllowListVerification'), ('core.HasENSVerification', 'HasENSVerification'), ('core.HasLensProfile', 'HasLensProfile'), ('core.IsFollowingLensUser', 'IsFollowingLensUser'), ('core.BeFollowedByLensUser', 'BeFollowedByLensUser'), ('core.DidMirrorOnLensPublication', 'DidMirrorOnLensPublication'), ('core.DidCollectLensPublication', 'DidCollectLensPublication'), ('core.HasMinimumLensPost', 'HasMinimumLensPost'), ('core.HasMinimumLensFollower', 'HasMinimumLensFollower'), ('core.BeFollowedByFarcasterUser', 'BeFollowedByFarcasterUser'), ('core.HasMinimumFarcasterFollower', 'HasMinimumFarcasterFollower'), ('core.DidLikedFarcasterCast', 'DidLikedFarcasterCast'), ('core.DidRecastFarcasterCast', 'DidRecastFarcasterCast'), ('core.IsFollowingFarcasterUser', 'IsFollowingFarcasterUser'), ('core.HasFarcasterProfile', 'HasFarcasterProfile'), ('core.BeAttestedBy', 'BeAttestedBy'), ('core.Attest', 'Attest'), ('core.HasDonatedOnGitcoin', 'HasDonatedOnGitcoin'), ('core.HasMinimumHumanityScore', 'HasMinimumHumanityScore'), ('core.HasGitcoinPassportProfile', 'HasGitcoinPassportProfile'), ('core.IsFollowingFarcasterChannel', 'IsFollowingFarcasterChannel'), ('core.BridgeEthToArb', 'BridgeEthToArb'), ('core.IsFollowinTwitterUser', 'IsFollowinTwitterUser'), ('core.BeFollowedByTwitterUser', 'BeFollowedByTwitterUser'), ('core.DidRetweetTweet', 'DidRetweetTweet'), ('core.DidQuoteTweet', 'DidQuoteTweet'), ('core.HasMuonNode', 'HasMuonNode'), ('core.DelegateArb', 'DelegateArb'), ('core.DelegateOP', 'DelegateOP'), ('core.DidDelegateArbToAddress', 'DidDelegateArbToAddress'), ('core.DidDelegateOPToAddress', 'DidDelegateOPToAddress'), ('core.GLMStakingVerification', 'GLMStakingVerification'), ('core.IsFollowingTwitterBatch', 'IsFollowingTwitterBatch'), ('core.IsFollowingFarcasterBatch', 'IsFollowingFarcasterBatch'), ('core.HasVerifiedCloudflareCaptcha', 'HasVerifiedCloudflareCaptcha'), ('tokenTap.OncePerMonthVerification', 'OncePerMonthVerification'), ('tokenTap.OnceInALifeTimeVerification', 'OnceInALifeTimeVerification'), ('faucet.OptimismHasClaimedGasConstraint', 'OptimismHasClaimedGasConstraint')], max_length=255, unique=True), + ), + migrations.RunPython(create_tokentap_constraint), + ] diff --git a/tokenTap/validators.py b/tokenTap/validators.py index c10db4c6..315e4643 100644 --- a/tokenTap/validators.py +++ b/tokenTap/validators.py @@ -38,11 +38,17 @@ def is_valid(self, data): class TokenDistributionValidator: def __init__( - self, td: TokenDistribution, user_profile: UserProfile, td_data: dict + self, + td: TokenDistribution, + user_profile: UserProfile, + td_data: dict, + *args, + **kwargs, ) -> None: self.td = td self.td_data = td_data self.user_profile = user_profile + self.request = kwargs.get("request") def check_user_permissions(self, raise_exception=True): try: @@ -54,7 +60,8 @@ def check_user_permissions(self, raise_exception=True): result = dict() for c in self.td.constraints.all(): constraint: ConstraintVerification = get_constraint(c.name)( - self.user_profile + self.user_profile, + context={"request": self.request} ) constraint.response = c.response try: diff --git a/tokenTap/views.py b/tokenTap/views.py index 0467b642..8a60af85 100644 --- a/tokenTap/views.py +++ b/tokenTap/views.py @@ -133,7 +133,10 @@ def post(self, request, *args, **kwargs): pass validator = TokenDistributionValidator( - token_distribution, user_profile, td_data + token_distribution, + user_profile, + td_data, + request=request, ) validator.is_valid() @@ -184,7 +187,9 @@ def get(self, request, td_id): reversed_constraints = td.reversed_constraints_list response_constraints = [] - validator = TokenDistributionValidator(td, user_profile, td_data) + validator = TokenDistributionValidator( + td, user_profile, td_data, request=request + ) validated_constraints = validator.check_user_permissions(raise_exception=False) for c_pk, data in validated_constraints.items(): response_constraints.append(