diff --git a/config/celery.py b/config/celery.py new file mode 100644 index 0000000..edd621b --- /dev/null +++ b/config/celery.py @@ -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() \ No newline at end of file diff --git a/config/conf/celery.py b/config/conf/celery.py new file mode 100644 index 0000000..d11eea8 --- /dev/null +++ b/config/conf/celery.py @@ -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 diff --git a/config/conf/drf_spectacular.py b/config/conf/drf_spectacular.py new file mode 100644 index 0000000..24f1b26 --- /dev/null +++ b/config/conf/drf_spectacular.py @@ -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, +} \ No newline at end of file diff --git a/config/conf/rest_framework.py b/config/conf/rest_framework.py new file mode 100644 index 0000000..87d24a6 --- /dev/null +++ b/config/conf/rest_framework.py @@ -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"], +} \ No newline at end of file diff --git a/config/conf/simplejwt.py b/config/conf/simplejwt.py new file mode 100644 index 0000000..a02ea80 --- /dev/null +++ b/config/conf/simplejwt.py @@ -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", +} \ No newline at end of file diff --git a/config/settings/base.py b/config/settings/base.py index 6ccd804..c0ede8c 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -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 * diff --git a/config/urls.py b/config/urls.py index 35a0802..2fbd1d2 100644 --- a/config/urls.py +++ b/config/urls.py @@ -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'), ] diff --git a/core/apps/accounts/admins/__init__.py b/core/apps/accounts/admins/__init__.py index e69de29..bebeb5f 100644 --- a/core/apps/accounts/admins/__init__.py +++ b/core/apps/accounts/admins/__init__.py @@ -0,0 +1,2 @@ +from .user import * +from .verification_code import * \ No newline at end of file diff --git a/core/apps/accounts/admins/user.py b/core/apps/accounts/admins/user.py new file mode 100644 index 0000000..289d76c --- /dev/null +++ b/core/apps/accounts/admins/user.py @@ -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", + ) diff --git a/core/apps/accounts/admins/verification_code.py b/core/apps/accounts/admins/verification_code.py new file mode 100644 index 0000000..2cb42c8 --- /dev/null +++ b/core/apps/accounts/admins/verification_code.py @@ -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'] + \ No newline at end of file diff --git a/core/apps/accounts/apps.py b/core/apps/accounts/apps.py index 197bf14..477b173 100644 --- a/core/apps/accounts/apps.py +++ b/core/apps/accounts/apps.py @@ -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 \ No newline at end of file diff --git a/core/apps/accounts/enums/__init__.py b/core/apps/accounts/enums/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/accounts/enums/user.py b/core/apps/accounts/enums/user.py new file mode 100644 index 0000000..8f53b1b --- /dev/null +++ b/core/apps/accounts/enums/user.py @@ -0,0 +1,7 @@ +from django.db import models + + +ROLE_CHOICES = ( + ('PP', 'physical person'), + ('LP', 'legal person') +) \ No newline at end of file diff --git a/core/apps/accounts/managers/user.py b/core/apps/accounts/managers/user.py index b0ee275..1be2f64 100644 --- a/core/apps/accounts/managers/user.py +++ b/core/apps/accounts/managers/user.py @@ -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) diff --git a/core/apps/accounts/migrations/0001_initial.py b/core/apps/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..8756d88 --- /dev/null +++ b/core/apps/accounts/migrations/0001_initial.py @@ -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()), + ], + ), + ] diff --git a/core/apps/accounts/migrations/0002_verificationcode.py b/core/apps/accounts/migrations/0002_verificationcode.py new file mode 100644 index 0000000..abc22a9 --- /dev/null +++ b/core/apps/accounts/migrations/0002_verificationcode.py @@ -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', + }, + ), + ] diff --git a/core/apps/accounts/migrations/0003_verificationcode_expiration_time.py b/core/apps/accounts/migrations/0003_verificationcode_expiration_time.py new file mode 100644 index 0000000..247eb1d --- /dev/null +++ b/core/apps/accounts/migrations/0003_verificationcode_expiration_time.py @@ -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), + ), + ] diff --git a/core/apps/accounts/models/__init__.py b/core/apps/accounts/models/__init__.py index e69de29..bebeb5f 100644 --- a/core/apps/accounts/models/__init__.py +++ b/core/apps/accounts/models/__init__.py @@ -0,0 +1,2 @@ +from .user import * +from .verification_code import * \ No newline at end of file diff --git a/core/apps/accounts/models/user.py b/core/apps/accounts/models/user.py new file mode 100644 index 0000000..52ea28c --- /dev/null +++ b/core/apps/accounts/models/user.py @@ -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' \ No newline at end of file diff --git a/core/apps/accounts/models/verification_code.py b/core/apps/accounts/models/verification_code.py new file mode 100644 index 0000000..00b532d --- /dev/null +++ b/core/apps/accounts/models/verification_code.py @@ -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' \ No newline at end of file diff --git a/core/apps/accounts/serializers/auth.py b/core/apps/accounts/serializers/auth.py new file mode 100644 index 0000000..03a4899 --- /dev/null +++ b/core/apps/accounts/serializers/auth.py @@ -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 + + diff --git a/core/apps/accounts/tasks/__init__.py b/core/apps/accounts/tasks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/accounts/tasks/user.py b/core/apps/accounts/tasks/user.py new file mode 100644 index 0000000..c265ed1 --- /dev/null +++ b/core/apps/accounts/tasks/user.py @@ -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) \ No newline at end of file diff --git a/core/apps/accounts/urls.py b/core/apps/accounts/urls.py index 1500611..f57ccea 100644 --- a/core/apps/accounts/urls.py +++ b/core/apps/accounts/urls.py @@ -1 +1,12 @@ -from django.urls import path \ No newline at end of file +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'), + ] + )) +] \ No newline at end of file diff --git a/core/apps/accounts/validators/__init__.py b/core/apps/accounts/validators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/accounts/validators/user.py b/core/apps/accounts/validators/user.py new file mode 100644 index 0000000..8ef47d0 --- /dev/null +++ b/core/apps/accounts/validators/user.py @@ -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" +) \ No newline at end of file diff --git a/core/apps/accounts/views/auth.py b/core/apps/accounts/views/auth.py new file mode 100644 index 0000000..c13c6f9 --- /dev/null +++ b/core/apps/accounts/views/auth.py @@ -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 = [] \ No newline at end of file diff --git a/core/apps/shared/models/__init__.py b/core/apps/shared/models/__init__.py index e69de29..773cfc4 100644 --- a/core/apps/shared/models/__init__.py +++ b/core/apps/shared/models/__init__.py @@ -0,0 +1 @@ +from .base import * \ No newline at end of file diff --git a/core/apps/shared/models/base.py b/core/apps/shared/models/base.py new file mode 100644 index 0000000..45ce7a0 --- /dev/null +++ b/core/apps/shared/models/base.py @@ -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 \ No newline at end of file diff --git a/core/services/__init__.py b/core/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/services/sms.py b/core/services/sms.py new file mode 100644 index 0000000..d909251 --- /dev/null +++ b/core/services/sms.py @@ -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() diff --git a/docker-compose.yaml b/docker-compose.yaml index 1f515fb..3d737c9 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -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} + diff --git a/requirements.txt b/requirements.txt index 6b13ab1..79bf393 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,12 @@ django==5.2 gunicorn uvicorn psycopg2 -django-environ==0.12.0 \ No newline at end of file +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 \ No newline at end of file