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
|
||||
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
|
||||
|
||||
|
||||
@@ -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
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)
|
||||
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,
|
||||
|
||||
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
|
||||
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
|
||||
Reference in New Issue
Block a user