Skip to content

Commit

Permalink
Merge pull request #605 from UnitapApp/feature/requirements/captcha
Browse files Browse the repository at this point in the history
added captcha requirement
  • Loading branch information
alimaktabi authored Aug 26, 2024
2 parents 0a83582 + ad76e29 commit 26d8223
Show file tree
Hide file tree
Showing 16 changed files with 234 additions and 13 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion brightIDfaucet/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]


Expand Down Expand Up @@ -259,4 +261,4 @@ def before_send(event, hint):
"djangorestframework_camel_case.parser.CamelCaseJSONParser",
),
}
CELERY_BROKER_URL = REDIS_URL
CELERY_BROKER_URL = REDIS_URL
1 change: 1 addition & 0 deletions core/constraints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
IsFollowingTwitterBatch,
IsFollowinTwitterUser,
)
from core.constraints.captcha import HasVerifiedCloudflareCaptcha


def get_constraint(constraint_label: str) -> ConstraintVerification:
Expand Down
3 changes: 2 additions & 1 deletion core/constraints/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions core/constraints/captcha.py
Original file line number Diff line number Diff line change
@@ -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
)
2 changes: 2 additions & 0 deletions core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -155,6 +156,7 @@ class Type(models.TextChoices):
GLMStakingVerification,
IsFollowingTwitterBatch,
IsFollowingFarcasterBatch,
HasVerifiedCloudflareCaptcha,
]

name = models.CharField(
Expand Down
24 changes: 24 additions & 0 deletions core/thirdpartyapp/cloudflare.py
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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


3 changes: 1 addition & 2 deletions faucet/migrations/0019_brightuser__last_verified_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import datetime
from django.db import migrations, models
from django.utils.timezone import utc


class Migration(migrations.Migration):
Expand All @@ -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)),
),
]
89 changes: 89 additions & 0 deletions prizetap/migrations/0076_alter_constraint_name.py
Original file line number Diff line number Diff line change
@@ -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),
]
3 changes: 2 additions & 1 deletion prizetap/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
10 changes: 8 additions & 2 deletions prizetap/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion start_dev.sh
Original file line number Diff line number Diff line change
@@ -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
31 changes: 31 additions & 0 deletions tokenTap/migrations/0062_alter_constraint_name.py
Original file line number Diff line number Diff line change
@@ -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),
]
11 changes: 9 additions & 2 deletions tokenTap/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
9 changes: 7 additions & 2 deletions tokenTap/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit 26d8223

Please sign in to comment.