diff --git a/.gitignore b/.gitignore index 0d9702f..a637850 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ delme*.py .env /venv/ oidc.key +/log/ diff --git a/apps/users/models.py b/apps/users/models.py index 4da2cee..16fadab 100644 --- a/apps/users/models.py +++ b/apps/users/models.py @@ -161,9 +161,6 @@ class User(AbstractUser): return state.get_province_by_id(self.province) 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: self._otp = '77501' elif self.phone_number in settings.TEST_PHONENUMBERS: diff --git a/apps/users/serializers.py b/apps/users/serializers.py index 9f1ba07..c4b5e72 100644 --- a/apps/users/serializers.py +++ b/apps/users/serializers.py @@ -70,16 +70,9 @@ class RequestOTPSerializer(serializers.ModelSerializer): user = User.objects.create_user(**validated_data) if not user.otp_is_valid(): - try: - user.set_otp() - except Exception as e: - raise UnprocessableEntity(_('otp expire time not reached'), code='opt_not_expired') - + user.set_otp() user.send_otp() - else: - raise UnprocessableEntity(_('otp expire time not reached yet'), code='opt_not_expired') - self.instance = user return user diff --git a/apps/users/views.py b/apps/users/views.py index a5fca61..94dafbe 100644 --- a/apps/users/views.py +++ b/apps/users/views.py @@ -21,7 +21,7 @@ from apps.users.models import User from apps.users.provinces_and_cities import State from apps.users.serializers import PublicUserSerializer, AccountSerializer, RequestOTPSerializer, RequestOTTSerializer, \ ChangePasswordSerializer, UserInquirySerializer -from utils.throttles import RequestOTPDayRateThrottle, RequestOTPMinRateThrottle +from utils.throttles import RequestOTPDayRateThrottle, RequestOTPMinRateThrottle, NumberedRequestOTPDayRateThrottle, NumberedRequestOTPMinRateThrottle UserModel = get_user_model() @@ -90,7 +90,10 @@ class RequestOTPView(generics.CreateAPIView): permission_classes = [] serializer_class = RequestOTPSerializer required_scopes = [] - throttle_classes = [RequestOTPMinRateThrottle, RequestOTPDayRateThrottle] + throttle_classes = [RequestOTPMinRateThrottle, + RequestOTPDayRateThrottle, + NumberedRequestOTPDayRateThrottle, + NumberedRequestOTPMinRateThrottle] class RequestOTTView(generics.CreateAPIView): permission_classes = [IsAuthenticatedOrTokenHasScope] diff --git a/accounts/__init__.py b/main/__init__.py similarity index 100% rename from accounts/__init__.py rename to main/__init__.py diff --git a/accounts/asgi.py b/main/asgi.py similarity index 82% rename from accounts/asgi.py rename to main/asgi.py index 3116ceb..f41decb 100644 --- a/accounts/asgi.py +++ b/main/asgi.py @@ -11,6 +11,6 @@ import os 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() diff --git a/accounts/broker_rpc.py b/main/broker_rpc.py similarity index 63% rename from accounts/broker_rpc.py rename to main/broker_rpc.py index 6b322c4..30950c1 100644 --- a/accounts/broker_rpc.py +++ b/main/broker_rpc.py @@ -1,6 +1,6 @@ #!/usr/bin/env python 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 diff --git a/accounts/celery.py b/main/celery.py similarity index 90% rename from accounts/celery.py rename to main/celery.py index 6b7fc88..697b017 100644 --- a/accounts/celery.py +++ b/main/celery.py @@ -6,7 +6,7 @@ # from django.conf import settings # # # 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') # diff --git a/main/other_settings/__init__.py b/main/other_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/main/other_settings/logging.py b/main/other_settings/logging.py new file mode 100644 index 0000000..74038a3 --- /dev/null +++ b/main/other_settings/logging.py @@ -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, +# }, +# }, +# } \ No newline at end of file diff --git a/accounts/settings.py b/main/settings.py similarity index 89% rename from accounts/settings.py rename to main/settings.py index 460f1ad..13d8103 100644 --- a/accounts/settings.py +++ b/main/settings.py @@ -121,7 +121,9 @@ REST_FRAMEWORK = { # ], 'DEFAULT_THROTTLE_RATES': { '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', } @@ -135,7 +137,7 @@ SPECTACULAR_SETTINGS = { "PARSER_WHITELIST": ["rest_framework.parsers.JSONParser"], } -ROOT_URLCONF = 'accounts.urls' +ROOT_URLCONF = 'main.urls' TEMPLATES = [ { @@ -159,7 +161,7 @@ AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', ) -WSGI_APPLICATION = 'accounts.wsgi.application' +WSGI_APPLICATION = 'main.wsgi.application' # Database # 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) -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 main.other_settings.logging import LOGGING from datetime import timedelta from typing import List, Tuple diff --git a/accounts/urls.py b/main/urls.py similarity index 100% rename from accounts/urls.py rename to main/urls.py diff --git a/accounts/wsgi.py b/main/wsgi.py similarity index 82% rename from accounts/wsgi.py rename to main/wsgi.py index d4e6b0e..2d4a6a6 100644 --- a/accounts/wsgi.py +++ b/main/wsgi.py @@ -11,6 +11,6 @@ import os 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() diff --git a/manage.py b/manage.py index 7a22e43..063eacc 100644 --- a/manage.py +++ b/manage.py @@ -5,7 +5,7 @@ import sys def main(): - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'accounts.settings') + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'main.settings') try: from django.core.management import execute_from_command_line except ImportError as exc: diff --git a/run.sh b/run.sh index 80e55fd..0eb9a8e 100755 --- a/run.sh +++ b/run.sh @@ -5,5 +5,5 @@ while ! nc -z $DB_HOST 5432 ; do done #python3 manage.py collectstatic --noinput python3 manage.py migrate -#gunicorn accounts.wsgi:application --bind 0.0.0.0:8000 -w 4 -daphne -b 0.0.0.0 -p 8000 accounts.asgi:application \ No newline at end of file +gunicorn main.wsgi:application --bind 0.0.0.0:8000 -w 4 +#daphne -b 0.0.0.0 -p 8000 main.asgi:application \ No newline at end of file diff --git a/utils/throttles.py b/utils/throttles.py index 33b9e9c..fb4f5ee 100644 --- a/utils/throttles.py +++ b/utils/throttles.py @@ -16,9 +16,33 @@ class RequestOTPDayRateThrottle(SimpleRateThrottle): } + class RequestOTPMinRateThrottle(RequestOTPDayRateThrottle): 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' + +