start auth apis and packages, redis, celery and run with docker

This commit is contained in:
behruz-dev
2025-07-14 18:09:23 +05:00
parent 2040e43585
commit dd56acf978
33 changed files with 492 additions and 21 deletions

13
config/celery.py Normal file
View File

@@ -0,0 +1,13 @@
import os
import celery
from config.env import env
os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE"))
app = celery.Celery("config")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()

7
config/conf/celery.py Normal file
View File

@@ -0,0 +1,7 @@
from django.conf import settings
from config.env import env
CELERY_BROKER_URL = env.str('REDIS_URL')
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_TIMEZONE = settings.TIME_ZONE

View File

@@ -0,0 +1,7 @@
SPECTACULAR_SETTINGS = {
"TITLE": "Your Project API",
"DESCRIPTION": "Your project description",
"VERSION": "1.0.0",
"SERVE_INCLUDE_SCHEMA": False,
"CAMELIZE_NAMES": True,
}

View File

@@ -0,0 +1,6 @@
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework_simplejwt.authentication.JWTAuthentication",),
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
}

36
config/conf/simplejwt.py Normal file
View File

@@ -0,0 +1,36 @@
from datetime import timedelta
from config.env import env
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=60),
"REFRESH_TOKEN_LIFETIME": timedelta(days=30),
"ROTATE_REFRESH_TOKENS": False,
"BLACKLIST_AFTER_ROTATION": False,
"UPDATE_LAST_LOGIN": False,
"ALGORITHM": "HS256",
"SIGNING_KEY": env("SECRET_KEY"),
"VERIFYING_KEY": "",
"AUDIENCE": None,
"ISSUER": None,
"JSON_ENCODER": None,
"JWK_URL": None,
"LEEWAY": 0,
"AUTH_HEADER_TYPES": ("Bearer",),
"AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",
"USER_ID_FIELD": "id",
"USER_ID_CLAIM": "user_id",
"AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
"TOKEN_TYPE_CLAIM": "token_type",
"TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser",
"JTI_CLAIM": "jti",
"SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
"SLIDING_TOKEN_LIFETIME": timedelta(minutes=60),
"SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=30),
"TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainPairSerializer",
"TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSerializer",
"TOKEN_VERIFY_SERIALIZER": "rest_framework_simplejwt.serializers.TokenVerifySerializer",
"TOKEN_BLACKLIST_SERIALIZER": "rest_framework_simplejwt.serializers.TokenBlacklistSerializer",
"SLIDING_TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainSlidingSerializer",
"SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer",
}

View File

@@ -34,7 +34,9 @@ APPS = [
]
PACKAGES = [
'drf_spectacular',
'rest_framework',
'rest_framework_simplejwt',
]
INSTALLED_APPS = []
@@ -131,3 +133,10 @@ MEDIA_ROOT = BASE_DIR / 'resources/media/'
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
AUTH_USER_MODEL = 'accounts.User'
from config.conf.drf_spectacular import *
from config.conf.rest_framework import *
from config.conf.simplejwt import *
from config.conf.celery import *

View File

@@ -1,22 +1,18 @@
"""
URL configuration for config project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
from django.urls import path, include
from drf_spectacular.views import SpectacularSwaggerView, SpectacularAPIView
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/', include(
[
path('', include('core.apps.accounts.urls')),
]
)),
# swagger
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
path('swagger/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
]

View File

@@ -0,0 +1,2 @@
from .user import *
from .verification_code import *

View File

@@ -0,0 +1,45 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as CustomUserAdmin
from django.contrib.auth.models import Group
from django.utils.translation import gettext_lazy as _
from core.apps.accounts.models.user import User
admin.site.unregister(Group)
@admin.register(User)
class UserAdmin(CustomUserAdmin):
fieldsets = (
(None, {"fields": ("phone", "password")}),
(_("Personal info"), {"fields": ("first_name", "last_name", "email", 'role')}),
(
_("Permissions"),
{
"fields": (
"is_active",
"is_staff",
"is_superuser",
"groups",
"user_permissions",
),
},
),
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
)
add_fieldsets = (
(
None,
{
"classes": ("wide",),
"fields": ("phone", "usable_password", "password1", "password2"),
},
),
)
list_display = ("phone", "email", "first_name", "last_name", "is_staff")
list_filter = ("is_staff", "is_superuser", "is_active", "groups")
search_fields = ("phone", "first_name", "last_name", "email")
ordering = ("phone",)
filter_horizontal = (
"groups",
"user_permissions",
)

View File

@@ -0,0 +1,8 @@
from django.contrib import admin
from core.apps.accounts.models.verification_code import VerificationCode
@admin.register(VerificationCode)
class VerificationCodeAdmin(admin.ModelAdmin):
list_display = ['id', 'user', 'code', 'is_expired', 'is_verify']

View File

@@ -4,3 +4,6 @@ from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'core.apps.accounts'
def ready(self):
import core.apps.accounts.admins

View File

View File

@@ -0,0 +1,7 @@
from django.db import models
ROLE_CHOICES = (
('PP', 'physical person'),
('LP', 'legal person')
)

View File

@@ -19,3 +19,14 @@ class BaseUserManager(UserManager):
extra_fields.setdefault("is_staff", False)
extra_fields.setdefault("is_superuser", False)
return self._create_user(phone, password, **extra_fields)
def create_superuser(self, phone, password=None, **extra_fields):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
if extra_fields.get("is_staff") is not True:
raise ValueError("Superuser must have is_staff=True.")
if extra_fields.get("is_superuser") is not True:
raise ValueError("Superuser must have is_superuser=True.")
return self._create_user(phone, password, **extra_fields)

View File

@@ -0,0 +1,50 @@
# Generated by Django 5.2 on 2025-07-14 15:16
import core.apps.accounts.managers.user
import django.core.validators
import django.utils.timezone
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('phone', models.CharField(max_length=13, unique=True, validators=[django.core.validators.RegexValidator(message="Telefon raqam formatda bo'lishi kerak: +998XXXXXXXXX", regex='^\\+998\\d{9}$')])),
('role', models.CharField(choices=[('PP', 'physical person'), ('LP', 'legal person')], max_length=2)),
('indentification_num', models.CharField(blank=True, max_length=14, null=True)),
('profile_image', models.ImageField(blank=True, null=True, upload_to='users/profile_image/%Y/%m/')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'db_table': 'users',
},
managers=[
('objects', core.apps.accounts.managers.user.BaseUserManager()),
],
),
]

View File

@@ -0,0 +1,33 @@
# Generated by Django 5.2 on 2025-07-14 17:08
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='VerificationCode',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('code', models.PositiveIntegerField()),
('is_expired', models.BooleanField(default=False)),
('is_verify', models.BooleanField(default=False)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='verification_codes', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Verification Code',
'verbose_name_plural': 'Verification Codes',
'db_table': 'verification_codes',
},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2 on 2025-07-14 17:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0002_verificationcode'),
]
operations = [
migrations.AddField(
model_name='verificationcode',
name='expiration_time',
field=models.TimeField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,2 @@
from .user import *
from .verification_code import *

View File

@@ -0,0 +1,41 @@
import random, json, datetime
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.utils import timezone
from core.apps.accounts.managers.user import BaseUserManager
from core.apps.accounts.enums.user import ROLE_CHOICES
from core.apps.accounts.validators.user import phone_regex
from core.apps.accounts.models.verification_code import VerificationCode
from core.apps.shared.models.base import BaseModel
class User(BaseModel, AbstractUser):
phone = models.CharField(max_length=13, validators=[phone_regex], unique=True)
role = models.CharField(max_length=2, choices=ROLE_CHOICES)
indentification_num = models.CharField(max_length=14, null=True, blank=True)
profile_image = models.ImageField(upload_to='users/profile_image/%Y/%m/', null=True, blank=True)
objects = BaseUserManager()
username = None
USERNAME_FIELD = 'phone'
REQUIRED_FIELDS = []
def __str__(self):
return self.phone
def generate_code(self):
code = ''.join([str(random.randint(0, 100) % 10) for _ in range(4)])
expiration_time = timezone.now() + datetime.timedelta(minutes=2)
VerificationCode.objects.create(
code=code,
user=self,
expiration_time=expiration_time,
)
return code
class Meta:
verbose_name = 'user'
verbose_name_plural = 'users'
db_table = 'users'

View File

@@ -0,0 +1,18 @@
from django.db import models
from core.apps.shared.models.base import BaseModel
class VerificationCode(BaseModel):
code = models.PositiveIntegerField()
user = models.ForeignKey('User', on_delete=models.CASCADE, related_name='verification_codes')
is_expired = models.BooleanField(default=False)
is_verify = models.BooleanField(default=False)
expiration_time = models.TimeField(null=True, blank=True)
def __str__(self):
return f'{self.user.phone} - {self.code}'
class Meta:
verbose_name = 'Verification Code'
verbose_name_plural = 'Verification Codes'
db_table = 'verification_codes'

View File

@@ -0,0 +1,42 @@
from django.db import transaction
from django.contrib.auth import get_user_model
from rest_framework import serializers
User = get_user_model()
class LoginSerializer(serializers.Serializer):
phone = serializers.CharField()
password = serializers.CharField()
def validate(self, data):
try:
user = User.objects.get(phone=data.get('phone'))
except User.DoesNotExist:
raise serializers.ValidationError({'detail': 'User not found'})
else:
if not user.check_password(data.get('password')):
raise serializers.ValidationError({'detail': 'User not found'})
data['user'] = user
return data
class RegisterSerializer(serializers.Serializer):
phone = serializers.CharField()
password = serializers.CharField()
def validate(self, data):
if User.objects.filter(phone=data.get('phone')).exists():
raise serializers.ValidationError({'detail': "User with this phone number already exists"})
return data
def create(self, validated_data):
with transaction.atomic():
new_user = User.objects.create_user(
phone=validated_data.pop('phone'),
)
new_user.set_password(validated_data.pop('password'))
new_user.save()
return new_user

View File

View File

@@ -0,0 +1,9 @@
from celery import shared_task
from core.apps.accounts.models.verification_code import VerificationCode
from core.services.sms import send_sms_eskiz
@shared_task
def create_and_send_sms_code(user):
code = user.generate_code()
send_sms_eskiz(user.phone, code)

View File

@@ -1 +1,12 @@
from django.urls import path
from django.urls import path, include
from core.apps.accounts.views.auth import LoginApiView, RegisterApiView
urlpatterns = [
path('auth/', include(
[
path('login/', LoginApiView.as_view(), name='login'),
path('register/', RegisterApiView.as_view(), name='login'),
]
))
]

View File

@@ -0,0 +1,6 @@
from django.core.validators import RegexValidator
phone_regex = RegexValidator(
regex=r'^\+998\d{9}$',
message="Telefon raqam formatda bo'lishi kerak: +998XXXXXXXXX"
)

View File

@@ -0,0 +1,32 @@
from django.contrib.auth import get_user_model
from rest_framework import generics, status, views
from rest_framework.response import Response
from rest_framework_simplejwt.tokens import RefreshToken
from drf_spectacular.utils import extend_schema
from core.apps.accounts.serializers import auth as auth_serializer
User = get_user_model()
@extend_schema(tags=['auth'])
class LoginApiView(generics.GenericAPIView):
serializer_class = auth_serializer.LoginSerializer
queryset = User.objects.all()
permission_classes = []
def post(self, request):
serializer = self.serializer_class(data=request.data)
if serializer.is_valid(raise_exception=True):
user = serializer.validated_data.get('user')
tokens = RefreshToken.for_user(user)
return Response({'access_token': str(tokens.access_token), 'refresh_token': str(tokens), 'role': user.role}, status=status.HTTP_200_OK)
@extend_schema(tags=['auth'])
class RegisterApiView(generics.CreateAPIView):
serializer_class = auth_serializer.RegisterSerializer
queryset = User.objects.all()
permission_classes = []

View File

@@ -0,0 +1 @@
from .base import *

View File

@@ -0,0 +1,12 @@
import uuid
from django.db import models
class BaseModel(models.Model):
id = models.UUIDField(editable=False, primary_key=True, unique=True, default=uuid.uuid4)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True

View File

22
core/services/sms.py Normal file
View File

@@ -0,0 +1,22 @@
from config.env import env
import requests
def send_sms_eskiz(phone, code):
login_url = "https://notify.eskiz.uz/api/auth/login"
token_res = requests.post(login_url, json={
"email": env("ESKIZ_EMAIL"),
"password": env("ESKIZ_PASSWORD")
})
token = token_res.json()['data']['token']
sms_url = "https://notify.eskiz.uz/api/message/sms/send"
headers = {"Authorization": f"Bearer {token}"}
data = {
"mobile_phone": phone,
"message": f"Sizning tasdiqlash kodingiz: {code}",
"from": "4546"
}
response = requests.post(sms_url, headers=headers, json=data)
return response.json()

View File

@@ -55,4 +55,20 @@ services:
networks:
- trustme
restart: always
image: redis
image: redis:latest
celery:
networks:
- trustme
build:
context: .
dockerfile: ./docker/Dockerfile.web
command: celery -A config worker --loglevel=info
volumes:
- .:/code
depends_on:
- redis
- web
environment:
CELERY_BROKER_URL: ${REDIS_URL}

View File

@@ -2,4 +2,12 @@ django==5.2
gunicorn
uvicorn
psycopg2
django-environ==0.12.0
django-environ==0.12.0
pillow
drf-spectacular==0.28.0
djangorestframework_simplejwt==5.5.0
djangorestframework
requests
celery==5.5.3
redis==6.2.0
django-redis==6.0.0