From c227fda538bf9bf2034353ef2e3d29785adae07e Mon Sep 17 00:00:00 2001 From: Sayyid Hamid Mahdavi Date: Tue, 28 Apr 2026 14:48:54 +0330 Subject: [PATCH 01/12] better exception --- utils/exceptions.py | 62 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/utils/exceptions.py b/utils/exceptions.py index 756d9a4..8e730cd 100644 --- a/utils/exceptions.py +++ b/utils/exceptions.py @@ -1,25 +1,49 @@ import logging +from django.conf import settings +from django.utils import timezone from rest_framework.exceptions import APIException from rest_framework.views import exception_handler as drf_exception_handler logger = logging.getLogger(__name__) + + def exception_handler(exc, context): + logger.exception(exc) # پاسخ پیش‌فرض DRF را دریافت می‌کنیم response = drf_exception_handler(exc, context) if response is not None: - # ساختار دلخواه خود را تعریف می‌کنیم response_data = { 'success': False, 'status_code': response.status_code, - 'message': 'An error occurred', - 'details': {} + 'status_message': str(getattr(exc, 'default_detail', exc)), + 'details': getattr(exc, 'detail', None), } if isinstance(exc, APIException): - response_data['message'] = exc.detail if isinstance(exc.detail, str) else "Validation error" - response_data['details'] = exc.detail if isinstance(exc.detail, dict) else {} + try: + error = getattr(getattr(exc, 'detail', None), 'code', None) or getattr(exc, 'code', None) + if not error: + error = getattr(exc, 'default_code') + except: + error = '' + + # if isinstance(exc.detail, str): + # message = exc.detail + # elif isinstance(exc.detail, dict): + # if 'message' in exc.detail or 'string' in exc.detail: + # message = exc.detail.get('message') or exc.detail.get('string') + # else: + # message = error + # + # else: + # message = error + # + # response_data['message'] = message + response_data['details'] = {"message": exc.detail} + response_data['details']['error'] = error + response_data['details']['timestamp'] = timezone.now().isoformat() response.data = response_data @@ -29,6 +53,7 @@ def exception_handler(exc, context): # در فایل middleware.py from django.http import JsonResponse + class ErrorMiddleware: def __init__(self, get_response): self.get_response = get_response @@ -43,14 +68,33 @@ class ErrorMiddleware: { "success": False, "status_code": 500, - "message": "Internal server error", - "details": str(exception) # فقط در حالت توسعه! در تولید بهتر است لاگ شود. + "status_message": "Internal server error", + "details": str(exception) if settings.DEBUG else None, }, status=500 ) +from rest_framework.exceptions import APIException +from rest_framework import status +from django.utils.translation import gettext_lazy as _ + + class UnprocessableEntity(APIException): - status_code = 422 + status_code = status.HTTP_422_UNPROCESSABLE_ENTITY default_detail = 'The request was well-formed but cannot be processed due to semantic errors.' - default_code = 'unprocessable_entity' \ No newline at end of file + default_code = 'unprocessable_entity' + + +class ServiceUnavailable(APIException): + status_code = status.HTTP_503_SERVICE_UNAVAILABLE + default_ = _('SERVICE_UNAVAILABLE') + default_code = 'service_unavailable' + default_detail = 'Service Unavailable' + + +class Conflict(APIException): + status_code = status.HTTP_409_CONFLICT + default_ = _('CONFLICT') + default_code = 'conflict' + default_detail = 'Conflict' From 1f44f80a798b3df740bdfa9fae527bf026a8b739 Mon Sep 17 00:00:00 2001 From: Sayyid Hamid Mahdavi Date: Tue, 28 Apr 2026 14:53:37 +0330 Subject: [PATCH 02/12] better exception --- apps/users/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/users/serializers.py b/apps/users/serializers.py index ff7b3e0..9f1ba07 100644 --- a/apps/users/serializers.py +++ b/apps/users/serializers.py @@ -73,12 +73,12 @@ class RequestOTPSerializer(serializers.ModelSerializer): try: user.set_otp() except Exception as e: - raise UnprocessableEntity(_('otp expire time not reached')) + raise UnprocessableEntity(_('otp expire time not reached'), code='opt_not_expired') user.send_otp() else: - raise UnprocessableEntity(_('otp expire time not reached yet')) + raise UnprocessableEntity(_('otp expire time not reached yet'), code='opt_not_expired') self.instance = user return user From 280b8f1c2c211ed5caf772cd8930dd83f686cdaa Mon Sep 17 00:00:00 2001 From: Sayyid Hamid Mahdavi Date: Tue, 28 Apr 2026 15:59:56 +0330 Subject: [PATCH 03/12] throttles --- .gitignore | 1 + apps/users/models.py | 3 - apps/users/serializers.py | 9 +- apps/users/views.py | 7 +- {accounts => main}/__init__.py | 0 {accounts => main}/asgi.py | 2 +- {accounts => main}/broker_rpc.py | 2 +- {accounts => main}/celery.py | 2 +- main/other_settings/__init__.py | 0 main/other_settings/logging.py | 141 +++++++++++++++++++++++++++++++ {accounts => main}/settings.py | 43 ++-------- {accounts => main}/urls.py | 0 {accounts => main}/wsgi.py | 2 +- manage.py | 2 +- run.sh | 4 +- utils/throttles.py | 24 ++++++ 16 files changed, 185 insertions(+), 57 deletions(-) rename {accounts => main}/__init__.py (100%) rename {accounts => main}/asgi.py (82%) rename {accounts => main}/broker_rpc.py (63%) rename {accounts => main}/celery.py (90%) create mode 100644 main/other_settings/__init__.py create mode 100644 main/other_settings/logging.py rename {accounts => main}/settings.py (89%) rename {accounts => main}/urls.py (100%) rename {accounts => main}/wsgi.py (82%) 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' + + From 28ae006b5d642ad675d4757fb594626b61436c47 Mon Sep 17 00:00:00 2001 From: Sayyid Hamid Mahdavi Date: Tue, 28 Apr 2026 16:42:17 +0330 Subject: [PATCH 04/12] int ttl --- apps/users/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/users/serializers.py b/apps/users/serializers.py index 07e5211..0a8a52b 100644 --- a/apps/users/serializers.py +++ b/apps/users/serializers.py @@ -80,7 +80,7 @@ class RequestOTPSerializer(serializers.ModelSerializer): def get_ttl(self, obj: User): if obj.otp_expire > timezone.now(): ttl = obj.otp_expire - timezone.now() - result = ttl.total_seconds() + result = int(ttl.total_seconds()) else: result = 0 From 1981122f2833cf4a68a85a620b2f36ec904e4b34 Mon Sep 17 00:00:00 2001 From: Sayyid Hamid Mahdavi Date: Wed, 29 Apr 2026 17:00:56 +0330 Subject: [PATCH 05/12] float ttl --- apps/users/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/users/serializers.py b/apps/users/serializers.py index 0a8a52b..07e5211 100644 --- a/apps/users/serializers.py +++ b/apps/users/serializers.py @@ -80,7 +80,7 @@ class RequestOTPSerializer(serializers.ModelSerializer): def get_ttl(self, obj: User): if obj.otp_expire > timezone.now(): ttl = obj.otp_expire - timezone.now() - result = int(ttl.total_seconds()) + result = ttl.total_seconds() else: result = 0 From 8859fe4d2073c701f42a6bf58e98db308564360a Mon Sep 17 00:00:00 2001 From: Haydar Ghasemi Date: Mon, 18 May 2026 11:37:58 +0330 Subject: [PATCH 06/12] FEATURE(account): convert phone number to english numbers convert phone number to english numbers --- apps/users/serializers.py | 5 +++++ requirements.in | 3 ++- requirements.txt | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/users/serializers.py b/apps/users/serializers.py index e4bf1cc..82e13be 100644 --- a/apps/users/serializers.py +++ b/apps/users/serializers.py @@ -5,6 +5,7 @@ from rest_framework import serializers from apps.users.models import User from django.utils import timezone from django.utils.translation import gettext_lazy as _ +from unidecode import unidecode from utils.exceptions import UnprocessableEntity @@ -62,6 +63,10 @@ class RequestOTPSerializer(serializers.ModelSerializer): read_only_fields = ['otp_expire', 'ttl'] + def validate(self, attrs): + attrs['phone_number'] = unidecode(attrs['phone_number']) + return attrs + def save(self, **kwargs): # TODO: move it to query set validated_data = self.validated_data diff --git a/requirements.in b/requirements.in index ccc7cf9..9caa867 100644 --- a/requirements.in +++ b/requirements.in @@ -21,4 +21,5 @@ django_redis django_minio_backend daphne Twisted[tls,http2] -packaging==25.0 \ No newline at end of file +packaging==25.0 +unidecode \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 1eb7ad6..ebd29a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -246,3 +246,4 @@ zope-interface==8.2 # via # gevent # twisted +unidecode==1.4.0 From 706de574273abd745c89deeec0507c832be7ed0d Mon Sep 17 00:00:00 2001 From: Haydar Ghasemi Date: Wed, 20 May 2026 13:08:56 +0330 Subject: [PATCH 07/12] Fix(accounts): Dockerfile Dockerfile --- Dockerfile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 298a33a..9e378fd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ -FROM debian:13 +FROM base:debian13 ENV PYTHONUNBUFFERED 1 WORKDIR /app -RUN apt update -RUN apt install gnupg2 netcat-traditional libssl-dev libcrypto++-dev lsb-release python3-pip -y -COPY requirements.txt /app/requirements.txt -RUN pip3 install --break-system-packages -r requirements.txt -RUN pip3 install --break-system-packages setuptools +#RUN apt update +#RUN apt install gnupg2 netcat-traditional libssl-dev libcrypto++-dev lsb-release python3-pip -y +#COPY requirements.txt /app/requirements.txt +#RUN pip3 install --break-system-packages -r requirements.txt +#RUN pip3 install --break-system-packages setuptools #ENTRYPOINT ["./run.sh"] From 43b5ba9028a3190ed85d99ea6e9142b06c06a980 Mon Sep 17 00:00:00 2001 From: Haydar Ghasemi Date: Sat, 23 May 2026 13:55:47 +0330 Subject: [PATCH 08/12] Fix(accounts): send sms send sms --- .../commands/send_sms_by_uuid_list.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 apps/users/management/commands/send_sms_by_uuid_list.py diff --git a/apps/users/management/commands/send_sms_by_uuid_list.py b/apps/users/management/commands/send_sms_by_uuid_list.py new file mode 100644 index 0000000..ede31b5 --- /dev/null +++ b/apps/users/management/commands/send_sms_by_uuid_list.py @@ -0,0 +1,62 @@ +from apps.users.models import * +from django.contrib.gis.geos import Point, Polygon +import random +import os + + +from django.core.management.base import BaseCommand, CommandError + +class Command(BaseCommand): + help = "send sms by uuid list" + + def add_arguments(self, parser): + parser.add_argument('message_file', type=str, help='Message as a file') + parser.add_argument('uuids_file', type=str, help='File with one UUID per line') + + def handle(self, *args, **options): + print(options['message_file']) + print(options['uuids_file']) + message_file_path = options['message_file'] + uuids_file_path = options['uuids_file'] + + if not os.path.exists(message_file_path): + raise CommandError(f'File "{message_file_path}" does not exist') + + if not os.path.exists(uuids_file_path): + raise CommandError(f'File "{uuids_file_path}" does not exist') + + uuids = [] + invalid_lines = [] + message: str = '''''' + + with open(message_file_path, 'r', encoding='utf-8') as f: + message = f.read() + + with open(uuids_file_path, 'r', encoding='utf-8') as f: + for line_num, line in enumerate(f, 1): + line = line.strip() + if not line: # Skip empty lines + continue + try: + # Validate and normalize UUID + valid_uuid = str(uuid.UUID(line)) + uuids.append(valid_uuid) + self.stdout.write(f'Line {line_num}: ✓ {valid_uuid}') + except (ValueError, AttributeError, TypeError): + invalid_lines.append((line_num, line)) + self.stdout.write( + self.style.ERROR(f'Line {line_num}: ✗ Invalid UUID "{line}"') + ) + + sms_client = SMSPolicy.get_client() + users = User.objects.filter(uuid__in=uuids) + empty_phone_numbers = list(filter(lambda u: not u.phone_number, users)) + if empty_phone_numbers: + logger.error(f'not exist phone numbers for following uuids') + print(empty_phone_numbers) + logger.error(f'end of not exist phone numbers') + return + for user in users: + logger.info(f'try to send message for: {user.uuid} and {user.phone_number}') + # sms_client.send_sms(user.phone_number.strip('+'), message) + logger.info(f'sent message for: {user.uuid} and {user.phone_number}') From 926e48a5dd8e081667139b2eadac71f2be073ae6 Mon Sep 17 00:00:00 2001 From: Haydar Ghasemi Date: Mon, 25 May 2026 17:28:24 +0330 Subject: [PATCH 09/12] Fix(accounts): fix send_sms_by_uuid_list.py fix send_sms_by_uuid_list.py --- apps/users/management/commands/send_sms_by_uuid_list.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/users/management/commands/send_sms_by_uuid_list.py b/apps/users/management/commands/send_sms_by_uuid_list.py index ede31b5..3e1b15a 100644 --- a/apps/users/management/commands/send_sms_by_uuid_list.py +++ b/apps/users/management/commands/send_sms_by_uuid_list.py @@ -1,6 +1,4 @@ from apps.users.models import * -from django.contrib.gis.geos import Point, Polygon -import random import os From f4fe506aefbd4d9e1ee629449792eebe7f5a144d Mon Sep 17 00:00:00 2001 From: Haydar Ghasemi Date: Mon, 25 May 2026 17:47:52 +0330 Subject: [PATCH 10/12] Fix(accounts): fix send_sms_by_uuid_list.py fix send_sms_by_uuid_list.py --- apps/users/management/commands/send_sms_by_uuid_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/users/management/commands/send_sms_by_uuid_list.py b/apps/users/management/commands/send_sms_by_uuid_list.py index 3e1b15a..92c46e9 100644 --- a/apps/users/management/commands/send_sms_by_uuid_list.py +++ b/apps/users/management/commands/send_sms_by_uuid_list.py @@ -56,5 +56,5 @@ class Command(BaseCommand): return for user in users: logger.info(f'try to send message for: {user.uuid} and {user.phone_number}') - # sms_client.send_sms(user.phone_number.strip('+'), message) + sms_client.send_sms(user.phone_number.strip('+'), message) logger.info(f'sent message for: {user.uuid} and {user.phone_number}') From cc6a76581e45c13bd24857bc256f805eb9bd9b16 Mon Sep 17 00:00:00 2001 From: Sayyid Hamid Mahdavi Date: Tue, 2 Jun 2026 14:14:17 +0330 Subject: [PATCH 11/12] float ttl --- apps/users/filters.py | 4 +++- main/settings.py | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/users/filters.py b/apps/users/filters.py index 3e15c05..4fe64ac 100644 --- a/apps/users/filters.py +++ b/apps/users/filters.py @@ -3,10 +3,12 @@ from .models import User class UserFilter(django_filters.FilterSet): + uuid_in = django_filters.UUIDFilter(required=True, lookup_expr='in') + class Meta: model = User fields = { - 'uuid': ['in'], + # 'uuid': ['in'], } diff --git a/main/settings.py b/main/settings.py index fcf4d23..bbb1e87 100644 --- a/main/settings.py +++ b/main/settings.py @@ -306,10 +306,10 @@ STORAGES = { "default": { "BACKEND": "django_minio_backend.models.MinioBackend", "OPTIONS": { - "MINIO_ENDPOINT": config('MINIO_ENDPOINT', default='drive.gooyal.com'), + "MINIO_ENDPOINT": config('MINIO_ENDPOINT', default='drive.gooyal.ir'), "MINIO_USE_HTTPS": config('MINIO_USE_HTTPS', default=True, cast=bool), - "MINIO_EXTERNAL_ENDPOINT": config('MINIO_EXTERNAL_ENDPOINT', default='drive.gooyal.com'), + "MINIO_EXTERNAL_ENDPOINT": config('MINIO_EXTERNAL_ENDPOINT', default='drive.gooyal.ir'), # Default is same as MINIO_ENDPOINT "MINIO_EXTERNAL_ENDPOINT_USE_HTTPS": config('MINIO_EXTERNAL_ENDPOINT_USE_HTTPS', default=True, cast=bool), # Default is same as MINIO_USE_HTTPS @@ -362,7 +362,7 @@ MINIO_ENDPOINT = config('MINIO_ENDPOINT', default='drive.gooyal.ir') MINIO_USE_HTTPS = config('MINIO_USE_HTTPS', default=True, cast=bool) MINIO_EXTERNAL_ENDPOINT = config('MINIO_EXTERNAL_ENDPOINT', - default='drive.gooyal.com') # Default is same as MINIO_ENDPOINT + default='drive.gooyal.ir') # Default is same as MINIO_ENDPOINT MINIO_EXTERNAL_ENDPOINT_USE_HTTPS = config('MINIO_EXTERNAL_ENDPOINT_USE_HTTPS', default=True, cast=bool) # Default is same as MINIO_USE_HTTPS MINIO_REGION = None # Default is set to None From e6ea563d197b5b653fdb033c07b3c08c1f1b29f9 Mon Sep 17 00:00:00 2001 From: Haydar Ghasemi Date: Wed, 3 Jun 2026 17:08:17 +0330 Subject: [PATCH 12/12] FEATURE(accounts): provide all users endpoint provide all users endpoint --- apps/users/filters.py | 9 +++++---- apps/users/urls.py | 7 ++++--- apps/users/views.py | 14 ++++++++++++-- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/apps/users/filters.py b/apps/users/filters.py index 4fe64ac..9754c70 100644 --- a/apps/users/filters.py +++ b/apps/users/filters.py @@ -1,14 +1,15 @@ import django_filters from .models import User +class ListUUIDFilter(django_filters.BaseInFilter, django_filters.UUIDFilter): + pass + class UserFilter(django_filters.FilterSet): - uuid_in = django_filters.UUIDFilter(required=True, lookup_expr='in') + uuid_in = ListUUIDFilter(field_name='uuid', required=True) class Meta: model = User fields = { # 'uuid': ['in'], - } - - + } \ No newline at end of file diff --git a/apps/users/urls.py b/apps/users/urls.py index 789744e..99aeea2 100644 --- a/apps/users/urls.py +++ b/apps/users/urls.py @@ -2,17 +2,17 @@ from django.urls import path from django.contrib.auth.views import LogoutView from .views import UserListView, UserPublicRetrieveView, AccountView, RequestOTPView, ChangePasswordView, \ OTPLoginView, ProfileDetailView, ProfileUpdateView, RequestOTTView, UserCurrentAvatarUrlView, UserInquiryView, \ - UserDetailedRetrieveView, UserSessionListView + UserDetailedRetrieveView, UserSessionListView, UserProfileView app_name = "users" - urlpatterns = [ path('login/', OTPLoginView.as_view(), name='login'), path('logout/', LogoutView.as_view(), name='logout'), path('account/', ProfileDetailView.as_view(), name='account_detail'), path('account/update/', ProfileUpdateView.as_view(), name='account_update'), path('api/account/', AccountView.as_view(), name='account_api'), - path('api/users/', UserListView.as_view(), name='user_list_api'), + path('api/users/all', UserListView.as_view(), name='user_list_api'), + path('api/users/', UserProfileView.as_view(), name='user_profile_list_api'), path('api/sessions/', UserSessionListView.as_view(), name='user_sessions_api'), path('api/users//', UserPublicRetrieveView.as_view(), name='user_public_retrieve_api'), path('api/users//avatar', UserCurrentAvatarUrlView.as_view(), name='user_avatar_api'), @@ -22,3 +22,4 @@ urlpatterns = [ path('api/change_password/', ChangePasswordView.as_view(), name='change_password_api'), path('api/inquiry/', UserInquiryView.as_view(), name='user_inquiry_api'), ] + diff --git a/apps/users/views.py b/apps/users/views.py index ef6b6f0..6485658 100644 --- a/apps/users/views.py +++ b/apps/users/views.py @@ -26,17 +26,27 @@ from utils.throttles import RequestOTPDayRateThrottle, RequestOTPMinRateThrottle UserModel = get_user_model() -class UserListView(generics.ListAPIView): +class BaseUserListView(generics.ListAPIView): permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements] queryset = User.objects.all() serializer_class = PublicUserSerializer filter_backends = (DjangoFilterBackend,) - filterset_class = UserFilter + # Make filterset_class abstract - must be defined in subclasses + filterset_class = None + +class UserProfileView(BaseUserListView): + filterset_class = UserFilter required_alternate_scopes = { "GET": [["accounts.profile:list"]], } +class UserListView(BaseUserListView): + # filterset_class = UserFilter + required_alternate_scopes = { + "GET": [["accounts.users:list"]], + } + class UserPublicRetrieveView(generics.RetrieveAPIView): permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]