throttles
This commit is contained in:
parent
1f44f80a79
commit
280b8f1c2c
16 changed files with 185 additions and 57 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -6,3 +6,4 @@ delme*.py
|
||||||
.env
|
.env
|
||||||
/venv/
|
/venv/
|
||||||
oidc.key
|
oidc.key
|
||||||
|
/log/
|
||||||
|
|
|
||||||
|
|
@ -161,9 +161,6 @@ class User(AbstractUser):
|
||||||
return state.get_province_by_id(self.province)
|
return state.get_province_by_id(self.province)
|
||||||
|
|
||||||
def set_otp(self):
|
def set_otp(self):
|
||||||
if self.otp_expire and (self.otp_expire + timedelta(seconds=MAX_OTP_VALID_DURATION)) > timezone.now():
|
|
||||||
raise Exception(_('otp expire time not reached.'))
|
|
||||||
|
|
||||||
if not settings.SMS_SEND:
|
if not settings.SMS_SEND:
|
||||||
self._otp = '77501'
|
self._otp = '77501'
|
||||||
elif self.phone_number in settings.TEST_PHONENUMBERS:
|
elif self.phone_number in settings.TEST_PHONENUMBERS:
|
||||||
|
|
|
||||||
|
|
@ -70,16 +70,9 @@ class RequestOTPSerializer(serializers.ModelSerializer):
|
||||||
user = User.objects.create_user(**validated_data)
|
user = User.objects.create_user(**validated_data)
|
||||||
|
|
||||||
if not user.otp_is_valid():
|
if not user.otp_is_valid():
|
||||||
try:
|
|
||||||
user.set_otp()
|
user.set_otp()
|
||||||
except Exception as e:
|
|
||||||
raise UnprocessableEntity(_('otp expire time not reached'), code='opt_not_expired')
|
|
||||||
|
|
||||||
user.send_otp()
|
user.send_otp()
|
||||||
|
|
||||||
else:
|
|
||||||
raise UnprocessableEntity(_('otp expire time not reached yet'), code='opt_not_expired')
|
|
||||||
|
|
||||||
self.instance = user
|
self.instance = user
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ from apps.users.models import User
|
||||||
from apps.users.provinces_and_cities import State
|
from apps.users.provinces_and_cities import State
|
||||||
from apps.users.serializers import PublicUserSerializer, AccountSerializer, RequestOTPSerializer, RequestOTTSerializer, \
|
from apps.users.serializers import PublicUserSerializer, AccountSerializer, RequestOTPSerializer, RequestOTTSerializer, \
|
||||||
ChangePasswordSerializer, UserInquirySerializer
|
ChangePasswordSerializer, UserInquirySerializer
|
||||||
from utils.throttles import RequestOTPDayRateThrottle, RequestOTPMinRateThrottle
|
from utils.throttles import RequestOTPDayRateThrottle, RequestOTPMinRateThrottle, NumberedRequestOTPDayRateThrottle, NumberedRequestOTPMinRateThrottle
|
||||||
|
|
||||||
UserModel = get_user_model()
|
UserModel = get_user_model()
|
||||||
|
|
||||||
|
|
@ -90,7 +90,10 @@ class RequestOTPView(generics.CreateAPIView):
|
||||||
permission_classes = []
|
permission_classes = []
|
||||||
serializer_class = RequestOTPSerializer
|
serializer_class = RequestOTPSerializer
|
||||||
required_scopes = []
|
required_scopes = []
|
||||||
throttle_classes = [RequestOTPMinRateThrottle, RequestOTPDayRateThrottle]
|
throttle_classes = [RequestOTPMinRateThrottle,
|
||||||
|
RequestOTPDayRateThrottle,
|
||||||
|
NumberedRequestOTPDayRateThrottle,
|
||||||
|
NumberedRequestOTPMinRateThrottle]
|
||||||
|
|
||||||
class RequestOTTView(generics.CreateAPIView):
|
class RequestOTTView(generics.CreateAPIView):
|
||||||
permission_classes = [IsAuthenticatedOrTokenHasScope]
|
permission_classes = [IsAuthenticatedOrTokenHasScope]
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,6 @@ import os
|
||||||
|
|
||||||
from django.core.asgi import get_asgi_application
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'accounts.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'main.settings')
|
||||||
|
|
||||||
application = get_asgi_application()
|
application = get_asgi_application()
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
import os
|
import os
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'accounts.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'main.settings')
|
||||||
|
|
||||||
from utils.broker import get_rpc_broker_consumer
|
from utils.broker import get_rpc_broker_consumer
|
||||||
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
# from django.conf import settings
|
# from django.conf import settings
|
||||||
#
|
#
|
||||||
# # set the default Django settings module for the 'celery' program.
|
# # set the default Django settings module for the 'celery' program.
|
||||||
# # os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'accounts.settings')
|
# # os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'main.settings')
|
||||||
#
|
#
|
||||||
# app = Celery('notification_service')
|
# app = Celery('notification_service')
|
||||||
#
|
#
|
||||||
0
main/other_settings/__init__.py
Normal file
0
main/other_settings/__init__.py
Normal file
141
main/other_settings/logging.py
Normal file
141
main/other_settings/logging.py
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
def get_settings(key):
|
||||||
|
from django.conf import settings
|
||||||
|
return getattr(settings, 'BASE_DIR')
|
||||||
|
|
||||||
|
BASE_DIR = get_settings('BASE_DIR')
|
||||||
|
|
||||||
|
LOG_DIR = BASE_DIR / 'log'
|
||||||
|
if not os.path.exists(LOG_DIR):
|
||||||
|
os.makedirs(LOG_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
LOGGING = {
|
||||||
|
'version': 1,
|
||||||
|
'disable_existing_loggers': False,
|
||||||
|
'formatters': {
|
||||||
|
'verbose': {
|
||||||
|
'format': '{asctime} [{levelname}] {name} {module} {process:d} {thread:d} {message}',
|
||||||
|
'style': '{',
|
||||||
|
},
|
||||||
|
'standard': {
|
||||||
|
'format': '{asctime} [{levelname}] {name}: {message}',
|
||||||
|
'style': '{',
|
||||||
|
},
|
||||||
|
'simple': {
|
||||||
|
'format': '{levelname} {message}',
|
||||||
|
'style': '{',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'filters': {
|
||||||
|
'require_debug_true': {
|
||||||
|
'()': 'django.utils.log.RequireDebugTrue',
|
||||||
|
},
|
||||||
|
'require_debug_false': {
|
||||||
|
'()': 'django.utils.log.RequireDebugFalse',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'handlers': {
|
||||||
|
# هندلر برای نوشتن در فایل
|
||||||
|
'file': {
|
||||||
|
'level': 'INFO',
|
||||||
|
'class': 'logging.handlers.TimedRotatingFileHandler',
|
||||||
|
'filename': os.path.join(LOG_DIR, 'accounts.log'),
|
||||||
|
# 'maxBytes': 1024 * 1024 * 10, # 10 MB
|
||||||
|
'when': 'midnight',
|
||||||
|
'interval': 5,
|
||||||
|
'backupCount': 30,
|
||||||
|
'formatter': 'verbose',
|
||||||
|
'encoding': 'utf-8',
|
||||||
|
},
|
||||||
|
# هندلر برای خطاها در فایل جداگانه
|
||||||
|
'error_file': {
|
||||||
|
'level': 'ERROR',
|
||||||
|
'class': 'logging.handlers.TimedRotatingFileHandler',
|
||||||
|
'filename': os.path.join(LOG_DIR, 'errors.log'),
|
||||||
|
# 'maxBytes': 1024 * 1024 * 10,
|
||||||
|
'when': 'midnight',
|
||||||
|
'interval': 5,
|
||||||
|
'backupCount': 30,
|
||||||
|
'formatter': 'verbose',
|
||||||
|
'encoding': 'utf-8',
|
||||||
|
},
|
||||||
|
# هندلر برای کنسول (فقط در حالت DEBUG)
|
||||||
|
'console': {
|
||||||
|
'level': 'DEBUG',
|
||||||
|
'filters': ['require_debug_true'],
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
'formatter': 'simple',
|
||||||
|
},
|
||||||
|
# هندلر برای کنسول همیشه فعال (حتی در production)
|
||||||
|
'console_always': {
|
||||||
|
'level': 'INFO',
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
'formatter': 'standard',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'loggers': {
|
||||||
|
# لاگر ریشه
|
||||||
|
'': { # empty string = root logger
|
||||||
|
'handlers': ['console', 'file', 'error_file'],
|
||||||
|
'level': 'INFO',
|
||||||
|
'propagate': True,
|
||||||
|
},
|
||||||
|
# لاگر اختصاصی برای Django
|
||||||
|
'django': {
|
||||||
|
'handlers': ['console', 'file'],
|
||||||
|
'level': 'INFO',
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
# لاگر اختصاصی برای درخواستها
|
||||||
|
'django.request': {
|
||||||
|
'handlers': ['file', 'error_file', 'console'],
|
||||||
|
'level': 'ERROR',
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
# # لاگر اختصاصی برای برنامه خودتان
|
||||||
|
# 'root': {
|
||||||
|
# 'handlers': ['console', 'file', 'error_file'],
|
||||||
|
# 'level': 'DEBUG',
|
||||||
|
# 'propagate': False,
|
||||||
|
# },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# LOKI_BASE_PUBLIC_URL = config('LOKI_BASE_PUBLIC_URL', default=None, cast=str)
|
||||||
|
# {
|
||||||
|
# 'version': 1,
|
||||||
|
# 'disable_existing_loggers': False,
|
||||||
|
# 'formatters': {
|
||||||
|
# 'loki': {
|
||||||
|
# 'class': 'utils.logs.LokiFormatter', # required
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
#
|
||||||
|
# 'handlers': {
|
||||||
|
# 'loki': {
|
||||||
|
# 'level': 'DEBUG', # Log level. Required
|
||||||
|
# 'class': 'utils.logs.LokiHandler', # Required
|
||||||
|
# 'formatter': 'loki', # Loki formatter. Required
|
||||||
|
# 'timeout': 2, # Post request timeout, default is 0.5. Optional
|
||||||
|
# 'url': f'{LOKI_BASE_PUBLIC_URL}/loki/api/v1/push', # Loki url. Defaults to localhost. Optional.
|
||||||
|
# # 'auth': ("user", "password"), # Basic auth to authenticate with loki. Default is None (i.e. no auth). Optional
|
||||||
|
# 'tags': {"app": "accounts"}, # Tags / Labels to attach to the log. Optional, but strongly encoraged to use.
|
||||||
|
# 'mode': 'thread',
|
||||||
|
# # Push mode. Can be 'sync' or 'thread'. Sync is blocking, thread is non-blocking. Defaults to sync. Optional.
|
||||||
|
# },
|
||||||
|
# 'console': {
|
||||||
|
# 'level': 'DEBUG',
|
||||||
|
# 'class': 'logging.StreamHandler',
|
||||||
|
# # 'formatter': 'verbose',
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
# 'loggers': {
|
||||||
|
# '': {
|
||||||
|
# 'handlers': ['console', 'loki'],
|
||||||
|
# 'level': 'INFO',
|
||||||
|
# 'propagate': True,
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
# }
|
||||||
|
|
@ -121,7 +121,9 @@ REST_FRAMEWORK = {
|
||||||
# ],
|
# ],
|
||||||
'DEFAULT_THROTTLE_RATES': {
|
'DEFAULT_THROTTLE_RATES': {
|
||||||
'otp_min': '2/min',
|
'otp_min': '2/min',
|
||||||
'otp_day': '200/day',
|
'otp_day': '50/day',
|
||||||
|
'numbered_otp_min': '1/min',
|
||||||
|
'numbered_otp_day': '10/day',
|
||||||
},
|
},
|
||||||
'EXCEPTION_HANDLER': 'utils.exceptions.exception_handler',
|
'EXCEPTION_HANDLER': 'utils.exceptions.exception_handler',
|
||||||
}
|
}
|
||||||
|
|
@ -135,7 +137,7 @@ SPECTACULAR_SETTINGS = {
|
||||||
"PARSER_WHITELIST": ["rest_framework.parsers.JSONParser"],
|
"PARSER_WHITELIST": ["rest_framework.parsers.JSONParser"],
|
||||||
}
|
}
|
||||||
|
|
||||||
ROOT_URLCONF = 'accounts.urls'
|
ROOT_URLCONF = 'main.urls'
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
|
|
@ -159,7 +161,7 @@ AUTHENTICATION_BACKENDS = (
|
||||||
'django.contrib.auth.backends.ModelBackend',
|
'django.contrib.auth.backends.ModelBackend',
|
||||||
)
|
)
|
||||||
|
|
||||||
WSGI_APPLICATION = 'accounts.wsgi.application'
|
WSGI_APPLICATION = 'main.wsgi.application'
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
|
||||||
|
|
@ -293,40 +295,7 @@ CACHES = {
|
||||||
|
|
||||||
LOKI_BASE_PUBLIC_URL = config('LOKI_BASE_PUBLIC_URL', default=None, cast=str)
|
LOKI_BASE_PUBLIC_URL = config('LOKI_BASE_PUBLIC_URL', default=None, cast=str)
|
||||||
|
|
||||||
LOGGING = {
|
from main.other_settings.logging import LOGGING
|
||||||
'version': 1,
|
|
||||||
'disable_existing_loggers': False,
|
|
||||||
'formatters': {
|
|
||||||
'loki': {
|
|
||||||
'class': 'utils.logs.LokiFormatter', # required
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'handlers': {
|
|
||||||
'loki': {
|
|
||||||
'level': 'DEBUG', # Log level. Required
|
|
||||||
'class': 'utils.logs.LokiHandler', # Required
|
|
||||||
'formatter': 'loki', # Loki formatter. Required
|
|
||||||
'timeout': 2, # Post request timeout, default is 0.5. Optional
|
|
||||||
'url': f'{LOKI_BASE_PUBLIC_URL}/loki/api/v1/push', # Loki url. Defaults to localhost. Optional.
|
|
||||||
# 'auth': ("user", "password"), # Basic auth to authenticate with loki. Default is None (i.e. no auth). Optional
|
|
||||||
'tags': {"app": "accounts"}, # Tags / Labels to attach to the log. Optional, but strongly encoraged to use.
|
|
||||||
'mode': 'thread', # Push mode. Can be 'sync' or 'thread'. Sync is blocking, thread is non-blocking. Defaults to sync. Optional.
|
|
||||||
},
|
|
||||||
'console': {
|
|
||||||
'level': 'DEBUG',
|
|
||||||
'class': 'logging.StreamHandler',
|
|
||||||
# 'formatter': 'verbose',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'loggers': {
|
|
||||||
'': {
|
|
||||||
'handlers': ['console', 'loki'],
|
|
||||||
'level': 'INFO',
|
|
||||||
'propagate': True,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
|
|
@ -11,6 +11,6 @@ import os
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'accounts.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'main.settings')
|
||||||
|
|
||||||
application = get_wsgi_application()
|
application = get_wsgi_application()
|
||||||
|
|
@ -5,7 +5,7 @@ import sys
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'accounts.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'main.settings')
|
||||||
try:
|
try:
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
|
|
|
||||||
4
run.sh
4
run.sh
|
|
@ -5,5 +5,5 @@ while ! nc -z $DB_HOST 5432 ; do
|
||||||
done
|
done
|
||||||
#python3 manage.py collectstatic --noinput
|
#python3 manage.py collectstatic --noinput
|
||||||
python3 manage.py migrate
|
python3 manage.py migrate
|
||||||
#gunicorn accounts.wsgi:application --bind 0.0.0.0:8000 -w 4
|
gunicorn main.wsgi:application --bind 0.0.0.0:8000 -w 4
|
||||||
daphne -b 0.0.0.0 -p 8000 accounts.asgi:application
|
#daphne -b 0.0.0.0 -p 8000 main.asgi:application
|
||||||
|
|
@ -16,9 +16,33 @@ class RequestOTPDayRateThrottle(SimpleRateThrottle):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class RequestOTPMinRateThrottle(RequestOTPDayRateThrottle):
|
class RequestOTPMinRateThrottle(RequestOTPDayRateThrottle):
|
||||||
scope = 'otp_min'
|
scope = 'otp_min'
|
||||||
|
|
||||||
|
|
||||||
|
class NumberedRequestOTPDayRateThrottle(SimpleRateThrottle):
|
||||||
|
scope = 'numbered_otp_day'
|
||||||
|
|
||||||
|
def get_ident(self, request):
|
||||||
|
return request.data.get('phone_number')
|
||||||
|
|
||||||
|
def get_cache_key(self, request, view):
|
||||||
|
if request.user and request.user.is_authenticated:
|
||||||
|
ident = request.user.pk
|
||||||
|
else:
|
||||||
|
ident = self.get_ident(request)
|
||||||
|
|
||||||
|
return self.cache_format % {
|
||||||
|
'scope': self.scope,
|
||||||
|
'ident': ident
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class NumberedRequestOTPMinRateThrottle(NumberedRequestOTPDayRateThrottle):
|
||||||
|
scope = 'numbered_otp_min'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue