deploy login & reCaptcha

This commit is contained in:
2025-05-04 15:24:28 +03:30
parent 3ab3fa2d13
commit 70fa849840
36 changed files with 494 additions and 5 deletions

View File

@@ -9,8 +9,10 @@ https://docs.djangoproject.com/en/5.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.2/ref/settings/
"""
import os.path
from pathlib import Path
from datetime import timedelta
from django.conf import settings
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
@@ -39,8 +41,12 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'rest_framework',
"corsheaders",
'rest_framework_simplejwt',
'apps.authentication.apps.AuthenticationConfig',
'apps.authorization.apps.AuthorizationConfig',
'rest_captcha',
'captcha',
'apps.captcha_app.apps.CaptchaAppConfig'
]
MIDDLEWARE = [
@@ -94,12 +100,81 @@ REST_FRAMEWORK = {
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication',
'rest_framework.authentication.SessionAuthentication',
'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
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators

View File

@@ -15,8 +15,10 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
from django.urls import path, include
urlpatterns = [
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
View File

View File

View File

View 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

View 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

View 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'),
]

View File

@@ -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'),
),
]

View 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),
),
]

View File

@@ -9,6 +9,19 @@ class User(AbstractUser, BaseModel):
mobile = models.CharField(max_length=18)
national_code = models.CharField(max_length=16)
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):
return f'{self.username} {self.last_name}-{self.last_login}'
@@ -27,6 +40,16 @@ class Province(BaseModel):
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):
name = models.CharField(max_length=50)
type = models.CharField(max_length=50)
@@ -36,6 +59,12 @@ class Organization(BaseModel):
related_name="province_organization",
null=True
)
city = models.ForeignKey(
'City',
on_delete=models.CASCADE,
related_name='city_organization',
null=True
)
parent_organization = models.ForeignKey(
'Organization',
on_delete=models.CASCADE,

View 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')),
]

View File

@@ -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),
),
]

View File

@@ -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',
),
]

View File

View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

View 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

View 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')
]

View 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

View 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
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class CaptchaAppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.captcha_app'

View 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
View File

3
apps/pos/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

0
apps/pos/api/v1/api.py Normal file
View File

View File

0
apps/pos/api/v1/urls.py Normal file
View File

6
apps/pos/apps.py Normal file
View 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
View File

@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

3
apps/pos/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
apps/pos/views.py Normal file
View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -37,7 +37,7 @@ oauth2-provider
oauthlib
openpyxl
packaging
Pillow
pillow==9.5.0
prompt-toolkit
psycopg2
psycopg2-binary
@@ -64,3 +64,6 @@ typing-extensions
urllib3
XlsxWriter
zipp
django-simple-captcha
django-rest-captcha
pymemcache

BIN
ss.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B