diff --git a/README.md b/README.md index 06c2699..c562850 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,4 @@ docker exec -it bash python manage.py createclient ``` -## SuperUser yaratish uchun -``` bash -python manage.py createuser -``` -- Schema name: -> client qoshishda kiritgan schema name. -- Username: -> login qilish uchun username. -- First name: -> Ism (shart emas). -- Last name: -> Familiya (shart emas). -- Phone number: -> Telefon raqam (shart emas). -- Password: -> login qilish uchun parol. \ No newline at end of file + diff --git a/config/settings/base.py b/config/settings/base.py index 075538e..2b8eaeb 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -10,26 +10,29 @@ env.read_env(BASE_DIR / '.env') SECRET_KEY = env.str('SECRET_KEY') DEBUG = env.bool('DEBUG') ALLOWED_HOSTS = env.list('ALLOWED_HOSTS') +ALLOWED_HOSTS = ["*"] # APPS SHARED_APPS = [ 'django_tenants', - 'jazzmin', 'core.apps.customers', + # django apps +] + + +TENANT_APPS = [ + # django apps 'django.contrib.contenttypes', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - # accounts + # local apps 'core.apps.accounts', -] - - -TENANT_APPS = [ 'core.apps.shared', + 'core.apps.products', ] PACKAGES = [ @@ -40,7 +43,7 @@ PACKAGES = [ ] -INSTALLED_APPS = SHARED_APPS + PACKAGES + TENANT_APPS +INSTALLED_APPS = SHARED_APPS + TENANT_APPS + PACKAGES # Middlewares MIDDLEWARE = [ diff --git a/config/urls.py b/config/urls.py index d38a628..15f5ee6 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,6 +1,6 @@ # django from django.contrib import admin -from django.urls import path +from django.urls import path, include from django.conf import settings from django.conf.urls.static import static @@ -34,5 +34,13 @@ urlpatterns += [ path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), ] +urlpatterns += [ + path('api/v1/', include( + [ + path('accounts/', include('core.apps.accounts.urls')), + ] + )), +] + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/core/apps/accounts/admin/user.py b/core/apps/accounts/admin/user.py index f90c7c2..1f59fc4 100644 --- a/core/apps/accounts/admin/user.py +++ b/core/apps/accounts/admin/user.py @@ -11,7 +11,7 @@ from core.apps.accounts.models.user import User class UserAdmin(DjangoUserAdmin): fieldsets = ( (None, {"fields": ("username", "password")}), - (("Personal info"), {"fields": ("first_name", "last_name", "email", "client", "phone_number", "profile_image")}), + (("Personal info"), {"fields": ("first_name", "last_name", "email", "phone_number", "profile_image")}), ( ("Permissions"), { @@ -29,11 +29,12 @@ class UserAdmin(DjangoUserAdmin): None, { "classes": ("wide",), - "fields": ("username", "password1", "password2", "client"), + "fields": ("username", "first_name", "last_name", "phone_number", "password1", "password2"), }, ), ) list_display = ("username", "phone_number", "first_name", "last_name", "is_staff") - list_filter = ("is_staff", "is_superuser", "is_active", "groups") + list_filter = ("is_staff", "is_superuser", "is_active") search_fields = ("username", "first_name", "last_name", "email") - ordering = ("username",) \ No newline at end of file + ordering = ("username",) + filter_horizontal = () \ No newline at end of file diff --git a/core/apps/accounts/management/commands/createuser.py b/core/apps/accounts/management/commands/createuser.py deleted file mode 100644 index e9f8fc9..0000000 --- a/core/apps/accounts/management/commands/createuser.py +++ /dev/null @@ -1,53 +0,0 @@ -# pypi -from getpass import getpass - -# django -from django.contrib.auth.management.commands.createsuperuser import Command as SuperUserCommand - -# django tenants -from django_tenants.utils import schema_context - -# accounts -from core.apps.accounts.models import User -# customers -from core.apps.customers.models import Client - - -class Command(SuperUserCommand): - def handle(self, *args, **options): - while True: - schema = input("Enter schema name: ") - - client = Client.objects.filter(schema_name=schema).first() - - if not client: - self.stdout.write(self.style.WARNING("Schema not found")) - else: - break - - while True: - username = input("Enter username: ") - if User.objects.filter(username=username).exists(): - self.stdout.write(self.style.WARNING("User already exists")) - else: - break - first_name = input("Enter first name: ") - last_name = input("Enter last name: ") - phone_number = input("Enter phone number: ") - - password = getpass("Enter password: ") - - User.objects.create_superuser( - password=password, - username=username, - client=client, - first_name=first_name, - last_name=last_name, - phone_number=phone_number, - ) - - self.stdout.write( - self.style.SUCCESS( - f"Superuser created successfully in schema '{schema}'" - ) - ) \ No newline at end of file diff --git a/core/apps/accounts/migrations/0001_initial.py b/core/apps/accounts/migrations/0001_initial.py index 3e8ec94..3226f8b 100644 --- a/core/apps/accounts/migrations/0001_initial.py +++ b/core/apps/accounts/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2 on 2025-12-05 11:43 +# Generated by Django 5.2 on 2025-12-07 13:08 import django.contrib.auth.models import django.contrib.auth.validators @@ -32,8 +32,9 @@ class Migration(migrations.Migration): ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), + ('is_deleted', models.BooleanField(default=False)), ('profile_image', models.ImageField(blank=True, null=True, upload_to='user/profile_images/')), - ('phone_number', models.CharField(blank=True, max_length=15, null=True, validators=[django.core.validators.RegexValidator(message='The phone_number is invalid. The format should be like this: 998XXXXXXXXX', regex='^998\\d{9}$')])), + ('phone_number', models.CharField(blank=True, max_length=15, null=True, validators=[django.core.validators.RegexValidator(message='The phone_number is invalid. The format should be like this: +998XXXXXXXXX', regex='^\\+998\\d{9}$')])), ('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')), ], diff --git a/core/apps/accounts/migrations/0002_user_client.py b/core/apps/accounts/migrations/0002_user_client.py deleted file mode 100644 index 735d337..0000000 --- a/core/apps/accounts/migrations/0002_user_client.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.2 on 2025-12-05 12:07 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0001_initial'), - ('customers', '0002_remove_client_on_trial_remove_client_paid_until'), - ] - - operations = [ - migrations.AddField( - model_name='user', - name='client', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='users', to='customers.client'), - ), - ] diff --git a/core/apps/accounts/models/user.py b/core/apps/accounts/models/user.py index 6db8f69..7bb00d5 100644 --- a/core/apps/accounts/models/user.py +++ b/core/apps/accounts/models/user.py @@ -21,14 +21,10 @@ class User(AbstractUser, BaseModel): phone_number = models.CharField( max_length=15, null=True, blank=True, validators=[uz_phone_validator] ) - client = models.ForeignKey( - Client, on_delete=models.CASCADE, related_name='users', null=True, - ) def __str__(self): return f"#{self.id}: {self.first_name} {self.last_name}" - @property def get_jwt_token(self): token = RefreshToken.for_user(self) return { diff --git a/core/apps/accounts/management/__init__.py b/core/apps/accounts/serializers/auth/__init__.py similarity index 100% rename from core/apps/accounts/management/__init__.py rename to core/apps/accounts/serializers/auth/__init__.py diff --git a/core/apps/accounts/serializers/auth/login.py b/core/apps/accounts/serializers/auth/login.py new file mode 100644 index 0000000..58e8a03 --- /dev/null +++ b/core/apps/accounts/serializers/auth/login.py @@ -0,0 +1,18 @@ +# rest framework +from rest_framework import serializers + + +# accounts +from core.apps.accounts.models import User + + +class LoginSerializer(serializers.Serializer): + username = serializers.CharField() + password = serializers.CharField() + + def validate(self, data): + user = User.objects.filter(username=data['username']).first() + if not user or (user and not user.check_password(data['password'])): + raise serializers.ValidationError({"user": "Username yoki parol noto'g'ri"}) + data['user'] = user + return data \ No newline at end of file diff --git a/core/apps/accounts/serializers/user/__init__.py b/core/apps/accounts/serializers/user/__init__.py new file mode 100644 index 0000000..82da278 --- /dev/null +++ b/core/apps/accounts/serializers/user/__init__.py @@ -0,0 +1 @@ +from .user import * \ No newline at end of file diff --git a/core/apps/accounts/serializers/user/user.py b/core/apps/accounts/serializers/user/user.py new file mode 100644 index 0000000..6ac0580 --- /dev/null +++ b/core/apps/accounts/serializers/user/user.py @@ -0,0 +1,22 @@ +# rest framework +from rest_framework import serializers + + +# accounts +from core.apps.accounts.models import User + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = [ + 'id', + 'first_name', + 'last_name', + 'username', + 'phone_number', + 'profile_image', + 'created_at', + 'updated_at', + ] + \ No newline at end of file diff --git a/core/apps/accounts/urls.py b/core/apps/accounts/urls.py index e69de29..be55e8f 100644 --- a/core/apps/accounts/urls.py +++ b/core/apps/accounts/urls.py @@ -0,0 +1,33 @@ +# django +from django.urls import path, include + +# rest framework +from rest_framework.routers import DefaultRouter + + +# accounts +# ------- user ------ +from core.apps.accounts.views.user import UserViewSet +# ------- auth ------ +from core.apps.accounts.views.auth.login import LoginApiView + + +urlpatterns = [ + path('user/', include( + [ + + ] + )), + # ------ authentication ------ + path('auth/', include( + [ + path('login/', LoginApiView.as_view(), name='login'), + ] + )), +] + +router = DefaultRouter() +router.register("user", UserViewSet) + + +urlpatterns += router.urls diff --git a/core/apps/accounts/views/auth/__init__.py b/core/apps/accounts/views/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/accounts/views/auth/login.py b/core/apps/accounts/views/auth/login.py new file mode 100644 index 0000000..f7a3230 --- /dev/null +++ b/core/apps/accounts/views/auth/login.py @@ -0,0 +1,75 @@ +# rest framework +from rest_framework import generics + +# drf yasg +from drf_yasg import openapi +from drf_yasg.utils import swagger_auto_schema + + +# accounts +from core.apps.accounts.models import User +from core.apps.accounts.serializers.auth import login as serializers +from core.apps.accounts.serializers.user import UserSerializer + +# utils +from core.utils.response.mixin import ResponseMixin + + +class LoginApiView(generics.GenericAPIView, ResponseMixin): + serializer_class = serializers.LoginSerializer + queryset = User.objects.all() + + @swagger_auto_schema( + tags=["Authentication and Authorization"], + responses={ + 200: openapi.Response( + description="Success", + schema=None, + examples={ + "application/json": { + "status_code": 200, + "status": "success", + "message": "Login muvaffaqiyatli amalga oshirildi", + "data": { + "user": { + "id": 0, + "first_name": "string", + "last_name": "string", + "username": "string", + "phone_number": "string", + "profile_image": "string", + "created_at": "string", + "updated_at": "string", + }, + "tokens": { + "access_token": "string", + "refresh_token": "string", + } + } + } + } + ) + } + ) + def post(self, request): + try: + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(): + user = serializer.validated_data.get('user') + token = user.get_jwt_token() + data = { + "user": UserSerializer(user).data, + "tokens": token, + } + return self.success_response( + data=data, + message="Login muvaffaqiyatli amalga oshirildi" + ) + return self.failure_response( + data=serializer.errors, + message="Kiritayotgan malumotingizni tekshirib ko'ring" + ) + except Exception as e: + return self.error_response( + data=str(e) + ) diff --git a/core/apps/accounts/views/user/__init__.py b/core/apps/accounts/views/user/__init__.py new file mode 100644 index 0000000..82da278 --- /dev/null +++ b/core/apps/accounts/views/user/__init__.py @@ -0,0 +1 @@ +from .user import * \ No newline at end of file diff --git a/core/apps/accounts/views/user/user.py b/core/apps/accounts/views/user/user.py new file mode 100644 index 0000000..139268f --- /dev/null +++ b/core/apps/accounts/views/user/user.py @@ -0,0 +1,45 @@ +# rest framework +from rest_framework import viewsets +from rest_framework.decorators import action + +# drf yasg +from drf_yasg import openapi +from drf_yasg.utils import swagger_auto_schema + + +# accounts +from core.apps.accounts.models import User +from core.apps.accounts.serializers.user import user as serializers + +# utils +from core.utils.response.mixin import ResponseMixin +from core.utils.permissions.tenant_user import IsTenantUser + + +class UserViewSet(viewsets.GenericViewSet, ResponseMixin): + queryset = User.objects.all() + permission_classes = [IsTenantUser] + + def get_serializer_class(self): + match self.action: + case "POST": + return + case ["PUT", "PATCH"]: + return + case _: + return serializers.UserSerializer + + @action( + methods=["GET"], url_name="me", url_path="me", detail=False + ) + def me(self, request): + try: + serializer = self.get_serializer(request.user) + return self.success_response( + data=serializer.data, + message="User ma'lumotlari" + ) + except Exception as e: + return self.error_response( + data=str(e), + ) diff --git a/core/apps/customers/admin/client.py b/core/apps/customers/admin/client.py index 95da56e..b65c2ed 100644 --- a/core/apps/customers/admin/client.py +++ b/core/apps/customers/admin/client.py @@ -1,11 +1,16 @@ +# django from django.contrib import admin +# django tenants +from django_tenants.admin import TenantAdminMixin + +# curstomers from core.apps.customers.models import Client from core.apps.customers.admin.domain import DomainInline @admin.register(Client) -class ClientAdmin(admin.ModelAdmin): +class ClientAdmin(TenantAdminMixin, admin.ModelAdmin): list_display = ['id', 'name', 'schema_name'] search_fields = ['name'] inlines = [DomainInline] \ No newline at end of file diff --git a/core/apps/customers/admin/domain.py b/core/apps/customers/admin/domain.py index 48a3caf..91374ab 100644 --- a/core/apps/customers/admin/domain.py +++ b/core/apps/customers/admin/domain.py @@ -1,8 +1,16 @@ +# django from django.contrib import admin + +# customers from core.apps.customers.models import Domain class DomainInline(admin.TabularInline): model = Domain - extra = 0 \ No newline at end of file + extra = 0 + + +@admin.register(Domain) +class DomainAdmin(admin.ModelAdmin): + pass \ No newline at end of file diff --git a/core/apps/customers/migrations/0001_initial.py b/core/apps/customers/migrations/0001_initial.py index d28f3f4..57db465 100644 --- a/core/apps/customers/migrations/0001_initial.py +++ b/core/apps/customers/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2 on 2025-11-19 10:53 +# Generated by Django 5.2 on 2025-12-07 13:08 import django.db.models.deletion import django_tenants.postgresql_backend.base @@ -19,8 +19,6 @@ class Migration(migrations.Migration): ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('schema_name', models.CharField(db_index=True, max_length=63, unique=True, validators=[django_tenants.postgresql_backend.base._check_schema_name])), ('name', models.CharField(max_length=100)), - ('paid_until', models.DateField()), - ('on_trial', models.BooleanField()), ('created_at', models.DateField(auto_now_add=True)), ], options={ diff --git a/core/apps/customers/migrations/0002_remove_client_on_trial_remove_client_paid_until.py b/core/apps/customers/migrations/0002_remove_client_on_trial_remove_client_paid_until.py deleted file mode 100644 index 279dcce..0000000 --- a/core/apps/customers/migrations/0002_remove_client_on_trial_remove_client_paid_until.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 5.2 on 2025-11-19 10:57 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('customers', '0001_initial'), - ] - - operations = [ - migrations.RemoveField( - model_name='client', - name='on_trial', - ), - migrations.RemoveField( - model_name='client', - name='paid_until', - ), - ] diff --git a/core/apps/products/__init__.py b/core/apps/products/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/products/admin/__init__.py b/core/apps/products/admin/__init__.py new file mode 100644 index 0000000..610e401 --- /dev/null +++ b/core/apps/products/admin/__init__.py @@ -0,0 +1 @@ +from .product import * \ No newline at end of file diff --git a/core/apps/products/admin/product.py b/core/apps/products/admin/product.py new file mode 100644 index 0000000..0615eee --- /dev/null +++ b/core/apps/products/admin/product.py @@ -0,0 +1,9 @@ +# django +from django.contrib import admin + + +# products +from core.apps.products.models import Product + + +admin.site.register(Product) \ No newline at end of file diff --git a/core/apps/products/apps.py b/core/apps/products/apps.py new file mode 100644 index 0000000..8d6cf4c --- /dev/null +++ b/core/apps/products/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class ProductsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'core.apps.products' + + def ready(self): + import core.apps.products.admin \ No newline at end of file diff --git a/core/apps/products/migrations/0001_initial.py b/core/apps/products/migrations/0001_initial.py new file mode 100644 index 0000000..90b05a1 --- /dev/null +++ b/core/apps/products/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 5.2 on 2025-12-07 13:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('is_deleted', models.BooleanField(default=False)), + ('name', models.CharField(max_length=200)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/core/apps/products/migrations/__init__.py b/core/apps/products/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/products/models/__init__.py b/core/apps/products/models/__init__.py new file mode 100644 index 0000000..610e401 --- /dev/null +++ b/core/apps/products/models/__init__.py @@ -0,0 +1 @@ +from .product import * \ No newline at end of file diff --git a/core/apps/products/models/product.py b/core/apps/products/models/product.py new file mode 100644 index 0000000..c7ec685 --- /dev/null +++ b/core/apps/products/models/product.py @@ -0,0 +1,14 @@ +# django +from django.db import models + + +# shared +from core.apps.shared.models import BaseModel + + +class Product(BaseModel): + name = models.CharField(max_length=200) + + def __str__(self): + return self.name + diff --git a/core/apps/products/urls.py b/core/apps/products/urls.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/shared/models/base.py b/core/apps/shared/models/base.py index 342a4e2..2edde61 100644 --- a/core/apps/shared/models/base.py +++ b/core/apps/shared/models/base.py @@ -4,6 +4,7 @@ from django.db import models class BaseModel(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + is_deleted = models.BooleanField(default=False) class Meta: abstract = True \ No newline at end of file diff --git a/core/utils/permissions/tenant_user.py b/core/utils/permissions/tenant_user.py new file mode 100644 index 0000000..aacad6c --- /dev/null +++ b/core/utils/permissions/tenant_user.py @@ -0,0 +1,12 @@ +# rest framework +from rest_framework.permissions import BasePermission + + +class IsTenantUser(BasePermission): + """ + Allow access only if request.tenant_user exists. + """ + + def has_permission(self, request, view): + + return bool(request.tenant_user) \ No newline at end of file diff --git a/core/utils/response/__int__.py b/core/utils/response/__int__.py index 3c8fe36..0abc44d 100644 --- a/core/utils/response/__int__.py +++ b/core/utils/response/__int__.py @@ -1 +1 @@ -from .mixin import ResponseMixin \ No newline at end of file +from .mixin import * \ No newline at end of file diff --git a/core/utils/response/mixin.py b/core/utils/response/mixin.py index f11e10d..71e4511 100644 --- a/core/utils/response/mixin.py +++ b/core/utils/response/mixin.py @@ -31,7 +31,7 @@ class ResponseMixin: return Response(response_data, status=response_data["status_code"]) @classmethod - def failure_response(cls, data=None, message=None): + def failure_response(cls, data=None): """ Docstring for failure_response @@ -43,8 +43,7 @@ class ResponseMixin: "status_code": status.HTTP_400_BAD_REQUEST, "status": cls.FAILURE } - if message is not None: - response_data["message"] = message + response_data["message"] = "Kiritayotgan malumotingizni tekshirib ko'ring" if data is not None: response_data["data"] = data return @@ -105,7 +104,7 @@ class ResponseMixin: return Response(response_data, status=response_data['status_code']) @classmethod - def error_response(cls, data=None, message=None): + def error_response(cls, data=None): """ Docstring for error_response @@ -117,8 +116,9 @@ class ResponseMixin: "status_code": status.HTTP_500_INTERNAL_SERVER_ERROR, "status": cls.ERROR } - if message is not None: - response_data["message"] = message + response_data["message"] = "Xatolik, Iltimos backend dasturchiga murojaat qiling" + if data is not None: response_data["data"] = data + return Response(response_data, status=response_data["status_code"]) \ No newline at end of file