From 8b832f8e154396e43113ecd603c300596b8b2e49 Mon Sep 17 00:00:00 2001 From: behruz-dev Date: Sun, 7 Dec 2025 18:10:04 +0500 Subject: [PATCH] shared_account app olib tashlandi --- README.md | 2 + config/settings/base.py | 12 +-- core/apps/accounts/migrations/0001_initial.py | 6 +- core/apps/accounts/models/user.py | 3 - .../serializers/auth}/__init__.py | 0 core/apps/accounts/serializers/auth/login.py | 18 +++++ .../serializers/user}/__init__.py | 0 core/apps/accounts/serializers/user/user.py | 22 ++++++ core/apps/accounts/urls.py | 22 ++++-- .../views/auth}/__init__.py | 0 core/apps/accounts/views/auth/login.py | 75 +++++++++++++++++++ .../views/user}/__init__.py | 0 core/apps/accounts/views/user/user.py | 45 +++++++++++ .../apps/customers/migrations/0001_initial.py | 2 +- core/apps/products/migrations/0001_initial.py | 3 +- core/apps/shared/models/base.py | 1 + core/apps/shared_accounts/admin/user.py | 39 ---------- core/apps/shared_accounts/apps.py | 9 --- .../migrations/0001_initial.py | 45 ----------- core/apps/shared_accounts/models/user.py | 16 ---- core/apps/shared_accounts/urls.py | 0 core/utils/permissions/tenant_user.py | 12 +++ core/utils/response/__int__.py | 2 +- core/utils/response/mixin.py | 12 +-- 24 files changed, 213 insertions(+), 133 deletions(-) rename core/apps/{shared_accounts => accounts/serializers/auth}/__init__.py (100%) create mode 100644 core/apps/accounts/serializers/auth/login.py rename core/apps/{shared_accounts/admin => accounts/serializers/user}/__init__.py (100%) create mode 100644 core/apps/accounts/serializers/user/user.py rename core/apps/{shared_accounts/migrations => accounts/views/auth}/__init__.py (100%) create mode 100644 core/apps/accounts/views/auth/login.py rename core/apps/{shared_accounts/models => accounts/views/user}/__init__.py (100%) create mode 100644 core/apps/accounts/views/user/user.py delete mode 100644 core/apps/shared_accounts/admin/user.py delete mode 100644 core/apps/shared_accounts/apps.py delete mode 100644 core/apps/shared_accounts/migrations/0001_initial.py delete mode 100644 core/apps/shared_accounts/models/user.py delete mode 100644 core/apps/shared_accounts/urls.py create mode 100644 core/utils/permissions/tenant_user.py diff --git a/README.md b/README.md index 3835db9..c562850 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,5 @@ docker exec -it bash python manage.py createclient ``` + + diff --git a/config/settings/base.py b/config/settings/base.py index aa754e9..2b8eaeb 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -18,6 +18,11 @@ SHARED_APPS = [ 'django_tenants', 'core.apps.customers', # django apps +] + + +TENANT_APPS = [ + # django apps 'django.contrib.contenttypes', 'django.contrib.admin', 'django.contrib.auth', @@ -25,11 +30,6 @@ SHARED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', # local apps - 'core.apps.shared_accounts', -] - - -TENANT_APPS = [ 'core.apps.accounts', 'core.apps.shared', 'core.apps.products', @@ -123,7 +123,7 @@ MEDIA_ROOT = BASE_DIR / 'resources/media' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -AUTH_USER_MODEL = 'shared_accounts.AdminUser' +AUTH_USER_MODEL = 'accounts.User' # Django tenants TENANT_MODEL = "customers.Client" diff --git a/core/apps/accounts/migrations/0001_initial.py b/core/apps/accounts/migrations/0001_initial.py index a063b04..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-07 09:47 +# Generated by Django 5.2 on 2025-12-07 13:08 import django.contrib.auth.models import django.contrib.auth.validators @@ -12,6 +12,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), ] operations = [ @@ -31,8 +32,11 @@ 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}$')])), + ('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', diff --git a/core/apps/accounts/models/user.py b/core/apps/accounts/models/user.py index 2875c89..7bb00d5 100644 --- a/core/apps/accounts/models/user.py +++ b/core/apps/accounts/models/user.py @@ -21,13 +21,10 @@ class User(AbstractUser, BaseModel): phone_number = models.CharField( max_length=15, null=True, blank=True, validators=[uz_phone_validator] ) - groups = None - user_permissions = None 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/shared_accounts/__init__.py b/core/apps/accounts/serializers/auth/__init__.py similarity index 100% rename from core/apps/shared_accounts/__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/shared_accounts/admin/__init__.py b/core/apps/accounts/serializers/user/__init__.py similarity index 100% rename from core/apps/shared_accounts/admin/__init__.py rename to core/apps/accounts/serializers/user/__init__.py 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 9aafacd..be55e8f 100644 --- a/core/apps/accounts/urls.py +++ b/core/apps/accounts/urls.py @@ -1,8 +1,15 @@ # django from django.urls import path, include -# rest framework simplejwt -from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView +# 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 = [ @@ -14,8 +21,13 @@ urlpatterns = [ # ------ authentication ------ path('auth/', include( [ - path('login/', TokenObtainPairView.as_view(), name='login-api'), - path('token/refresh/', TokenRefreshView.as_view(), name='token-refresh-api'), + path('login/', LoginApiView.as_view(), name='login'), ] )), -] \ No newline at end of file +] + +router = DefaultRouter() +router.register("user", UserViewSet) + + +urlpatterns += router.urls diff --git a/core/apps/shared_accounts/migrations/__init__.py b/core/apps/accounts/views/auth/__init__.py similarity index 100% rename from core/apps/shared_accounts/migrations/__init__.py rename to core/apps/accounts/views/auth/__init__.py 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/shared_accounts/models/__init__.py b/core/apps/accounts/views/user/__init__.py similarity index 100% rename from core/apps/shared_accounts/models/__init__.py rename to core/apps/accounts/views/user/__init__.py 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/migrations/0001_initial.py b/core/apps/customers/migrations/0001_initial.py index 50543de..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-12-07 09:47 +# Generated by Django 5.2 on 2025-12-07 13:08 import django.db.models.deletion import django_tenants.postgresql_backend.base diff --git a/core/apps/products/migrations/0001_initial.py b/core/apps/products/migrations/0001_initial.py index 67a61f0..90b05a1 100644 --- a/core/apps/products/migrations/0001_initial.py +++ b/core/apps/products/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2 on 2025-12-07 09:47 +# Generated by Django 5.2 on 2025-12-07 13:08 from django.db import migrations, models @@ -17,6 +17,7 @@ class Migration(migrations.Migration): ('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={ 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/apps/shared_accounts/admin/user.py b/core/apps/shared_accounts/admin/user.py deleted file mode 100644 index 210e8db..0000000 --- a/core/apps/shared_accounts/admin/user.py +++ /dev/null @@ -1,39 +0,0 @@ -# django -from django.contrib import admin -from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin - - -# shared_accounts -from core.apps.shared_accounts.models.user import AdminUser - - -@admin.register(AdminUser) -class UserAdmin(DjangoUserAdmin): - fieldsets = ( - (None, {"fields": ("username", "password")}), - (("Personal info"), {"fields": ("first_name", "last_name", "email")}), - ( - ("Permissions"), - { - "fields": ( - "is_active", - "is_staff", - "is_superuser", - ), - }, - ), - (("Important dates"), {"fields": ("last_login", "date_joined")}), - ) - add_fieldsets = ( - ( - None, - { - "classes": ("wide",), - "fields": ("username", "password1", "password2"), - }, - ), - ) - list_display = ("username", "first_name", "last_name", "is_staff") - list_filter = ("is_staff", "is_superuser", "is_active",) - search_fields = ("username", "first_name", "last_name", "email") - ordering = ("username",) \ No newline at end of file diff --git a/core/apps/shared_accounts/apps.py b/core/apps/shared_accounts/apps.py deleted file mode 100644 index a568547..0000000 --- a/core/apps/shared_accounts/apps.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.apps import AppConfig - - -class SharedAccountsConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'core.apps.shared_accounts' - - def ready(self): - import core.apps.shared_accounts.admin diff --git a/core/apps/shared_accounts/migrations/0001_initial.py b/core/apps/shared_accounts/migrations/0001_initial.py deleted file mode 100644 index f9c266f..0000000 --- a/core/apps/shared_accounts/migrations/0001_initial.py +++ /dev/null @@ -1,45 +0,0 @@ -# Generated by Django 5.2 on 2025-12-07 09:47 - -import django.contrib.auth.models -import django.contrib.auth.validators -import django.utils.timezone -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='AdminUser', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('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')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('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')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('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': 'Admin User', - 'verbose_name_plural': 'Admin Users', - }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], - ), - ] diff --git a/core/apps/shared_accounts/models/user.py b/core/apps/shared_accounts/models/user.py deleted file mode 100644 index 6d9bfe8..0000000 --- a/core/apps/shared_accounts/models/user.py +++ /dev/null @@ -1,16 +0,0 @@ -# django -from django.db import models -from django.contrib.auth.models import AbstractUser - - -# shared -from core.apps.shared.models import BaseModel - - -class AdminUser(AbstractUser, BaseModel): - def __str__(self): - return f"{self.first_name} {self.last_name}" - - class Meta: - verbose_name = "Admin User" - verbose_name_plural = "Admin Users" diff --git a/core/apps/shared_accounts/urls.py b/core/apps/shared_accounts/urls.py deleted file mode 100644 index e69de29..0000000 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