deploy login & reCaptcha
This commit is contained in:
@@ -9,8 +9,10 @@ https://docs.djangoproject.com/en/5.2/topics/settings/
|
|||||||
For the full list of settings and their values, see
|
For the full list of settings and their values, see
|
||||||
https://docs.djangoproject.com/en/5.2/ref/settings/
|
https://docs.djangoproject.com/en/5.2/ref/settings/
|
||||||
"""
|
"""
|
||||||
|
import os.path
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from datetime import timedelta
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
@@ -39,8 +41,12 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
"corsheaders",
|
"corsheaders",
|
||||||
|
'rest_framework_simplejwt',
|
||||||
'apps.authentication.apps.AuthenticationConfig',
|
'apps.authentication.apps.AuthenticationConfig',
|
||||||
'apps.authorization.apps.AuthorizationConfig',
|
'apps.authorization.apps.AuthorizationConfig',
|
||||||
|
'rest_captcha',
|
||||||
|
'captcha',
|
||||||
|
'apps.captcha_app.apps.CaptchaAppConfig'
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@@ -94,12 +100,81 @@ REST_FRAMEWORK = {
|
|||||||
'rest_framework.permissions.IsAuthenticated',
|
'rest_framework.permissions.IsAuthenticated',
|
||||||
),
|
),
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
|
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||||
'rest_framework.authentication.SessionAuthentication',
|
'rest_framework.authentication.SessionAuthentication',
|
||||||
'rest_framework.authentication.BasicAuthentication',
|
'rest_framework.authentication.BasicAuthentication',
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SIMPLE_JWT = {
|
||||||
|
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=5),
|
||||||
|
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
|
||||||
|
"ROTATE_REFRESH_TOKENS": False,
|
||||||
|
"BLACKLIST_AFTER_ROTATION": False,
|
||||||
|
"UPDATE_LAST_LOGIN": False,
|
||||||
|
|
||||||
|
"ALGORITHM": "HS256",
|
||||||
|
"SIGNING_KEY": settings.SECRET_KEY,
|
||||||
|
"VERIFYING_KEY": "",
|
||||||
|
"AUDIENCE": None,
|
||||||
|
"ISSUER": None,
|
||||||
|
"JSON_ENCODER": None,
|
||||||
|
"JWK_URL": None,
|
||||||
|
"LEEWAY": 0,
|
||||||
|
|
||||||
|
"AUTH_HEADER_TYPES": ("Bearer",),
|
||||||
|
"AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",
|
||||||
|
"USER_ID_FIELD": "id",
|
||||||
|
"USER_ID_CLAIM": "user_id",
|
||||||
|
"USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule",
|
||||||
|
|
||||||
|
"AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
|
||||||
|
"TOKEN_TYPE_CLAIM": "token_type",
|
||||||
|
"TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser",
|
||||||
|
|
||||||
|
"JTI_CLAIM": "jti",
|
||||||
|
|
||||||
|
"SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
|
||||||
|
"SLIDING_TOKEN_LIFETIME": timedelta(minutes=5),
|
||||||
|
"SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),
|
||||||
|
|
||||||
|
"TOKEN_OBTAIN_SERIALIZER": 'apps.authentication.api.v1.jwt_serializer.CustomizedTokenObtainPairSerializer',
|
||||||
|
"TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSerializer",
|
||||||
|
"TOKEN_VERIFY_SERIALIZER": "rest_framework_simplejwt.serializers.TokenVerifySerializer",
|
||||||
|
"TOKEN_BLACKLIST_SERIALIZER": "rest_framework_simplejwt.serializers.TokenBlacklistSerializer",
|
||||||
|
"SLIDING_TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainSlidingSerializer",
|
||||||
|
"SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer",
|
||||||
|
}
|
||||||
|
|
||||||
|
CACHES = {
|
||||||
|
'default': {
|
||||||
|
'BACKEND': 'django_redis.cache.RedisCache',
|
||||||
|
'LOCATION': 'redis://127.0.0.1:6379/1', # Use the appropriate Redis server URL
|
||||||
|
'OPTIONS': {
|
||||||
|
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'memcache': {
|
||||||
|
"BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
|
||||||
|
"LOCATION": "127.0.0.1:11211",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
REST_CAPTCHA = {
|
||||||
|
'CAPTCHA_CACHE': 'default',
|
||||||
|
'CAPTCHA_TIMEOUT': 300, # 5 minutes
|
||||||
|
'CAPTCHA_LENGTH': 6,
|
||||||
|
'CAPTCHA_FONT_SIZE': 35,
|
||||||
|
'CAPTCHA_IMAGE_SIZE': (90, 20),
|
||||||
|
'CAPTCHA_LETTER_ROTATION': (-35, 35),
|
||||||
|
'CAPTCHA_FOREGROUND_COLOR': '#000000',
|
||||||
|
'CAPTCHA_BACKGROUND_COLOR': '#ffffff',
|
||||||
|
# 'CAPTCHA_FONT_PATH': 'apps.authentication.api.v1.serializers.captcha.FONT_PATH',
|
||||||
|
'CAPTCHA_CACHE_KEY': 'rest_captcha_{key}.{version}',
|
||||||
|
'FILTER_FUNCTION': 'rest_captcha.captcha.filter_default',
|
||||||
|
'NOISE_FUNCTION': 'apps.captcha_app.api.v1.serializers.noise_default'
|
||||||
|
}
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,10 @@ Including another URLconf
|
|||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
"""
|
"""
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path
|
from django.urls import path, include
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
|
path('auth/', include('apps.authentication.urls')),
|
||||||
|
path('', include('apps.captcha_app.api.v1.urls')),
|
||||||
]
|
]
|
||||||
|
|||||||
0
apps/__init__.py
Normal file
0
apps/__init__.py
Normal file
0
apps/authentication/api/__init__.py
Normal file
0
apps/authentication/api/__init__.py
Normal file
0
apps/authentication/api/v1/__init__.py
Normal file
0
apps/authentication/api/v1/__init__.py
Normal file
27
apps/authentication/api/v1/api.py
Normal file
27
apps/authentication/api/v1/api.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from apps.authentication.api.v1.serializers.jwt import CustomizedTokenObtainPairSerializer
|
||||||
|
from rest_framework_simplejwt.views import TokenObtainPairView
|
||||||
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from apps.authentication.models import User
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
|
||||||
|
class CustomizedTokenObtainPairView(TokenObtainPairView):
|
||||||
|
serializer_class = CustomizedTokenObtainPairSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class Authentication(ModelViewSet):
|
||||||
|
queryset = User
|
||||||
|
serializer_class = ''
|
||||||
|
permission_classes = ''
|
||||||
|
|
||||||
|
@action(
|
||||||
|
methods=['post', ],
|
||||||
|
detail=False,
|
||||||
|
name='login',
|
||||||
|
url_name='login',
|
||||||
|
url_path='login'
|
||||||
|
)
|
||||||
|
@transaction.atomic
|
||||||
|
def login(self, request):
|
||||||
|
pass
|
||||||
0
apps/authentication/api/v1/serializers/__init__.py
Normal file
0
apps/authentication/api/v1/serializers/__init__.py
Normal file
55
apps/authentication/api/v1/serializers/jwt.py
Normal file
55
apps/authentication/api/v1/serializers/jwt.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
||||||
|
from apps.captcha_app import exceptions as captcha_exception
|
||||||
|
from rest_framework_simplejwt.settings import api_settings
|
||||||
|
from django.contrib.auth.models import update_last_login
|
||||||
|
from rest_framework import exceptions
|
||||||
|
from django.core.cache import cache
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class CustomizedTokenObtainPairSerializer(TokenObtainPairSerializer): # noqa
|
||||||
|
"""
|
||||||
|
customize jwt token
|
||||||
|
'set new variables in generated token'
|
||||||
|
"""
|
||||||
|
|
||||||
|
def validate(self, attrs: dict[str, Any]) -> dict[str, str]:
|
||||||
|
"""
|
||||||
|
override validate method to add more conditions
|
||||||
|
"""
|
||||||
|
captcha_code, captcha_key = attrs['captcha_code'], attrs['captcha_key']
|
||||||
|
|
||||||
|
if captcha_code != cache.get(captcha_key) or captcha_code not in attrs.keys():
|
||||||
|
raise captcha_exception.CaptchaFailed()
|
||||||
|
|
||||||
|
data = super().validate(attrs)
|
||||||
|
|
||||||
|
refresh = self.get_token(self.user)
|
||||||
|
|
||||||
|
data["refresh"] = str(refresh)
|
||||||
|
data["access"] = str(refresh.access_token)
|
||||||
|
data["otp_status"] = self.user.otp_status
|
||||||
|
|
||||||
|
if not self.user.is_active:
|
||||||
|
raise exceptions.AuthenticationFailed(
|
||||||
|
self.error_messages["no_active_account"],
|
||||||
|
"no_active_account",
|
||||||
|
)
|
||||||
|
|
||||||
|
if api_settings.UPDATE_LAST_LOGIN:
|
||||||
|
update_last_login(None, self.user)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_token(cls, user):
|
||||||
|
"""
|
||||||
|
set variables in encoded jwt token
|
||||||
|
"""
|
||||||
|
|
||||||
|
token = super().get_token(user)
|
||||||
|
|
||||||
|
# Add custom claims
|
||||||
|
token['name'] = user.username
|
||||||
|
|
||||||
|
return token
|
||||||
14
apps/authentication/api/v1/urls.py
Normal file
14
apps/authentication/api/v1/urls.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from django.urls import path, include
|
||||||
|
from rest_framework.routers import DefaultRouter
|
||||||
|
from rest_framework_simplejwt.views import (
|
||||||
|
TokenObtainPairView,
|
||||||
|
TokenRefreshView,
|
||||||
|
TokenVerifyView
|
||||||
|
)
|
||||||
|
from .api import CustomizedTokenObtainPairView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('login/', CustomizedTokenObtainPairView.as_view(), name='token_obtain_pair'),
|
||||||
|
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
||||||
|
path('token/verify/', TokenVerifyView.as_view(), name='token_verify'),
|
||||||
|
]
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Generated by Django 5.0 on 2025-05-03 07:35
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('authentication', '0002_alter_user_id'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='province',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='user_province', to='authentication.province'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='City',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('create_date', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('modify_date', models.DateTimeField(auto_now=True)),
|
||||||
|
('trash', models.BooleanField(default=False)),
|
||||||
|
('name', models.CharField(max_length=50)),
|
||||||
|
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_createdby', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('modified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_modifiedby', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='organization',
|
||||||
|
name='city',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='city_organization', to='authentication.city'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='city',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='user_city', to='authentication.city'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
apps/authentication/migrations/0004_user_otp_status.py
Normal file
18
apps/authentication/migrations/0004_user_otp_status.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0 on 2025-05-03 08:20
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('authentication', '0003_user_province_city_organization_city_user_city'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='otp_status',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -9,6 +9,19 @@ class User(AbstractUser, BaseModel):
|
|||||||
mobile = models.CharField(max_length=18)
|
mobile = models.CharField(max_length=18)
|
||||||
national_code = models.CharField(max_length=16)
|
national_code = models.CharField(max_length=16)
|
||||||
photo = models.CharField(max_length=50)
|
photo = models.CharField(max_length=50)
|
||||||
|
province = models.ForeignKey(
|
||||||
|
'Province',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='user_province',
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
city = models.ForeignKey(
|
||||||
|
'City',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='user_city',
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
otp_status = models.BooleanField(default=False)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.username} {self.last_name}-{self.last_login}'
|
return f'{self.username} {self.last_name}-{self.last_login}'
|
||||||
@@ -27,6 +40,16 @@ class Province(BaseModel):
|
|||||||
super(Province, self).save(*args, **kwargs)
|
super(Province, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class City(BaseModel):
|
||||||
|
name = models.CharField(max_length=50)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.name}'
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
super(City, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Organization(BaseModel):
|
class Organization(BaseModel):
|
||||||
name = models.CharField(max_length=50)
|
name = models.CharField(max_length=50)
|
||||||
type = models.CharField(max_length=50)
|
type = models.CharField(max_length=50)
|
||||||
@@ -36,6 +59,12 @@ class Organization(BaseModel):
|
|||||||
related_name="province_organization",
|
related_name="province_organization",
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
|
city = models.ForeignKey(
|
||||||
|
'City',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='city_organization',
|
||||||
|
null=True
|
||||||
|
)
|
||||||
parent_organization = models.ForeignKey(
|
parent_organization = models.ForeignKey(
|
||||||
'Organization',
|
'Organization',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
|||||||
10
apps/authentication/urls.py
Normal file
10
apps/authentication/urls.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from django.urls import path, include
|
||||||
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
|
router = DefaultRouter()
|
||||||
|
router.register('', '', basename='')
|
||||||
|
|
||||||
|
app_name = "authentication"
|
||||||
|
urlpatterns = [
|
||||||
|
path('api/v1/', include('apps.authentication.api.v1.urls')),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0 on 2025-05-03 07:42
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('authorization', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='organizationrole',
|
||||||
|
name='otp_status',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.0 on 2025-05-03 08:20
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('authorization', '0002_organizationrole_otp_status'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='organizationrole',
|
||||||
|
name='otp_status',
|
||||||
|
),
|
||||||
|
]
|
||||||
0
apps/captcha_app/__init__.py
Normal file
0
apps/captcha_app/__init__.py
Normal file
3
apps/captcha_app/admin.py
Normal file
3
apps/captcha_app/admin.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
0
apps/captcha_app/api/v1/api.py
Normal file
0
apps/captcha_app/api/v1/api.py
Normal file
15
apps/captcha_app/api/v1/serializers.py
Normal file
15
apps/captcha_app/api/v1/serializers.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from rest_captcha.serializers import RestCaptchaSerializer
|
||||||
|
from rest_captcha import utils
|
||||||
|
from rest_captcha.settings import api_settings
|
||||||
|
from django.core.cache import caches
|
||||||
|
|
||||||
|
cache = caches[api_settings.CAPTCHA_CACHE]
|
||||||
|
|
||||||
|
|
||||||
|
def noise_default(image, draw):
|
||||||
|
draw = utils.noise_dots(draw, image, api_settings.CAPTCHA_FOREGROUND_COLOR)
|
||||||
|
# draw = utils.noise_arcs(draw, image, api_settings.CAPTCHA_FOREGROUND_COLOR)
|
||||||
|
|
||||||
|
|
||||||
|
class HumanOnlyDataSerializer(RestCaptchaSerializer): # noqa
|
||||||
|
pass
|
||||||
8
apps/captcha_app/api/v1/urls.py
Normal file
8
apps/captcha_app/api/v1/urls.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from django.urls import path, include
|
||||||
|
from .views import CustomizeRestCaptchaView
|
||||||
|
|
||||||
|
app_name = 'captcha_app'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('captcha/', CustomizeRestCaptchaView.as_view(), name='captcha')
|
||||||
|
]
|
||||||
71
apps/captcha_app/api/v1/utils.py
Normal file
71
apps/captcha_app/api/v1/utils.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import os.path
|
||||||
|
import random
|
||||||
|
|
||||||
|
from rest_captcha.settings import api_settings as settings
|
||||||
|
from django.conf import settings as django_setting
|
||||||
|
from django.core.cache import caches
|
||||||
|
from rest_captcha import captcha
|
||||||
|
from PIL import ImageFont, ImageDraw, Image
|
||||||
|
from io import BytesIO as StringIO
|
||||||
|
|
||||||
|
cache = caches[settings.CAPTCHA_CACHE]
|
||||||
|
|
||||||
|
path = os.path.dirname(__file__) + '/' # noqa
|
||||||
|
|
||||||
|
|
||||||
|
def random_char_challenge(length):
|
||||||
|
chars = '123456789'
|
||||||
|
ret = ''
|
||||||
|
for i in range(length):
|
||||||
|
ret += random.choice(chars)
|
||||||
|
return ret.upper()
|
||||||
|
|
||||||
|
|
||||||
|
def generate_image(word):
|
||||||
|
font = ImageFont.load_default()
|
||||||
|
size = settings.CAPTCHA_IMAGE_SIZE
|
||||||
|
|
||||||
|
xpos = 2
|
||||||
|
from_top = 4
|
||||||
|
|
||||||
|
image = captcha.makeimg(size)
|
||||||
|
|
||||||
|
for char in word:
|
||||||
|
fgimage = Image.new('RGB', size, settings.CAPTCHA_FOREGROUND_COLOR)
|
||||||
|
charimage = Image.new('L', captcha.getsize(font, ' %s ' % char), '#000000')
|
||||||
|
chardraw = ImageDraw.Draw(charimage)
|
||||||
|
chardraw.text((0, 0), char, font=font, fill='#ffffff')
|
||||||
|
|
||||||
|
charimage = charimage.crop(charimage.getbbox())
|
||||||
|
maskimage = Image.new('L', size)
|
||||||
|
|
||||||
|
xpos2 = xpos + charimage.size[0]
|
||||||
|
from_top2 = from_top + charimage.size[1]
|
||||||
|
maskimage.paste(charimage, (xpos, from_top, xpos2, from_top2))
|
||||||
|
size = maskimage.size
|
||||||
|
image = Image.composite(fgimage, image, maskimage)
|
||||||
|
xpos = xpos + 2 + charimage.size[0]
|
||||||
|
|
||||||
|
if settings.CAPTCHA_IMAGE_SIZE:
|
||||||
|
# centering captcha on the image
|
||||||
|
tmpimg = captcha.makeimg(size)
|
||||||
|
xpos2 = int((size[0] - xpos) / 2)
|
||||||
|
from_top2 = int((size[1] - charimage.size[1]) / 2 - from_top)
|
||||||
|
tmpimg.paste(image, (xpos2, from_top2))
|
||||||
|
image = tmpimg.crop((0, 0, size[0], size[1]))
|
||||||
|
else:
|
||||||
|
image = image.crop((0, 0, xpos + 1, size[1]))
|
||||||
|
|
||||||
|
draw = ImageDraw.Draw(image)
|
||||||
|
|
||||||
|
# settings.FILTER_FUNCTION(image)
|
||||||
|
settings.NOISE_FUNCTION(image, draw)
|
||||||
|
|
||||||
|
out = StringIO()
|
||||||
|
image.save(out, 'PNG')
|
||||||
|
# image.save('ss.png', 'PNG')
|
||||||
|
content = out.getvalue()
|
||||||
|
out.seek(0)
|
||||||
|
out.close()
|
||||||
|
|
||||||
|
return content
|
||||||
43
apps/captcha_app/api/v1/views.py
Normal file
43
apps/captcha_app/api/v1/views.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
from rest_captcha.settings import api_settings as settings
|
||||||
|
from rest_framework import views
|
||||||
|
import uuid
|
||||||
|
from rest_captcha import utils
|
||||||
|
import base64
|
||||||
|
from rest_framework import response
|
||||||
|
from .utils import (
|
||||||
|
random_char_challenge,
|
||||||
|
generate_image
|
||||||
|
)
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
|
|
||||||
|
class CustomizeRestCaptchaView(views.APIView):
|
||||||
|
"""
|
||||||
|
overriding RestCaptchaView to generate captcha image
|
||||||
|
"""
|
||||||
|
authentication_classes = () # noqa
|
||||||
|
permission_classes = ()
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
key = str(uuid.uuid4())
|
||||||
|
value = random_char_challenge(settings.CAPTCHA_LENGTH)
|
||||||
|
cache_key = utils.get_cache_key(key)
|
||||||
|
print(cache_key)
|
||||||
|
cache.set(cache_key, value, settings.CAPTCHA_TIMEOUT)
|
||||||
|
print(cache.get(cache_key))
|
||||||
|
|
||||||
|
# generate image
|
||||||
|
image_bytes = generate_image(value)
|
||||||
|
image_b64 = base64.b64encode(image_bytes)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
settings.CAPTCHA_KEY: key,
|
||||||
|
settings.CAPTCHA_IMAGE: image_b64,
|
||||||
|
'image_type': 'image/png',
|
||||||
|
'image_decode': 'base64'
|
||||||
|
}
|
||||||
|
return response.Response(data)
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
key = cache.get("rest_captcha_9e3ca166-c2f8-41e8-8f19-aa6f520fc123.0")
|
||||||
|
return response.Response(key)
|
||||||
6
apps/captcha_app/apps.py
Normal file
6
apps/captcha_app/apps.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CaptchaAppConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'apps.captcha_app'
|
||||||
12
apps/captcha_app/exceptions.py
Normal file
12
apps/captcha_app/exceptions.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from rest_framework.exceptions import APIException
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
|
||||||
|
class CaptchaFailed(APIException):
|
||||||
|
"""
|
||||||
|
raised exception when user entered wrong captcha code
|
||||||
|
"""
|
||||||
|
status_code = status.HTTP_403_FORBIDDEN
|
||||||
|
default_detail = _('Wrong Captcha')
|
||||||
|
default_code = 'wrong captcha'
|
||||||
0
apps/pos/__init__.py
Normal file
0
apps/pos/__init__.py
Normal file
3
apps/pos/admin.py
Normal file
3
apps/pos/admin.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
0
apps/pos/api/v1/api.py
Normal file
0
apps/pos/api/v1/api.py
Normal file
0
apps/pos/api/v1/serializers.py
Normal file
0
apps/pos/api/v1/serializers.py
Normal file
0
apps/pos/api/v1/urls.py
Normal file
0
apps/pos/api/v1/urls.py
Normal file
6
apps/pos/apps.py
Normal file
6
apps/pos/apps.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PosConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'pos'
|
||||||
3
apps/pos/models.py
Normal file
3
apps/pos/models.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
3
apps/pos/tests.py
Normal file
3
apps/pos/tests.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
3
apps/pos/views.py
Normal file
3
apps/pos/views.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
@@ -37,7 +37,7 @@ oauth2-provider
|
|||||||
oauthlib
|
oauthlib
|
||||||
openpyxl
|
openpyxl
|
||||||
packaging
|
packaging
|
||||||
Pillow
|
pillow==9.5.0
|
||||||
prompt-toolkit
|
prompt-toolkit
|
||||||
psycopg2
|
psycopg2
|
||||||
psycopg2-binary
|
psycopg2-binary
|
||||||
@@ -63,4 +63,7 @@ tablib
|
|||||||
typing-extensions
|
typing-extensions
|
||||||
urllib3
|
urllib3
|
||||||
XlsxWriter
|
XlsxWriter
|
||||||
zipp
|
zipp
|
||||||
|
django-simple-captcha
|
||||||
|
django-rest-captcha
|
||||||
|
pymemcache
|
||||||
Reference in New Issue
Block a user