merge from upstream
This commit is contained in:
commit
f400eb6ab4
46 changed files with 1605 additions and 152 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -7,3 +7,4 @@ delme*.py
|
|||
/venv/
|
||||
oidc.key
|
||||
/log/
|
||||
/logs/*.log
|
||||
|
|
|
|||
|
|
@ -1,2 +1,17 @@
|
|||
from django.contrib import admin
|
||||
from .models import SMSPolicy, Config
|
||||
|
||||
|
||||
class SMSPolicyAdmin(admin.ModelAdmin):
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
|
||||
class ConfigAdmin(admin.ModelAdmin):
|
||||
list_display = ['key', 'value', 'value_type', 'get_value', 'description', 'comment']
|
||||
|
||||
admin.site.register(Config, ConfigAdmin)
|
||||
admin.site.register(SMSPolicy, SMSPolicyAdmin)
|
||||
|
|
|
|||
31
apps/core/decorators.py
Normal file
31
apps/core/decorators.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
from functools import wraps
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.core.models import Config, ConfigValueChoices
|
||||
from utils.exceptions import ServiceUnavailable
|
||||
|
||||
|
||||
def service_availability(key:str):
|
||||
def with_params(view_func):
|
||||
@wraps(view_func)
|
||||
def wrapper(self, request, *args, **kwargs):
|
||||
print(key)
|
||||
print(f"Executing {view_func.__name__} action")
|
||||
|
||||
service_is_available = Config.get_value_of(f"SERVICE_IS_AVAILABLE_{key.upper()}", "True", ConfigValueChoices.BOOLEAN)
|
||||
config = Config.objects.get(key=f"SERVICE_IS_AVAILABLE_{key.upper()}")
|
||||
if not service_is_available:
|
||||
raise ServiceUnavailable(
|
||||
detail={
|
||||
# "code": 'service_unavailable_at_now',
|
||||
# "timestamp": timezone.now().isoformat(),
|
||||
"message": config.description or "این سرویس در حال حاضر در دسترس نیست"
|
||||
},
|
||||
)
|
||||
|
||||
return view_func(self, request, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return with_params
|
||||
31
apps/core/migrations/0001_initial.py
Normal file
31
apps/core/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Generated by Django 6.0.2 on 2026-03-16 12:23
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SMSPolicy',
|
||||
fields=[
|
||||
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, primary_key=True, serialize=False, unique=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True, db_index=True)),
|
||||
('preferred', models.CharField(choices=[('fake', 'fake'), ('payam_sms', 'payam_sms'), ('mobin_sms', 'mobin_sms')], default='fake', max_length=16)),
|
||||
('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL, unique=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
30
apps/core/migrations/0002_config.py
Normal file
30
apps/core/migrations/0002_config.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Generated by Django 6.0.2 on 2026-04-06 05:59
|
||||
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Config',
|
||||
fields=[
|
||||
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, primary_key=True, serialize=False, unique=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True, db_index=True)),
|
||||
('key', models.CharField(db_index=True, max_length=255, unique=True, verbose_name='key')),
|
||||
('value', models.TextField(blank=True, null=True, verbose_name='value')),
|
||||
('value_type', models.CharField(choices=[('INT', 'Integer'), ('FLOAT', 'Float'), ('BOOLEAN', 'Boolean'), ('STRING', 'String'), ('DATE', 'Date'), ('DATETIME', 'DateTime'), ('JSON', 'JSON')], max_length=64)),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='description')),
|
||||
('comment', models.TextField(blank=True, null=True, verbose_name='comment')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -1,2 +1,106 @@
|
|||
import logging
|
||||
|
||||
from django.db.models import TextChoices
|
||||
|
||||
from apps.gooyal_oauth2.settings import oauth2_settings
|
||||
from utils.clients.sms.fake.client import FakeClient
|
||||
from utils.clients.sms.mobin_sms.client import MobinSMSClient
|
||||
from utils.clients.sms.payam_sms.client import PayamSMSClient
|
||||
from utils.clients.sms.sms import BaseSMSClient
|
||||
from utils.models import BaseModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class SMSClienChoises(TextChoices):
|
||||
FAKE = "fake", _("fake")
|
||||
PAYAM_SMS = 'payam_sms', _('payam_sms')
|
||||
MOBIN_SMS = 'mobin_sms', _('mobin_sms')
|
||||
|
||||
|
||||
class SMSPolicy(BaseModel):
|
||||
application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT,
|
||||
related_name='+', null=True, blank=True, unique=True)
|
||||
preferred = models.CharField(max_length=16, choices=SMSClienChoises.choices, default=SMSClienChoises.FAKE)
|
||||
|
||||
def __str__(self):
|
||||
return self.get_preferred_display()
|
||||
|
||||
@staticmethod
|
||||
def get_client() -> BaseSMSClient:
|
||||
policy = SMSPolicy.objects.get_or_create(application=None)[0]
|
||||
if policy.preferred == SMSClienChoises.MOBIN_SMS:
|
||||
return MobinSMSClient()
|
||||
elif policy.preferred == SMSClienChoises.PAYAM_SMS:
|
||||
return PayamSMSClient()
|
||||
else:
|
||||
return FakeClient()
|
||||
|
||||
|
||||
class ConfigValueChoices(models.TextChoices):
|
||||
INT = 'INT', _('Integer')
|
||||
FLOAT = 'FLOAT', _('Float')
|
||||
BOOLEAN = 'BOOLEAN', _('Boolean')
|
||||
STRING = 'STRING', _('String')
|
||||
DATE = 'DATE', _('Date')
|
||||
DATETIME = 'DATETIME', _('DateTime')
|
||||
JSON = 'JSON', _('JSON')
|
||||
|
||||
|
||||
class Config(BaseModel):
|
||||
key = models.CharField(_('key'), max_length=255, unique=True, db_index=True)
|
||||
value = models.TextField(_('value'), null=True, blank=True)
|
||||
value_type = models.CharField(choices=ConfigValueChoices.choices, max_length=64)
|
||||
description = models.TextField(_('description'), null=True, blank=True)
|
||||
comment = models.TextField(_('comment'), null=True, blank=True)
|
||||
|
||||
def get_value(self):
|
||||
return Config.get_value_of(self.key)
|
||||
|
||||
@staticmethod
|
||||
def type_cast(value, value_type):
|
||||
|
||||
try:
|
||||
if value_type == ConfigValueChoices.STRING.value:
|
||||
value = str(value)
|
||||
|
||||
elif value_type == ConfigValueChoices.BOOLEAN.value:
|
||||
value = value.lower() == 'true'
|
||||
|
||||
elif value_type == ConfigValueChoices.DATE.value:
|
||||
value = datetime.fromisoformat(value)
|
||||
|
||||
elif value_type == ConfigValueChoices.DATETIME.value:
|
||||
value = datetime.fromisoformat(value)
|
||||
|
||||
elif value_type == ConfigValueChoices.JSON.value:
|
||||
value = json.loads(value)
|
||||
|
||||
elif value_type == ConfigValueChoices.INT.value:
|
||||
value = int(value)
|
||||
|
||||
elif value_type == ConfigValueChoices.FLOAT.value:
|
||||
value = float(value)
|
||||
|
||||
except Exception as e:
|
||||
value = None
|
||||
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def get_value_of(key, default=None, casting_type=None):
|
||||
# TODO: use cache
|
||||
config, created = Config.objects.get_or_create(key=key, defaults={'value': default})
|
||||
|
||||
if config.value is None:
|
||||
return default
|
||||
|
||||
value = Config.type_cast(config.value, casting_type or config.value_type)
|
||||
|
||||
return value
|
||||
|
|
|
|||
9
apps/core/serializers.py
Normal file
9
apps/core/serializers.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from .models import Config
|
||||
|
||||
|
||||
class ConfigSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Config
|
||||
fields = ['key', 'value', 'value_type']
|
||||
|
|
@ -1,7 +1,17 @@
|
|||
from django.urls import path, include
|
||||
from .views import HomeView
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from .views import HomeView, TestIpView, HealthcheckView, StatusView
|
||||
|
||||
app_name = "core"
|
||||
|
||||
router = DefaultRouter()
|
||||
|
||||
# router.register('api/config', ConfigViewSet, basename='configs')
|
||||
|
||||
urlpatterns = [
|
||||
path('', HomeView.as_view(), name='home'),
|
||||
path('test-ip', TestIpView.as_view(), name='test-ip'),
|
||||
path('status', StatusView.as_view(), name='status'),
|
||||
path('healthcheck', HealthcheckView.as_view(), name='healthcheck'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2,9 +2,82 @@ from django.contrib.auth.decorators import login_required
|
|||
from django.shortcuts import render
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.generic import TemplateView
|
||||
from django.contrib.sessions.backends.db import SessionStore
|
||||
from django.contrib.sessions.models import Session
|
||||
from rest_framework import mixins
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from apps.core.decorators import service_availability
|
||||
from apps.core.models import Config
|
||||
from apps.core.serializers import ConfigSerializer
|
||||
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
|
||||
|
||||
|
||||
# Create your views here.
|
||||
# @method_decorator(login_required, name='dispatch')
|
||||
class HomeView(TemplateView):
|
||||
template_name = 'core/home.html'
|
||||
|
||||
# def get(self, request, *args, **kwargs):
|
||||
# session = request.session
|
||||
# print(type(session))
|
||||
# print(session.session_key)
|
||||
# return super(HomeView, self).get(request, *args, **kwargs)
|
||||
|
||||
|
||||
class TestIpView(TemplateView):
|
||||
template_name = 'core/home.html'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
print(request.headers)
|
||||
headers_data = {'Host': 'accounts.gooyal.ir',
|
||||
'X-Real-Ip': '212.23.216.131',
|
||||
'X-Forwarded-For': '5.216.121.72, 212.23.216.131',
|
||||
'X-Forwarded-Proto': 'http',
|
||||
'Connection': 'close',
|
||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'Accept-Language': 'en,en-US;q=0.7,fa;q=0.3',
|
||||
'Accept-Encoding': 'gzip, deflate, br, zstd',
|
||||
'Cookie': 'csrftoken=xor4RVhDDuhQ5l4hf44iI3o2I0FJU8kJ; sessionid=3c0kju13bswh6ufu3mdnl1u38wi7d4b3',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'Sec-Fetch-Dest': 'document',
|
||||
'Sec-Fetch-Mode': 'navigate',
|
||||
'Sec-Fetch-Site': 'none',
|
||||
'Sec-Fetch-User': '?1',
|
||||
'Priority': 'u=0, i'}
|
||||
|
||||
return super(TestIpView, self).get(request, *args, **kwargs)
|
||||
|
||||
|
||||
class ConfigViewSet(mixins.ListModelMixin, GenericViewSet):
|
||||
queryset = Config.objects.all()
|
||||
serializer_class = ConfigSerializer
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
|
||||
class StatusView(APIView):
|
||||
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
|
||||
required_alternate_scopes = {
|
||||
"GET": [["accounts.status:get"]],
|
||||
}
|
||||
|
||||
@service_availability('status')
|
||||
def get(self, request, format=None):
|
||||
|
||||
data = {
|
||||
"status": "ok",
|
||||
}
|
||||
return Response(data)
|
||||
|
||||
|
||||
class HealthcheckView(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def get(self, request, format=None):
|
||||
data = {
|
||||
"is_healthy": True
|
||||
}
|
||||
return Response(data)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from django.contrib.admin.sites import NotRegistered
|
|||
|
||||
from oauth2_provider.admin import ApplicationAdmin
|
||||
|
||||
from .models import Application, Resource, Scope
|
||||
from .models import Application, Scope
|
||||
from .forms import ApplicationForm
|
||||
|
||||
|
||||
|
|
@ -23,14 +23,9 @@ class ApplicationAdmin(ApplicationAdmin):
|
|||
form = ApplicationForm
|
||||
|
||||
|
||||
@admin.register(Resource)
|
||||
class ResourceAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "user", "expires")
|
||||
|
||||
|
||||
@admin.register(Scope)
|
||||
class ScopeAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', "resource", 'description', 'is_default')
|
||||
list_display = ('name', 'description', 'is_default')
|
||||
|
||||
|
||||
# admin.site.register(RestrictedApplication, RestrictedApplicationAdmin)
|
||||
|
|
|
|||
2
apps/gooyal_oauth2/decorators.py
Normal file
2
apps/gooyal_oauth2/decorators.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
def session_limit_count(count=0):
|
||||
pass
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# Generated by Django 6.0.2 on 2026-04-05 13:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('gooyal_oauth2', '0005_alter_refreshtoken_unique_together_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='scope',
|
||||
name='resource',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='application',
|
||||
name='resource',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='application',
|
||||
name='max_allowed_session',
|
||||
field=models.PositiveIntegerField(default=0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='application',
|
||||
name='authorization_grant_type',
|
||||
field=models.CharField(choices=[('authorization-code', 'Authorization code'), ('urn:ietf:params:oauth:grant-type:device_code', 'Device Code'), ('implicit', 'Implicit'), ('password', 'Resource owner password-based'), ('client-credentials', 'Client credentials'), ('openid-hybrid', 'OpenID connect hybrid')], max_length=44),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Resource',
|
||||
),
|
||||
]
|
||||
|
|
@ -23,19 +23,6 @@ from .settings import oauth2_settings
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Resource(BaseModel):
|
||||
name = models.CharField(max_length=255)
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True,
|
||||
related_name="resources"
|
||||
)
|
||||
expires = models.DateTimeField()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Application(AbstractApplication, BaseModel):
|
||||
"""
|
||||
Application model for use with Django OAuth Toolkit that allows the scopes
|
||||
|
|
@ -49,15 +36,10 @@ class Application(AbstractApplication, BaseModel):
|
|||
on_delete=models.PROTECT
|
||||
)
|
||||
allowed_scope = models.TextField(blank=True)
|
||||
resource = models.OneToOneField(
|
||||
Resource,
|
||||
models.PROTECT,
|
||||
blank=True, null=True,
|
||||
help_text='The resource of application.',
|
||||
related_name='application'
|
||||
)
|
||||
|
||||
avatar = models.ImageField(upload_to='avatars', null=True, blank=True)
|
||||
max_allowed_session = models.PositiveIntegerField(default=0)
|
||||
|
||||
|
||||
@property
|
||||
def allowed_scopes(self):
|
||||
|
|
@ -96,13 +78,6 @@ class Scope(BaseModel):
|
|||
help_text='The application to which the scope belongs.',
|
||||
related_name='scopes'
|
||||
)
|
||||
resource = models.ForeignKey(
|
||||
Resource,
|
||||
models.PROTECT,
|
||||
blank=True, null=True,
|
||||
help_text='The resource of scope.',
|
||||
related_name='scopes'
|
||||
)
|
||||
#: The name of the scope
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
|
|
@ -122,19 +97,10 @@ class Scope(BaseModel):
|
|||
|
||||
@property
|
||||
def final_name(self):
|
||||
args = []
|
||||
if self.resource:
|
||||
args.append(self.resource.name)
|
||||
|
||||
args.append(self.name)
|
||||
return '.'.join(args)
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def final_description(self):
|
||||
resource_name = self.resource and self.resource.name
|
||||
|
||||
if resource_name:
|
||||
return f"{resource_name} -> {self.description}"
|
||||
return self.description
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class Scopes(BaseScopes):
|
|||
def get_queryset(self, application=None):
|
||||
queryset = Scope.objects.all()
|
||||
if application:
|
||||
queryset = queryset.filter(name__in=application.allowed_scopes).order_by('resource__uuid')
|
||||
queryset = queryset.filter(name__in=application.allowed_scopes)
|
||||
return queryset
|
||||
|
||||
|
||||
|
|
|
|||
212
apps/gooyal_oauth2/tests.py
Normal file
212
apps/gooyal_oauth2/tests.py
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
import base64
|
||||
import json
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
from unicodedata import category
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from oauth2_provider.models import get_access_token_model, get_application_model
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient, APITestCase
|
||||
|
||||
|
||||
from apps.core.models import Config
|
||||
from apps.gooyal_oauth2.models import Scope
|
||||
from apps.users.models import User
|
||||
|
||||
AccessToken = get_access_token_model()
|
||||
Application = get_application_model()
|
||||
|
||||
|
||||
|
||||
def mock_notifications_push_user_success(user_uuid, title, message, priority=5, extras=None):
|
||||
import uuid as sys_uuid
|
||||
class Tmp():
|
||||
uuid = sys_uuid.uuid4()
|
||||
|
||||
data = Tmp()
|
||||
return data
|
||||
|
||||
|
||||
class GooyalOAuth2Tests(APITestCase):
|
||||
user_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb47005f')
|
||||
access_token_1 = 'au4naVsdKCbKNOhnElPyXcrwSnqqFbm'
|
||||
access_token_2 = 'vu4naVsdKCbKNOhnElPyXcrwSnqqFbm'
|
||||
application_uuid = uuid.UUID('a14e8b86-8f4a-44d9-b29d-badceb47005f')
|
||||
client_id = '4INGOCMoulE0fNY1SQlTbPtsWqqxGj2DdqjADq6u'
|
||||
client_secret = '4INGOCMoulE0fNY1SQlTbPtsWqqxGj2DdqjADq6u'
|
||||
|
||||
visitor_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb47005a')
|
||||
|
||||
expire_datetime = timezone.now() + timedelta(seconds=3600)
|
||||
expire_datetime.isoformat()
|
||||
client = APIClient()
|
||||
|
||||
def setUp(self):
|
||||
from django.conf import settings
|
||||
settings.SMS_SEND = False
|
||||
|
||||
self.notifications_push_user_success_patcher = patch('apps.users.models.User.notify',
|
||||
mock_notifications_push_user_success)
|
||||
|
||||
self.notifications_push_user_success_patcher.start()
|
||||
self.user_phone_number = '+989100000000'
|
||||
self.user = User.objects.create(pk=self.user_uuid, phone_number=self.user_phone_number)
|
||||
|
||||
scope = Scope.objects.create(name='accounts.status:get', description='accounts.status:get')
|
||||
|
||||
self.application = Application.objects.create(
|
||||
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
authorization_grant_type='password',
|
||||
hash_client_secret=False,
|
||||
uuid=self.application_uuid,
|
||||
user_id=self.user_uuid,
|
||||
max_allowed_session=1,
|
||||
allowed_scope='accounts.status:get',
|
||||
)
|
||||
|
||||
# self.sys_date_patcher = patch('simata_safte.models.get_sys_date', mock_get_sys_date)
|
||||
# self.set_id_patcher = patch('simata_safte.models.set_id', mock_set_id)
|
||||
# self.sign_pdf_patcher = patch('simata_safte.models.sign_pdf', mock_sign_pdf)
|
||||
# self.check_pdf_signature_patcher = patch('simata_safte.models.check_pdf_signature', mock_check_pdf_signature)
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
|
||||
|
||||
|
||||
def _create_authorization_header(self, token):
|
||||
return "Bearer {0}".format(token)
|
||||
|
||||
def test_authentication_allow(self):
|
||||
access_token_1 = AccessToken.objects.create(
|
||||
**{
|
||||
"token": self.access_token_1,
|
||||
"user": self.user,
|
||||
# "client_id": self.client_id,
|
||||
# "client_owner": owner,
|
||||
"application_id": self.application_uuid,
|
||||
"scope": 'accounts.status:get',
|
||||
"expires": self.expire_datetime.isoformat(),
|
||||
},
|
||||
)
|
||||
|
||||
auth_1 = self._create_authorization_header(access_token_1.token)
|
||||
|
||||
response = self.client.get(reverse("core:status"), HTTP_AUTHORIZATION=auth_1)
|
||||
|
||||
self.assertContains(response, 'status')
|
||||
|
||||
access_token_2 = AccessToken.objects.create(
|
||||
**{
|
||||
"token": self.access_token_2,
|
||||
"user": self.user,
|
||||
|
||||
# "client_id": self.client_id,
|
||||
# "client_owner": owner,
|
||||
"application_id": self.application_uuid,
|
||||
"scope": 'accounts.status:get',
|
||||
"expires": self.expire_datetime.isoformat(),
|
||||
},
|
||||
)
|
||||
|
||||
auth_2 = self._create_authorization_header(access_token_2.token)
|
||||
|
||||
response = self.client.get(reverse("core:status"), HTTP_AUTHORIZATION=auth_1)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse("core:status"), HTTP_AUTHORIZATION=auth_2)
|
||||
self.assertEqual(response.status_code, status.HTTP_409_CONFLICT)
|
||||
|
||||
self.application.max_allowed_session = 0
|
||||
self.application.save()
|
||||
self.application.refresh_from_db()
|
||||
|
||||
response = self.client.get(reverse("core:status"), HTTP_AUTHORIZATION=auth_2)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def basic_auth_string(self, username, password):
|
||||
"""ساخت Basic Auth string"""
|
||||
import base64
|
||||
user_pass = f"{username}:{password}"
|
||||
basic_credentials = base64.b64encode(user_pass.encode('utf-8')).decode('utf-8')
|
||||
|
||||
return basic_credentials
|
||||
|
||||
def login(self):
|
||||
self.user.set_otp()
|
||||
|
||||
data = {
|
||||
"grant_type": "password",
|
||||
"username": self.user_phone_number,
|
||||
"password": '77501',
|
||||
"scope": 'accounts.status:get',
|
||||
"auth_fields": 'phone_number:otp'
|
||||
}
|
||||
self.client.credentials(
|
||||
HTTP_AUTHORIZATION='Basic ' + self.basic_auth_string(self.client_id, self.client_secret)
|
||||
)
|
||||
|
||||
result = self.client.post(reverse("gooyal_oauth2:token"), data=data)
|
||||
access_token = result.json()['access_token']
|
||||
self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + access_token)
|
||||
|
||||
self.assertEqual(result.status_code, 200)
|
||||
return result
|
||||
|
||||
|
||||
def test_loginByOTP_allOK_success(self):
|
||||
print(self.login())
|
||||
response = self.client.get(reverse("core:status"))
|
||||
print(response.status_code)
|
||||
|
||||
def test_revoke_token(self):
|
||||
self.user.set_otp()
|
||||
|
||||
data = {
|
||||
"grant_type": "password",
|
||||
"username": self.user_phone_number,
|
||||
"password": '77501',
|
||||
"scope": 'accounts.status:get',
|
||||
"auth_fields": 'phone_number:otp'
|
||||
}
|
||||
self.client.credentials(
|
||||
HTTP_AUTHORIZATION='Basic ' + self.basic_auth_string(self.client_id, self.client_secret)
|
||||
)
|
||||
result = self.client.post(reverse("gooyal_oauth2:token"), data=data)
|
||||
access_token = result.json()['access_token']
|
||||
|
||||
revoke_data = {
|
||||
"token": access_token,
|
||||
}
|
||||
|
||||
result = self.client.post(reverse("gooyal_oauth2:revoke-token"), data=revoke_data)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertEqual(AccessToken.objects.count(), 0)
|
||||
|
||||
self.user.refresh_from_db()
|
||||
self.user.set_otp()
|
||||
self.client.credentials(
|
||||
HTTP_AUTHORIZATION='Basic ' + self.basic_auth_string(self.client_id, self.client_secret)
|
||||
)
|
||||
result = self.client.post(reverse("gooyal_oauth2:token"), data=data)
|
||||
access_token = result.json()['access_token']
|
||||
access_token_object = AccessToken.objects.first()
|
||||
revoke_data = {
|
||||
"token": access_token_object.pk,
|
||||
}
|
||||
result = self.client.post(reverse("gooyal_oauth2:revoke-token"), data=revoke_data)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertEqual(AccessToken.objects.count(), 0)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
47
apps/gooyal_oauth2/throttling.py
Normal file
47
apps/gooyal_oauth2/throttling.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
from rest_framework.throttling import UserRateThrottle
|
||||
|
||||
from apps.gooyal_oauth2.models import AccessToken
|
||||
|
||||
|
||||
def get_application(request):
|
||||
try:
|
||||
application = request.auth.application
|
||||
except:
|
||||
application = None
|
||||
return application
|
||||
|
||||
|
||||
class TokenLimitThrottle:
|
||||
def allow_request(self, request, view):
|
||||
application = get_application(request)
|
||||
|
||||
if application:
|
||||
if hasattr(view, "max_allowed_session"):
|
||||
max_allowed_session = view.max_allowed_session
|
||||
else:
|
||||
max_allowed_session = application.max_allowed_session
|
||||
|
||||
if max_allowed_session:
|
||||
session_count = AccessToken.objects.filter(application=application, user=request.user).count()
|
||||
if max_allowed_session >= session_count:
|
||||
return True
|
||||
else:
|
||||
from utils.exceptions import Conflict
|
||||
raise Conflict(code='max_allowed_session_reached')
|
||||
|
||||
active_tokens = AccessToken.objects.filter(
|
||||
application=application, user=request.user
|
||||
).order_by("created")[:max_allowed_session].values_list("token", flat=True)
|
||||
if request.auth.token in active_tokens:
|
||||
return True
|
||||
else:
|
||||
from utils.exceptions import Conflict
|
||||
raise Conflict(code='max_allowed_session_reached')
|
||||
|
||||
else:
|
||||
return True
|
||||
|
||||
return True
|
||||
|
||||
def wait(self):
|
||||
pass
|
||||
|
|
@ -3,7 +3,7 @@ from django.urls import re_path, path
|
|||
from oauth2_provider import views
|
||||
from rest_framework import routers
|
||||
|
||||
from .views.introspect import IntrospectTokenView, IntrospectApplicationView, TokenView
|
||||
from .views.oauth_views import IntrospectTokenView, IntrospectApplicationView, TokenView, GooyalRevokeTokenView
|
||||
from .views import apis as api_views
|
||||
from .views import pages
|
||||
app_name = "gooyal_oauth2"
|
||||
|
|
|
|||
|
|
@ -1,22 +1,34 @@
|
|||
import base64
|
||||
import binascii
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timedelta
|
||||
from urllib.parse import unquote_plus
|
||||
|
||||
import requests
|
||||
# import service_clients
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils import timezone
|
||||
from django.utils.timezone import make_aware
|
||||
from oauth2_provider.models import get_access_token_model
|
||||
from oauth2_provider.exceptions import FatalClientError
|
||||
from oauth2_provider.models import get_access_token_model, get_application_model, get_id_token_model, get_grant_model, \
|
||||
get_refresh_token_model
|
||||
from oauth2_provider.oauth2_validators import OAuth2Validator as BaseOAuth2Validator
|
||||
from requests import Session
|
||||
|
||||
from utils.exceptions import ServiceUnavailable
|
||||
from .settings import oauth2_settings
|
||||
from django.conf import settings
|
||||
from django.db import router, transaction
|
||||
|
||||
log = logging.getLogger("oauth2_provider")
|
||||
|
||||
AccessTokenModel = get_access_token_model()
|
||||
UserModel = get_user_model()
|
||||
Application = get_application_model()
|
||||
AccessToken = get_access_token_model()
|
||||
IDToken = get_id_token_model()
|
||||
Grant = get_grant_model()
|
||||
RefreshToken = get_refresh_token_model()
|
||||
|
||||
|
||||
class OAuth2Validator(BaseOAuth2Validator): # pylint: disable=w0223
|
||||
|
|
@ -77,3 +89,199 @@ class OAuth2Validator(BaseOAuth2Validator): # pylint: disable=w0223
|
|||
|
||||
def get_oidc_issuer_endpoint(self, request):
|
||||
return oauth2_settings.oidc_issuer(request)
|
||||
|
||||
def save_token(self, token, request, *args, **kwargs):
|
||||
"""Persist the token with a token type specific method.
|
||||
|
||||
Currently, only save_bearer_token is supported.
|
||||
|
||||
:param token: A (Bearer) token dict.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
"""
|
||||
return self.save_bearer_token(token, request, *args, **kwargs)
|
||||
|
||||
def save_bearer_token(self, token, request, *args, **kwargs):
|
||||
"""
|
||||
Save access and refresh token.
|
||||
|
||||
Override _save_bearer_token and not this function when adding custom logic
|
||||
for the storing of these token. This allows the transaction logic to be
|
||||
separate from the token handling.
|
||||
"""
|
||||
# Use the AccessToken's database instead of making the assumption it is in 'default'.
|
||||
with transaction.atomic(using=router.db_for_write(AccessToken)):
|
||||
return self._save_bearer_token(token, request, *args, **kwargs)
|
||||
|
||||
def _save_bearer_token(self, token, request, *args, **kwargs):
|
||||
"""
|
||||
Save access and refresh token.
|
||||
|
||||
If refresh token is issued, remove or reuse old refresh token as in rfc:`6`.
|
||||
|
||||
@see: https://rfc-editor.org/rfc/rfc6749.html#section-6
|
||||
"""
|
||||
|
||||
if "scope" not in token:
|
||||
raise FatalClientError("Failed to renew access token: missing scope")
|
||||
|
||||
# expires_in is passed to Server on initialization
|
||||
# custom server class can have logic to override this
|
||||
expires = timezone.now() + timedelta(
|
||||
seconds=token.get(
|
||||
"expires_in",
|
||||
oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS,
|
||||
)
|
||||
)
|
||||
|
||||
if request.grant_type == "client_credentials":
|
||||
request.user = None
|
||||
|
||||
# This comes from OAuthLib:
|
||||
# https://github.com/idan/oauthlib/blob/1.0.3/oauthlib/oauth2/rfc6749/tokens.py#L267
|
||||
# Its value is either a new random code; or if we are reusing
|
||||
# refresh tokens, then it is the same value that the request passed in
|
||||
# (stored in `request.refresh_token`)
|
||||
refresh_token_code = token.get("refresh_token", None)
|
||||
|
||||
if refresh_token_code:
|
||||
# an instance of `RefreshToken` that matches the old refresh code.
|
||||
# Set on the request in `validate_refresh_token`
|
||||
refresh_token_instance = getattr(request, "refresh_token_instance", None)
|
||||
|
||||
# If we are to reuse tokens, and we can: do so
|
||||
if (
|
||||
not self.rotate_refresh_token(request)
|
||||
and isinstance(refresh_token_instance, RefreshToken)
|
||||
and refresh_token_instance.access_token
|
||||
):
|
||||
access_token = AccessToken.objects.select_for_update().get(
|
||||
pk=refresh_token_instance.access_token.pk
|
||||
)
|
||||
access_token.user = request.user
|
||||
access_token.scope = token["scope"]
|
||||
access_token.expires = expires
|
||||
access_token.token = token["access_token"]
|
||||
access_token.application = request.client
|
||||
access_token.save()
|
||||
|
||||
# else create fresh with access & refresh tokens
|
||||
else:
|
||||
# revoke existing tokens if possible to allow reuse of grant
|
||||
if isinstance(refresh_token_instance, RefreshToken):
|
||||
# First, to ensure we don't have concurrency issues, we refresh the refresh token
|
||||
# from the db while acquiring a lock on it
|
||||
# We also put it in the "request cache"
|
||||
refresh_token_instance = RefreshToken.objects.select_for_update().get(
|
||||
pk=refresh_token_instance.pk
|
||||
)
|
||||
request.refresh_token_instance = refresh_token_instance
|
||||
|
||||
previous_access_token = AccessToken.objects.filter(
|
||||
source_refresh_token=refresh_token_instance
|
||||
).first()
|
||||
try:
|
||||
refresh_token_instance.revoke()
|
||||
except (AccessToken.DoesNotExist, RefreshToken.DoesNotExist):
|
||||
pass
|
||||
else:
|
||||
setattr(request, "refresh_token_instance", None)
|
||||
else:
|
||||
previous_access_token = None
|
||||
|
||||
# If the refresh token has already been used to create an
|
||||
# access token (ie it's within the grace period), return that
|
||||
# access token
|
||||
if not previous_access_token:
|
||||
access_token = self._create_access_token(
|
||||
expires,
|
||||
request,
|
||||
token,
|
||||
source_refresh_token=refresh_token_instance,
|
||||
)
|
||||
|
||||
self._create_refresh_token(
|
||||
request, refresh_token_code, access_token, refresh_token_instance
|
||||
)
|
||||
else:
|
||||
# make sure that the token data we're returning matches
|
||||
# the existing token
|
||||
token["access_token"] = previous_access_token.token
|
||||
token["refresh_token"] = (
|
||||
RefreshToken.objects.filter(access_token=previous_access_token).first().token
|
||||
)
|
||||
token["scope"] = previous_access_token.scope
|
||||
|
||||
# No refresh token should be created, just access token
|
||||
else:
|
||||
self._create_access_token(expires, request, token)
|
||||
|
||||
def _create_access_token(self, expires, request, token, source_refresh_token=None):
|
||||
id_token = token.get("id_token", None)
|
||||
if id_token:
|
||||
id_token = self._load_id_token(id_token)
|
||||
headers = {}
|
||||
for header, value in dict(request.headers).items():
|
||||
if type(value) in [str, bool, int, float, tuple, list]:
|
||||
print(f'unserializable header: {header} -> {value}')
|
||||
headers[header] = value
|
||||
|
||||
return AccessToken.objects.create(
|
||||
user=request.user,
|
||||
scope=token["scope"],
|
||||
expires=expires,
|
||||
token=token["access_token"],
|
||||
id_token=id_token,
|
||||
application=request.client,
|
||||
source_refresh_token=source_refresh_token,
|
||||
detail={'headers': headers},
|
||||
)
|
||||
|
||||
# def _get_token_from_authentication_server
|
||||
# def validate_bearer_token(self, token, scopes, request):
|
||||
# result = super().validate_bearer_token(token, scopes, request)
|
||||
# if result:
|
||||
# application = request.client
|
||||
# if hasattr(request, "max_allowed_session"):
|
||||
# max_allowed_session = request.max_allowed_session
|
||||
# else:
|
||||
# max_allowed_session = application.max_allowed_session
|
||||
#
|
||||
# if max_allowed_session:
|
||||
# session_count = AccessToken.objects.filter(application=application, user=request.user).count()
|
||||
# if max_allowed_session >= session_count:
|
||||
# return result
|
||||
# else:
|
||||
# active_tokens = AccessToken.objects.filter(
|
||||
# application=application, user=request.user
|
||||
# ).order_by("created")[:max_allowed_session].values_list("token", flat=True)
|
||||
# if token in active_tokens:
|
||||
# return result
|
||||
# else:
|
||||
# raise ServiceUnavailable(code='max_allowed_session_reached')
|
||||
#
|
||||
# else:
|
||||
# return result
|
||||
|
||||
def revoke_token(self, token, token_type_hint, request, *args, **kwargs):
|
||||
"""
|
||||
Revoke an access or refresh token.
|
||||
|
||||
:param token: The token string.
|
||||
:param token_type_hint: access_token or refresh_token.
|
||||
:param request: The HTTP Request (oauthlib.common.Request)
|
||||
"""
|
||||
if token_type_hint not in ["access_token", "refresh_token"]:
|
||||
token_type_hint = None
|
||||
|
||||
token_types = {
|
||||
"access_token": AccessToken,
|
||||
"refresh_token": RefreshToken,
|
||||
}
|
||||
|
||||
token_type = token_types.get(token_type_hint, AccessToken)
|
||||
|
||||
try:
|
||||
token_type.objects.get(pk=token).revoke()
|
||||
except:
|
||||
token_type.objects.get(token=token).revoke()
|
||||
|
|
|
|||
|
|
@ -167,3 +167,17 @@ class TokenView(OAuthLibMixin, View):
|
|||
response[k] = v
|
||||
return response
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
@method_decorator(login_not_required, name="dispatch")
|
||||
class GooyalRevokeTokenView(OAuthLibMixin, View):
|
||||
"""
|
||||
Implements an endpoint to revoke access or refresh tokens
|
||||
"""
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
url, headers, body, status = self.create_revocation_response(request)
|
||||
response = HttpResponse(content=body or "", status=status)
|
||||
|
||||
for k, v in headers.items():
|
||||
response[k] = v
|
||||
return response
|
||||
|
|
@ -11,10 +11,7 @@ class UserAdmin(admin.ModelAdmin):
|
|||
'first_name',
|
||||
'last_name',
|
||||
'username',
|
||||
'email',
|
||||
'phone_number',
|
||||
'password',
|
||||
'otp',
|
||||
'is_staff',
|
||||
'is_superuser',
|
||||
'groups',
|
||||
|
|
@ -24,10 +21,9 @@ class UserAdmin(admin.ModelAdmin):
|
|||
'last_update',
|
||||
'otp_expire',
|
||||
'otp_try',
|
||||
'balance',
|
||||
]
|
||||
readonly_fields = ['last_update', 'uuid']
|
||||
list_display = ['pk', 'first_name', 'last_name', 'phone_number', 'date_joined', 'last_update']
|
||||
list_display = ['pk', 'date_joined', 'last_update']
|
||||
search_fields = ['pk', 'first_name', 'last_name', 'phone_number']
|
||||
|
||||
|
||||
|
|
|
|||
17
apps/users/migrations/0008_alter_user_options.py
Normal file
17
apps/users/migrations/0008_alter_user_options.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 6.0.2 on 2026-03-16 12:23
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0007_alter_user_otp'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='user',
|
||||
options={'ordering': ['-date_joined'], 'verbose_name': 'user', 'verbose_name_plural': 'users'},
|
||||
),
|
||||
]
|
||||
|
|
@ -15,8 +15,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
import uuid
|
||||
|
||||
from django_minio_backend import MinioBackend, iso_date_prefix
|
||||
|
||||
from utils.clients.payam_sms import send_sms
|
||||
from apps.core.models import SMSPolicy
|
||||
from .provinces_and_cities import state
|
||||
# from .tasks import send_notification
|
||||
|
||||
|
|
@ -193,6 +192,8 @@ class User(AbstractUser):
|
|||
|
||||
if result:
|
||||
self.otp = None
|
||||
self.otp_expire = None
|
||||
|
||||
if not self.date_joined:
|
||||
self.date_joined = timezone.now()
|
||||
else:
|
||||
|
|
@ -200,7 +201,9 @@ class User(AbstractUser):
|
|||
self.save()
|
||||
else:
|
||||
result = False
|
||||
|
||||
self.otp = None
|
||||
self.otp_expire = None
|
||||
self.save()
|
||||
|
||||
return result
|
||||
|
||||
|
|
@ -247,9 +250,10 @@ class User(AbstractUser):
|
|||
# # f"code is: {body}\n"
|
||||
# f"{settings.SMS_OTP_SIGNITURE}"
|
||||
)
|
||||
if settings.SMS_SEND:
|
||||
send_sms(self.phone_number.strip('+'), message)
|
||||
else:
|
||||
|
||||
sms_client = SMSPolicy.get_client()
|
||||
sms_client.send_sms(self.phone_number.strip('+'), message)
|
||||
|
||||
logger.info(f'otp for: {self.phone_number} is {message}')
|
||||
return
|
||||
# TODO: enable celery
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from django.core.validators import RegexValidator
|
||||
from oauth2_provider.models import get_access_token_model
|
||||
from rest_framework import serializers
|
||||
|
||||
from apps.users.models import User
|
||||
|
|
@ -126,3 +127,25 @@ class ChangePasswordSerializer(serializers.Serializer):
|
|||
old_password = serializers.CharField(required=True)
|
||||
old_password_field = serializers.CharField(default='password')
|
||||
new_password = serializers.CharField(required=True)
|
||||
|
||||
|
||||
AccessToken = get_access_token_model()
|
||||
class SessionSerializer(serializers.ModelSerializer):
|
||||
is_current = serializers.SerializerMethodField()
|
||||
|
||||
def get_is_current(self, obj):
|
||||
request = self.context.get('request')
|
||||
token = request and request.auth and request.auth.token
|
||||
if token == obj.token:
|
||||
result = True
|
||||
|
||||
else:
|
||||
result = False
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class Meta:
|
||||
model = AccessToken
|
||||
fields = ('uuid', 'created', "detail", "is_current")
|
||||
read_only_fields = ['uuid', 'created', "detail", "is_current" ]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,154 @@
|
|||
from django.test import TestCase
|
||||
import base64
|
||||
import json
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
from unicodedata import category
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from oauth2_provider.models import get_access_token_model, get_application_model
|
||||
from rest_framework.test import APIClient, APITestCase
|
||||
|
||||
|
||||
from apps.core.models import Config
|
||||
from apps.gooyal_oauth2.models import Scope
|
||||
from apps.users.models import User
|
||||
|
||||
AccessToken = get_access_token_model()
|
||||
Application = get_application_model()
|
||||
|
||||
|
||||
class UserTests(APITestCase):
|
||||
user_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb47005f')
|
||||
access_token_1 = 'au4naVsdKCbKNOhnElPyXcrwSnqqFbm'
|
||||
access_token_2 = 'vu4naVsdKCbKNOhnElPyXcrwSnqqFbm'
|
||||
application_uuid = uuid.UUID('a14e8b86-8f4a-44d9-b29d-badceb47005f')
|
||||
client_id = '4INGOCMoulE0fNY1SQlTbPtsWqqxGj2DdqjADq6u'
|
||||
client_secret = '4INGOCMoulE0fNY1SQlTbPtsWqqxGj2DdqjADq6u'
|
||||
|
||||
visitor_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb47005a')
|
||||
|
||||
expire_datetime = timezone.now() + timedelta(seconds=3600)
|
||||
expire_datetime.isoformat()
|
||||
client = APIClient()
|
||||
|
||||
def setUp(self):
|
||||
from django.conf import settings
|
||||
settings.SMS_SEND = False
|
||||
self.user_phone_number = '+989100000000'
|
||||
self.user = User.objects.create(pk=self.user_uuid, phone_number=self.user_phone_number)
|
||||
|
||||
Scope.objects.create(name='accounts.status:get', description='accounts.status:get')
|
||||
Scope.objects.create(name='accounts.account:retrieve', description='accounts.account:retrieve')
|
||||
|
||||
self.application = Application.objects.create(
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
authorization_grant_type='password',
|
||||
hash_client_secret=False,
|
||||
uuid=self.application_uuid,
|
||||
user_id=self.user_uuid,
|
||||
max_allowed_session=1,
|
||||
allowed_scope='accounts.status:get accounts.account:retrieve',
|
||||
)
|
||||
|
||||
# self.sys_date_patcher = patch('simata_safte.models.get_sys_date', mock_get_sys_date)
|
||||
# self.set_id_patcher = patch('simata_safte.models.set_id', mock_set_id)
|
||||
# self.sign_pdf_patcher = patch('simata_safte.models.sign_pdf', mock_sign_pdf)
|
||||
# self.check_pdf_signature_patcher = patch('simata_safte.models.check_pdf_signature', mock_check_pdf_signature)
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
|
||||
|
||||
def _create_authorization_header(self, token):
|
||||
return "Bearer {0}".format(token)
|
||||
|
||||
def test_authentication_allow(self):
|
||||
access_token_1 = AccessToken.objects.create(
|
||||
**{
|
||||
"token": self.access_token_1,
|
||||
"user": self.user,
|
||||
# "client_id": self.client_id,
|
||||
# "client_owner": owner,
|
||||
"application_id": self.application_uuid,
|
||||
"scope": 'accounts.status:get accounts.account:retrieve',
|
||||
"expires": self.expire_datetime.isoformat(),
|
||||
},
|
||||
)
|
||||
|
||||
auth_1 = self._create_authorization_header(access_token_1.token)
|
||||
|
||||
response = self.client.get(reverse("core:status"), HTTP_AUTHORIZATION=auth_1)
|
||||
|
||||
self.assertContains(response, 'status')
|
||||
|
||||
access_token_2 = AccessToken.objects.create(
|
||||
**{
|
||||
"token": self.access_token_2,
|
||||
"user": self.user,
|
||||
|
||||
# "client_id": self.client_id,
|
||||
# "client_owner": owner,
|
||||
"application_id": self.application_uuid,
|
||||
"scope": 'accounts.status:get',
|
||||
"expires": self.expire_datetime.isoformat(),
|
||||
},
|
||||
)
|
||||
|
||||
auth_2 = self._create_authorization_header(access_token_2.token)
|
||||
|
||||
response = self.client.get(reverse("core:status"), HTTP_AUTHORIZATION=auth_1)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse("core:status"), HTTP_AUTHORIZATION=auth_2)
|
||||
self.assertEqual(response.status_code, 503)
|
||||
|
||||
self.application.max_allowed_session = 0
|
||||
self.application.save()
|
||||
self.application.refresh_from_db()
|
||||
|
||||
response = self.client.get(reverse("core:status"), HTTP_AUTHORIZATION=auth_2)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def basic_auth_string(self, username, password):
|
||||
"""ساخت Basic Auth string"""
|
||||
import base64
|
||||
user_pass = f"{username}:{password}"
|
||||
basic_credentials = base64.b64encode(user_pass.encode('utf-8')).decode('utf-8')
|
||||
|
||||
return basic_credentials
|
||||
|
||||
def login(self):
|
||||
self.user.set_otp()
|
||||
|
||||
data = {
|
||||
"grant_type": "password",
|
||||
"username": self.user_phone_number,
|
||||
"password": '77501',
|
||||
"scope": 'accounts.status:get accounts.account:retrieve',
|
||||
"auth_fields": 'phone_number:otp'
|
||||
}
|
||||
self.client.credentials(
|
||||
HTTP_AUTHORIZATION='Basic ' + self.basic_auth_string(self.client_id, self.client_secret)
|
||||
)
|
||||
|
||||
result = self.client.post(reverse("gooyal_oauth2:token"), data=data)
|
||||
access_token = result.json()['access_token']
|
||||
self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + access_token)
|
||||
|
||||
self.assertEqual(result.status_code, 200)
|
||||
return result
|
||||
|
||||
|
||||
def test_getSessions_allOK_success(self):
|
||||
self.login()
|
||||
response = self.client.get(reverse("users:user_sessions_api"))
|
||||
self.assertEqual(response.json()['results'][0]['is_current'] , True)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Create your tests here.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ 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
|
||||
UserDetailedRetrieveView, UserSessionListView
|
||||
|
||||
app_name = "users"
|
||||
|
||||
|
|
@ -13,6 +13,7 @@ urlpatterns = [
|
|||
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/sessions/', UserSessionListView.as_view(), name='user_sessions_api'),
|
||||
path('api/users/<uuid>/', UserPublicRetrieveView.as_view(), name='user_public_retrieve_api'),
|
||||
path('api/users/<uuid>/avatar', UserCurrentAvatarUrlView.as_view(), name='user_avatar_api'),
|
||||
path('api/users/<uuid>/details', UserDetailedRetrieveView.as_view(), name='user_detailed_retrieve_api'),
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from apps.users.forms import OTPAuthenticationForm, ProfileUpdateForm
|
|||
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
|
||||
ChangePasswordSerializer, UserInquirySerializer, SessionSerializer
|
||||
from utils.throttles import RequestOTPDayRateThrottle, RequestOTPMinRateThrottle, NumberedRequestOTPDayRateThrottle, NumberedRequestOTPMinRateThrottle
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
|
@ -183,3 +183,20 @@ class ProfileUpdateView(UpdateView):
|
|||
|
||||
def form_valid(self, form):
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class UserSessionListView(generics.ListAPIView):
|
||||
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
|
||||
serializer_class = SessionSerializer
|
||||
filter_backends = (DjangoFilterBackend,)
|
||||
|
||||
max_allowed_session = 0
|
||||
|
||||
required_alternate_scopes = {
|
||||
"GET": [["accounts.account:retrieve"]],
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
from apps.gooyal_oauth2.models import AccessToken
|
||||
return AccessToken.objects.filter(user=self.request.user).all()
|
||||
|
||||
|
|
|
|||
32
client.py
32
client.py
|
|
@ -3,10 +3,10 @@ import time
|
|||
|
||||
import requests
|
||||
|
||||
OAUTH_CLIENT_ID = 'xrFXKf53jrxVOygbLEoqrtOlUwBP00jIQ4zVpzc6'
|
||||
OAUTH_CLIENT_SECRET = 'qelUdRCdEEalVdko5aHRojxsC3CaL29yjwxmc6FeWs39SeVSb6aM9mNrYQixMJNTYQK7s2wffwPW94JPPbP6jTdd9dSf1mlqYcr6hW2COuayFUk4jP33OWu4taUYP2F5'
|
||||
OAUTH_CLIENT_ID = '4INGOCMoulE0fNY1SQlTbPtsWqqxGj2DdqjADq6u'
|
||||
OAUTH_CLIENT_SECRET = 'KPc4dMSztNwAIB3vii3geXrzC1mKUIsAztz3t2ylC3HlrgJudJWhdrtoY6XeRJIuadTAREYYsXk9XtFHUDfPVcJvyfHMpNkz2VvghwrijhVprok0VYV7XrOirJ5nFUxD'
|
||||
|
||||
API_URI = 'https://accounts.gooyal.com'
|
||||
API_URI = 'https://accounts.gooyal.ir'
|
||||
|
||||
|
||||
# API_URI = 'http://127.0.0.1:8000'
|
||||
|
|
@ -27,7 +27,7 @@ class ApiClient():
|
|||
"grant_type": "password",
|
||||
"username": phone_number,
|
||||
"password": password,
|
||||
"scope": 'accounts.account:request_ott accounts.account:change_password accounts.profile:inquiry accounts.account:update accounts.account:retrieve accounts.profile:retrieve accounts.profile:list introspection wallet.wallet:get_balance',
|
||||
"scope": 'introspection',
|
||||
"auth_fields": 'phone_number:otp'
|
||||
}
|
||||
auth = (OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET)
|
||||
|
|
@ -51,7 +51,8 @@ class ApiClient():
|
|||
"grant_type": "password",
|
||||
"username": phone_number,
|
||||
"password": token,
|
||||
"scope": 'introspection education.course:retrieve education.course:submit superapp.applications:list accounts.account:request_ott accounts.profile:list accounts.profile:retrieve accounts.account:retrieve accounts.account:update accounts.profile:inquiry accounts.account:change_password wallet.transaction:list wallet.invoice:create wallet.transaction:retrieve wallet.invoice:pay wallet.invoice:receipt wallet.deposit:submit wallet.deposit:verify wallet.withdraw:submit wallet.withdraw:verify ',
|
||||
# "scope": 'introspection education.course:retrieve education.course:submit superapp.applications:list accounts.account:request_ott accounts.profile:list accounts.profile:retrieve accounts.account:retrieve accounts.account:update accounts.profile:inquiry accounts.account:change_password wallet.transaction:list wallet.invoice:create wallet.transaction:retrieve wallet.invoice:pay wallet.invoice:receipt wallet.deposit:submit wallet.deposit:verify wallet.withdraw:submit wallet.withdraw:verify ',
|
||||
"scope": 'introspection',
|
||||
"auth_fields": 'phone_number:ott'
|
||||
}
|
||||
auth = (OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET)
|
||||
|
|
@ -156,6 +157,7 @@ class ApiClient():
|
|||
}
|
||||
url = f'{API_URI}/{path}'
|
||||
response = self._request(url, method='PUT', data=data)
|
||||
print(response)
|
||||
return response and 'code' in response, response
|
||||
|
||||
def introspect_account(self, token):
|
||||
|
|
@ -185,19 +187,29 @@ class ApiClient():
|
|||
|
||||
return self._request(url=url)
|
||||
|
||||
def get_wallet_balance(self):
|
||||
url = 'http://127.0.0.1:8000/wallet/api/wallet/default/balance'
|
||||
def get_application_preferences(self):
|
||||
url = 'https://preferences.gooyal.com/data/application/'
|
||||
|
||||
return self._request(url=url)
|
||||
|
||||
|
||||
auth_data = {'access_token': 'DJ1mycH9r5FCwaNgDA9nB9uU3KWt9m', 'expires_in': 36000, 'token_type': 'Bearer', 'scope': 'accounts.account:change_password accounts.account:update accounts.account:retrieve wallet.wallet:get_balance billboard_merchant.billboard:retrieve billboard_merchant.billboard:update billboard_merchant.billboard:create wallet.user:transaction_list data_crud.data:retrieve data_crud.data:update data_crud.data:delete', 'refresh_token': 'VF4P11tfcnGv9tc2u0qH6035OW5qU4'}
|
||||
|
||||
client = ApiClient('+989106853582')
|
||||
# client.auth_data = auth_data
|
||||
# print(client.login_as_client_credentials())
|
||||
print(client.request_otp())
|
||||
print(client.otp_login('12345'))
|
||||
print(client.introspect_account('V3LUQ9OryHOy6q5iWwLBRAtRBm80ex'))
|
||||
print(client.get_wallet_balance())
|
||||
import time
|
||||
start = time.time()
|
||||
print(client.otp_login('77502'))
|
||||
# end = time.time()
|
||||
# print(end - start)
|
||||
# print(client.get_application_preferences())
|
||||
# exit()
|
||||
# print(client.introspect_account('V3LUQ9OryHOy6q5iWwLBRAtRBm80ex'))
|
||||
# print(client.get_wallet_balance())
|
||||
# ott = client.get_application_token()[1]['ott']
|
||||
# print(client.ott_login(ott))
|
||||
# print(ott)
|
||||
client.update_account('test')
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ INSTALLED_APPS = [
|
|||
'crispy_forms',
|
||||
'crispy_bootstrap5',
|
||||
'django_filters',
|
||||
'jalali_date',
|
||||
# 'jalali_date',
|
||||
'django_minio_backend.apps.DjangoMinioBackendConfig',
|
||||
|
||||
# local apps
|
||||
|
|
@ -119,6 +119,11 @@ REST_FRAMEWORK = {
|
|||
# 'DEFAULT_THROTTLE_CLASSES': [
|
||||
# 'rest_framework.throttling.AnonRateThrottle',
|
||||
# ],
|
||||
'DEFAULT_THROTTLE_CLASSES': [
|
||||
'apps.gooyal_oauth2.throttling.TokenLimitThrottle',
|
||||
# 'rest_framework.throttling.AnonRateThrottle',
|
||||
# 'rest_framework.throttling.UserRateThrottle'
|
||||
],
|
||||
'DEFAULT_THROTTLE_RATES': {
|
||||
'otp_min': '2/min',
|
||||
'otp_day': '50/day',
|
||||
|
|
@ -227,7 +232,6 @@ LANGUAGES = [
|
|||
('tr', _('Turkish')),
|
||||
]
|
||||
|
||||
|
||||
LOCALE_PATHS = [
|
||||
BASE_DIR / 'locale',
|
||||
]
|
||||
|
|
@ -277,12 +281,10 @@ PAYAM_SMS_PASSWORD = config('PAYAM_SMS_PASSWORD')
|
|||
PAYAM_SMS_CLIENT_ID = config('PAYAM_SMS_CLIENT_ID')
|
||||
PAYAM_SMS_CLIENT_SECRET = config('PAYAM_SMS_CLIENT_SECRET')
|
||||
|
||||
|
||||
SMS_OTP_SIGNITURE = config('SMS_OTP_SIGNITURE', '')
|
||||
SMS_SEND = config('SMS_SEND', True, cast=bool)
|
||||
TEST_PHONENUMBERS = config('TEST_PHONENUMBERS', [], cast=Csv(post_process=list))
|
||||
|
||||
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
|
|
@ -307,8 +309,10 @@ STORAGES = {
|
|||
"MINIO_ENDPOINT": config('MINIO_ENDPOINT', default='drive.gooyal.com'),
|
||||
"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
|
||||
"MINIO_EXTERNAL_ENDPOINT_USE_HTTPS": config('MINIO_EXTERNAL_ENDPOINT_USE_HTTPS', default=True, cast=bool), # Default is same as MINIO_USE_HTTPS
|
||||
"MINIO_EXTERNAL_ENDPOINT": config('MINIO_EXTERNAL_ENDPOINT', default='drive.gooyal.com'),
|
||||
# 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
|
||||
"MINIO_ACCESS_KEY": config('MINIO_ACCESS_KEY'),
|
||||
"MINIO_SECRET_KEY": config('MINIO_SECRET_KEY'),
|
||||
|
|
@ -357,8 +361,10 @@ MINIO_HTTP_CLIENT: urllib3.poolmanager.PoolManager = urllib3.PoolManager(
|
|||
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
|
||||
MINIO_EXTERNAL_ENDPOINT_USE_HTTPS = config('MINIO_EXTERNAL_ENDPOINT_USE_HTTPS', default=True, cast=bool) # Default is same as MINIO_USE_HTTPS
|
||||
MINIO_EXTERNAL_ENDPOINT = config('MINIO_EXTERNAL_ENDPOINT',
|
||||
default='drive.gooyal.com') # 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
|
||||
MINIO_ACCESS_KEY = config('MINIO_ACCESS_KEY')
|
||||
MINIO_SECRET_KEY = config('MINIO_SECRET_KEY')
|
||||
|
|
@ -377,3 +383,4 @@ MINIO_PUBLIC_BUCKETS = [
|
|||
MINIO_POLICY_HOOKS: List[Tuple[str, dict]] = []
|
||||
MINIO_BUCKET_CHECK_ON_SAVE = True # Default: True // Creates bucket if missing, then save
|
||||
|
||||
MOBIN_SMS_TOKEN = config('MOBIN_SMS_TOKEN', default='')
|
||||
|
|
|
|||
8
run.sh
8
run.sh
|
|
@ -1,8 +1,8 @@
|
|||
#!/usr/bin/env bash
|
||||
while ! nc -z $DB_HOST 5432 ; do
|
||||
echo "APP Waiting for the DB Server"
|
||||
sleep 3
|
||||
done
|
||||
#while ! nc -z $DB_HOST 5432 ; do
|
||||
# echo "APP Waiting for the DB Server"
|
||||
# sleep 3
|
||||
#done
|
||||
#python3 manage.py collectstatic --noinput
|
||||
python3 manage.py migrate
|
||||
gunicorn main.wsgi:application --bind 0.0.0.0:8000 -w 4
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load jalali_tags %}
|
||||
{#{% load jalali_tags %}#}
|
||||
|
||||
{% block title %}Accounts: Home{% endblock %}
|
||||
{% block page_title %}Accounts: Home{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load jalali_tags %}
|
||||
{#{% load jalali_tags %}#}
|
||||
|
||||
{% block title %}Application Register{% endblock %}
|
||||
{% block page_title %}Application Register{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load jalali_tags %}
|
||||
{#{% load jalali_tags %}#}
|
||||
|
||||
{% block title %}Application Detail: {{ object }}{% endblock %}
|
||||
{% block page_title %}Application Detail: {{ object }}{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load jalali_tags %}
|
||||
{#{% load jalali_tags %}#}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load tags %}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load jalali_tags %}
|
||||
{#{% load jalali_tags %}#}
|
||||
|
||||
{% block title %}Application Scope Register{% endblock %}
|
||||
{% block page_title %}Application Scope Register{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load jalali_tags %}
|
||||
{#{% load jalali_tags %}#}
|
||||
|
||||
{% block title %}Application Scope Detail: {{ object.name }}{% endblock %}
|
||||
{% block page_title %}Application Scope Detail: {{ object.name }}{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load jalali_tags %}
|
||||
{#{% load jalali_tags %}#}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load tags %}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load jalali_tags %}
|
||||
{#{% load jalali_tags %}#}
|
||||
|
||||
{% block title %}Application Update: {{ object.name }}{% endblock %}
|
||||
{% block page_title %}Application Update: {{ object.name }}{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load jalali_tags %}
|
||||
{#{% load jalali_tags %}#}
|
||||
|
||||
{% block title %}Application Update: {{ object.name }}{% endblock %}
|
||||
{% block page_title %}Application Update: {{ object.name }}{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load jalali_tags %}
|
||||
{#{% load jalali_tags %}#}
|
||||
|
||||
{% block title %}user account{% endblock %}
|
||||
{% block page_title %}user account{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load jalali_tags %}
|
||||
{#{% load jalali_tags %}#}
|
||||
|
||||
{% block title %}user account{% endblock %}
|
||||
{% block page_title %}user account{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
# TODO: this is time fourced code. refactor
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def get_access_token():
|
||||
payam_sms_system_name = settings.PAYAM_SMS_SYSTEM_NAME
|
||||
payam_sms_username = settings.PAYAM_SMS_USERNAME
|
||||
payam_sms_password = settings.PAYAM_SMS_PASSWORD
|
||||
payam_sms_client_id = settings.PAYAM_SMS_CLIENT_ID
|
||||
payam_sms_client_secret = settings.PAYAM_SMS_CLIENT_SECRET
|
||||
auth = (payam_sms_client_id, payam_sms_client_secret)
|
||||
url = f"https://www.payamsms.com/auth/oauth/token?systemName={payam_sms_system_name}&username={payam_sms_username}&password={payam_sms_password}&scope=webservice&grant_type=password"
|
||||
|
||||
response_object = requests.post(url, auth=auth, json={})
|
||||
response = response_object.json()
|
||||
return response["access_token"]
|
||||
|
||||
|
||||
def send_sms(phone_number, message):
|
||||
try:
|
||||
url = "https://www.payamsms.com/panel/webservice/send"
|
||||
|
||||
# access_token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJzb2JhbiIsInNjb3BlIjpbIndlYnNlcnZpY2UiXSwiZXhwIjoxNzQ0MTAyMDU5LCJ1c2VyRGV0YWlscyI6eyJpZCI6MjU0MCwidXNlck5hbWUiOiJzb2JhbiIsInJvbGUiOiJVU0VSIiwiZW5hYmxlZCI6dHJ1ZSwic3lzdGVtSWQiOjcyMiwiYWNjZXNzIjp7InJvbGUiOlsiYWRkIiwiZWRpdCIsImdldCIsImRlbGV0ZSJdLCJwaG9uZUJvb2siOlsiYWRkIiwiZWRpdCIsImdldExvY2FsIiwiZ2V0R2xvYmFsIiwiZGVsZXRlIl0sInNtcyI6WyJzaW5nbGUiLCJnZXRMb2NhbCIsImdldEdsb2JhbCIsImJ1bGsiLCJkYiJdLCJyZXBvcnQiOlsiZ2V0TW9HbG9iYWwiLCJnZXREYWlseUxvY2FsIiwiZ2V0TW9Mb2NhbCIsImdldERhaWx5R2xvYmFsIl0sIndzIjpbIndzIl0sInVzZXIiOlsiYWRkIiwiZWRpdCIsImdldExvY2FsIiwiZ2V0IiwiZ2V0R2xvYmFsIiwiZGVsZXRlIl0sInRvb2xzIjpbInJhY2UiLCJwZXJpb2RpYyIsInBvbGxpbmciLCJzZWNyZXRvcnkiLCJhdXRvYW5zd2VyIl0sImZpbmFuY2UiOlsidGRyIiwiY2hhcmdlIl19LCJyb2xlSWQiOjQ0NDAsImNoaWxkcmVuSWRzIjpbXSwic3lzdGVtVHlwZSI6Ik5PUk1BTCIsInN5c3RlbVBheW1lbnRUeXBlIjoiREVCSVQiLCJzeXN0ZW1OYW1lIjoic29iYW4iLCJzZW5kV2l0aE91dFBlcm1pc3Npb25TeXN0ZW0iOmZhbHNlLCJtaXNDdXN0b21lciI6Itio2YbYr9in2LEg2LPZiNio2KfZhiDYs9in2YXYp9mG2YciLCJwYXJlbnRVc2VyTmFtZSI6bnVsbCwicGFyZW50SWQiOm51bGwsImFkbWluIjp0cnVlfSwiYXV0aG9yaXRpZXMiOlsiVVNFUiJdLCJqdGkiOiJNeU5vTml2RURmc3YxUDR3U20tbTR2Y2NHODgiLCJjbGllbnRfaWQiOiJzb2JhbiJ9.9mdneDduK4OVH2zsXvRqvXt2qwpLvs0vCuseLaB-LCo'
|
||||
access_token = get_access_token()
|
||||
|
||||
body = [
|
||||
{
|
||||
"sender": "98200000443295",
|
||||
"recipient": phone_number,
|
||||
"body": message,
|
||||
# "customerId": "1",
|
||||
# "sendDate": "2024-03-04 10:17:00"
|
||||
}
|
||||
]
|
||||
|
||||
headers = {
|
||||
'Authorization': f'Bearer {access_token}',
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
|
||||
response_object = requests.post(url, headers=headers, json=body, timeout=10)
|
||||
print(response_object.text)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
|
||||
# send_sms("989106853582", "تست")
|
||||
6
utils/clients/sms/fake/client.py
Normal file
6
utils/clients/sms/fake/client.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from utils.clients.sms.sms import BaseSMSClient
|
||||
|
||||
|
||||
class FakeClient(BaseSMSClient):
|
||||
def send_sms(self, phone_number, message):
|
||||
pass
|
||||
43
utils/clients/sms/mobin_sms/client.py
Normal file
43
utils/clients/sms/mobin_sms/client.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import urllib.parse
|
||||
import requests
|
||||
|
||||
from django.conf import settings
|
||||
import logging
|
||||
|
||||
from utils.clients.sms.sms import BaseSMSClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MobinSMSClient(BaseSMSClient):
|
||||
access_token = settings.MOBIN_SMS_TOKEN
|
||||
base_url = 'https://connect.mobinsms.com'
|
||||
sender = '9890008050'
|
||||
|
||||
def send_sms(self, phone_number, message):
|
||||
return self.send_sms_quick([phone_number], message)
|
||||
|
||||
def send_sms_quick(self, phone_number_list, message):
|
||||
try:
|
||||
path = '/v1/send/quick'
|
||||
url = urllib.parse.urljoin(self.base_url, path)
|
||||
|
||||
body = {
|
||||
"from": self.sender,
|
||||
"to": phone_number_list,
|
||||
"body": message,
|
||||
"unique_id": "" # UDH (optional field)
|
||||
}
|
||||
|
||||
headers = {
|
||||
"X-API-Key": self.access_token,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, json=body, timeout=10)
|
||||
logger.info(response.text)
|
||||
return response.json()
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f'error sending sms to {phone_number_list}: {e}')
|
||||
|
||||
|
|
@ -0,0 +1,387 @@
|
|||
{
|
||||
"info": {
|
||||
"_postman_id": "9cc27226-bee8-4b29-a924-a9e14bb61d14",
|
||||
"name": "Connect API",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
"_exporter_id": "2960438"
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "Send by Pattern",
|
||||
"protocolProfileBehavior": {
|
||||
"followRedirects": true,
|
||||
"disableUrlEncoding": false,
|
||||
"disableCookies": false
|
||||
},
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "X-API-Key",
|
||||
"value": "{{apiKey}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n\t\"from\": \"989000xxxx\",\n\t\"to\": [\"989126880345\"],\n\t\"pattern\": \"YOUR_PATTERN_KEY\",\n\t\"data\": {\n\t\t\"0\": \"Var 0\",\n\t\t\"1\": \"Var 1\",\n\t\t\"2\": \"Var 2\",\n\t\t\"3\": \"Var 3\"\n\t}\n}\n",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseURL}}/v1/send/pattern",
|
||||
"host": [
|
||||
"{{baseURL}}"
|
||||
],
|
||||
"path": [
|
||||
"v1",
|
||||
"send",
|
||||
"pattern"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Send Quick",
|
||||
"protocolProfileBehavior": {
|
||||
"followRedirects": true,
|
||||
"disableUrlEncoding": false,
|
||||
"disableCookies": false
|
||||
},
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "X-API-Key",
|
||||
"value": "{{apiKey}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n\t\"from\": \"989000xxxx\",\n\t\"to\": [\"989126880345\", \"989127761851\"],\n\t\"body\": \"Your message\",\n \"unique_id\": \"\" // UDH (optional field)\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseURL}}/v1/send/quick",
|
||||
"host": [
|
||||
"{{baseURL}}"
|
||||
],
|
||||
"path": [
|
||||
"v1",
|
||||
"send",
|
||||
"quick"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Get Status",
|
||||
"protocolProfileBehavior": {
|
||||
"disableBodyPruning": true,
|
||||
"followRedirects": true,
|
||||
"disableUrlEncoding": false,
|
||||
"disableCookies": false
|
||||
},
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "X-API-Key",
|
||||
"value": "{{apiKey}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseURL}}/v1/message/status?message_id=1893",
|
||||
"host": [
|
||||
"{{baseURL}}"
|
||||
],
|
||||
"path": [
|
||||
"v1",
|
||||
"message",
|
||||
"status"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "unique_id",
|
||||
"value": "20",
|
||||
"disabled": true
|
||||
},
|
||||
{
|
||||
"key": "message_id",
|
||||
"value": "1893"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "Delivered Message",
|
||||
"originalRequest": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "User-Agent",
|
||||
"value": "insomnia/11.6.2"
|
||||
},
|
||||
{
|
||||
"key": "X-API-Key",
|
||||
"value": "07e7e9c79652b857aa77b8f3d40cc4314321970e1a5f78b9fc18e497aca2bf40"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n\t\"from\": \"9890001777\",\n\t\"to\": [\"989126880345\"],\n\t\"body\": \"Your message\",\n \"unique_id\": \"6\" // UDH (optional field)\n}\n",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseURL}}/v1/message/status?message_id=1852",
|
||||
"host": [
|
||||
"{{baseURL}}"
|
||||
],
|
||||
"path": [
|
||||
"v1",
|
||||
"message",
|
||||
"status"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "message_id",
|
||||
"value": "1852"
|
||||
},
|
||||
{
|
||||
"key": "unique_id",
|
||||
"value": "webengage-message-id",
|
||||
"disabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"_postman_previewlanguage": "json",
|
||||
"header": [
|
||||
{
|
||||
"key": "Server",
|
||||
"value": "nginx"
|
||||
},
|
||||
{
|
||||
"key": "Date",
|
||||
"value": "Sun, 09 Nov 2025 16:20:37 GMT"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"description": "",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Length",
|
||||
"value": "38"
|
||||
},
|
||||
{
|
||||
"key": "Connection",
|
||||
"value": "keep-alive"
|
||||
}
|
||||
],
|
||||
"cookie": [],
|
||||
"body": "{\n \"id\": 274,\n \"status\": \"2\", // 0: Pending, 1: Sent, 2: Delivered\n \"unique_id\": \"\"\n}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Webengage",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "X-API-Key",
|
||||
"value": "{{apiKey}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"version\": \"2.0\",\n \"smsData\": {\n \"toNumber\": \"989195712939\",\n \"fromNumber\": \"9890006950\",\n \"body\": \"Text message body\"\n },\n \"metadata\": {\n \"campaignType\": \"PROMOTIONAL\",\n \"timestamp\": \"2018-01-25T10:24:16+0000\",\n \"messageId\": \"webengage-message-id222\",\n \"custom\": {\n \"key1\": \"val1\",\n \"key2\": \"val2\"\n },\n \"indiaDLT\": {\n \"contentTemplateId\": \"xyz\",\n \"principalEntityId\": \"abc\",\n \"telemarketerId\": \"tm1,tm2\"\n }\n }\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseURL}}/v1/integration/webengage",
|
||||
"host": [
|
||||
"{{baseURL}}"
|
||||
],
|
||||
"path": [
|
||||
"v1",
|
||||
"integration",
|
||||
"webengage"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Send Bulk",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "X-API-Key",
|
||||
"value": "{{apiKey}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"from\": \"989000xxxx\",\n \"on_duplicate\": \"skip\", // Skip to send only unique ones, reject to stop sending any message even if one is not unique\n \"messages\": [\n {\n \"to\": \"989126880345\",\n \"body\": \"Your message\",\n \"unique_id\": \"1\" // UDH: Optional\n },\n {\n \"to\": \"989127865454\",\n \"body\": \"Your message 2\",\n \"unique_id\": \"20\" // UDH: Optional\n }\n ]\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseURL}}/v1/send/bulk",
|
||||
"host": [
|
||||
"{{baseURL}}"
|
||||
],
|
||||
"path": [
|
||||
"v1",
|
||||
"send",
|
||||
"bulk"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Account Balance",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "X-API-Key",
|
||||
"value": "{{apiKey}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseURL}}/v1/account/balance",
|
||||
"host": [
|
||||
"{{baseURL}}"
|
||||
],
|
||||
"path": [
|
||||
"v1",
|
||||
"account",
|
||||
"balance"
|
||||
],
|
||||
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Send vOTP",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "x-api-key",
|
||||
"value": "{{apiKey}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "content-type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n\t\"from\": \"9890002999\",\n\t\"to\": \"989126880345\",\n\t\"code\": \"123456\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseURL}}/v1/send/votp",
|
||||
"host": [
|
||||
"{{baseURL}}"
|
||||
],
|
||||
"path": [
|
||||
"v1",
|
||||
"send",
|
||||
"votp"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
],
|
||||
"variable": [
|
||||
{
|
||||
"key": "baseURL",
|
||||
"value": "",
|
||||
"type": "default"
|
||||
},
|
||||
{
|
||||
"key": "apiKey",
|
||||
"value": "",
|
||||
"type": "default"
|
||||
}
|
||||
]
|
||||
}
|
||||
54
utils/clients/sms/payam_sms/client.py
Normal file
54
utils/clients/sms/payam_sms/client.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# TODO: this is time fourced code. refactor
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
import logging
|
||||
|
||||
from utils.clients.sms.sms import BaseSMSClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class PayamSMSClient(BaseSMSClient):
|
||||
def get_access_token(self):
|
||||
payam_sms_system_name = settings.PAYAM_SMS_SYSTEM_NAME
|
||||
payam_sms_username = settings.PAYAM_SMS_USERNAME
|
||||
payam_sms_password = settings.PAYAM_SMS_PASSWORD
|
||||
payam_sms_client_id = settings.PAYAM_SMS_CLIENT_ID
|
||||
payam_sms_client_secret = settings.PAYAM_SMS_CLIENT_SECRET
|
||||
auth = (payam_sms_client_id, payam_sms_client_secret)
|
||||
url = f"https://www.payamsms.com/auth/oauth/token?systemName={payam_sms_system_name}&username={payam_sms_username}&password={payam_sms_password}&scope=webservice&grant_type=password"
|
||||
|
||||
response_object = requests.post(url, auth=auth, json={})
|
||||
response = response_object.json()
|
||||
return response["access_token"]
|
||||
|
||||
|
||||
def send_sms(self, phone_number, message):
|
||||
try:
|
||||
url = "https://www.payamsms.com/panel/webservice/send"
|
||||
|
||||
# access_token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJzb2JhbiIsInNjb3BlIjpbIndlYnNlcnZpY2UiXSwiZXhwIjoxNzQ0MTAyMDU5LCJ1c2VyRGV0YWlscyI6eyJpZCI6MjU0MCwidXNlck5hbWUiOiJzb2JhbiIsInJvbGUiOiJVU0VSIiwiZW5hYmxlZCI6dHJ1ZSwic3lzdGVtSWQiOjcyMiwiYWNjZXNzIjp7InJvbGUiOlsiYWRkIiwiZWRpdCIsImdldCIsImRlbGV0ZSJdLCJwaG9uZUJvb2siOlsiYWRkIiwiZWRpdCIsImdldExvY2FsIiwiZ2V0R2xvYmFsIiwiZGVsZXRlIl0sInNtcyI6WyJzaW5nbGUiLCJnZXRMb2NhbCIsImdldEdsb2JhbCIsImJ1bGsiLCJkYiJdLCJyZXBvcnQiOlsiZ2V0TW9HbG9iYWwiLCJnZXREYWlseUxvY2FsIiwiZ2V0TW9Mb2NhbCIsImdldERhaWx5R2xvYmFsIl0sIndzIjpbIndzIl0sInVzZXIiOlsiYWRkIiwiZWRpdCIsImdldExvY2FsIiwiZ2V0IiwiZ2V0R2xvYmFsIiwiZGVsZXRlIl0sInRvb2xzIjpbInJhY2UiLCJwZXJpb2RpYyIsInBvbGxpbmciLCJzZWNyZXRvcnkiLCJhdXRvYW5zd2VyIl0sImZpbmFuY2UiOlsidGRyIiwiY2hhcmdlIl19LCJyb2xlSWQiOjQ0NDAsImNoaWxkcmVuSWRzIjpbXSwic3lzdGVtVHlwZSI6Ik5PUk1BTCIsInN5c3RlbVBheW1lbnRUeXBlIjoiREVCSVQiLCJzeXN0ZW1OYW1lIjoic29iYW4iLCJzZW5kV2l0aE91dFBlcm1pc3Npb25TeXN0ZW0iOmZhbHNlLCJtaXNDdXN0b21lciI6Itio2YbYr9in2LEg2LPZiNio2KfZhiDYs9in2YXYp9mG2YciLCJwYXJlbnRVc2VyTmFtZSI6bnVsbCwicGFyZW50SWQiOm51bGwsImFkbWluIjp0cnVlfSwiYXV0aG9yaXRpZXMiOlsiVVNFUiJdLCJqdGkiOiJNeU5vTml2RURmc3YxUDR3U20tbTR2Y2NHODgiLCJjbGllbnRfaWQiOiJzb2JhbiJ9.9mdneDduK4OVH2zsXvRqvXt2qwpLvs0vCuseLaB-LCo'
|
||||
access_token = self.get_access_token()
|
||||
|
||||
body = [
|
||||
{
|
||||
"sender": "98200000443295",
|
||||
"recipient": phone_number,
|
||||
"body": message,
|
||||
# "customerId": "1",
|
||||
# "sendDate": "2024-03-04 10:17:00"
|
||||
}
|
||||
]
|
||||
|
||||
headers = {
|
||||
'Authorization': f'Bearer {access_token}',
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
|
||||
response_object = requests.post(url, headers=headers, json=body, timeout=10)
|
||||
logger.info(response_object.text)
|
||||
return response_object
|
||||
except Exception as e:
|
||||
logger.exception(f'error sending sms to {phone_number}: {e}')
|
||||
|
||||
3
utils/clients/sms/sms.py
Normal file
3
utils/clients/sms/sms.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
class BaseSMSClient:
|
||||
def send_sms(self, phone_number, message):
|
||||
raise NotImplementedError()
|
||||
Loading…
Add table
Reference in a new issue