diff --git a/config/conf/jazzmin.py b/config/conf/jazzmin.py index 5903705..210b930 100644 --- a/config/conf/jazzmin.py +++ b/config/conf/jazzmin.py @@ -25,7 +25,7 @@ JAZZMIN_SETTINGS = { "show_sidebar": True, "navigation_expanded": True, "hide_apps": [], - "hide_models": ["auth.Group"], + "hide_models": ["auth.Group"], "order_with_respect_to": ["auth"], "custom_links": { diff --git a/config/settings/base.py b/config/settings/base.py index 21d101f..c0b8af4 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -136,5 +136,6 @@ MEDIA_ROOT = BASE_DIR / 'resources/media' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +AUTH_USER_MODEL = 'accounts.User' from config.conf import * diff --git a/core/apps/accounts/admin/__init__.py b/core/apps/accounts/admin/__init__.py index e69de29..2be3281 100644 --- a/core/apps/accounts/admin/__init__.py +++ b/core/apps/accounts/admin/__init__.py @@ -0,0 +1,3 @@ +from .user import * +from .permission import * +from .role import * \ No newline at end of file diff --git a/core/apps/accounts/admin/permission.py b/core/apps/accounts/admin/permission.py new file mode 100644 index 0000000..e9aedd3 --- /dev/null +++ b/core/apps/accounts/admin/permission.py @@ -0,0 +1,9 @@ +from django.contrib import admin + +from core.apps.accounts.models.permission import Permission + + +@admin.register(Permission) +class PermissionAdmin(admin.ModelAdmin): + list_display = ['name', 'code'] + diff --git a/core/apps/accounts/admin/role.py b/core/apps/accounts/admin/role.py new file mode 100644 index 0000000..8cb5972 --- /dev/null +++ b/core/apps/accounts/admin/role.py @@ -0,0 +1,9 @@ +from django.contrib import admin + +from core.apps.accounts.models.role import Role + + +@admin.register(Role) +class RoleAdmin(admin.ModelAdmin): + list_display = ['name'] + \ No newline at end of file diff --git a/core/apps/accounts/admin/user.py b/core/apps/accounts/admin/user.py new file mode 100644 index 0000000..ee451b5 --- /dev/null +++ b/core/apps/accounts/admin/user.py @@ -0,0 +1,28 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin +from django.utils.translation import gettext_lazy as _ + +from core.apps.accounts.models import User + +@admin.register(User) +class UserAdmin(DjangoUserAdmin): + add_form_template = "admin/auth/user/add_form.html" + change_user_password_template = None + fieldsets = ( + (None, {"fields": ("username", "password")}), + (_("Personal info"), {"fields": ("first_name", "last_name", "email", "role")}), + (_("Important dates"), {"fields": ("last_login", "date_joined")}), + ) + add_fieldsets = ( + ( + None, + { + "classes": ("wide",), + "fields": ("username", "password1", "password2"), + }, + ), + ) + list_display = ("username", "email", "first_name", "last_name", "is_staff") + list_filter = ("is_staff", "is_superuser", "is_active", "groups") + search_fields = ("username", "first_name", "last_name", "email") + ordering = ("username",) \ No newline at end of file diff --git a/core/apps/accounts/apps.py b/core/apps/accounts/apps.py index 197bf14..cbf167a 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): + from . import admin \ No newline at end of file diff --git a/core/apps/accounts/migrations/0001_initial.py b/core/apps/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..e58549b --- /dev/null +++ b/core/apps/accounts/migrations/0001_initial.py @@ -0,0 +1,47 @@ +# Generated by Django 5.2.4 on 2025-07-31 10:59 + +import django.contrib.auth.models +import django.contrib.auth.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')), + ('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')), + ('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)), + ('profile_image', models.ImageField(blank=True, null=True, upload_to='users/profile_images/', verbose_name='profil rasmi')), + ('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': 'Foydalanuvchi', + 'verbose_name_plural': 'Foydalanuvchilar', + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/core/apps/accounts/migrations/0002_permission_role_user_role.py b/core/apps/accounts/migrations/0002_permission_role_user_role.py new file mode 100644 index 0000000..d1cf6a1 --- /dev/null +++ b/core/apps/accounts/migrations/0002_permission_role_user_role.py @@ -0,0 +1,48 @@ +# Generated by Django 5.2.4 on 2025-07-31 16:31 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Permission', + 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)), + ('name', models.CharField(max_length=200)), + ('code', models.CharField(max_length=100, unique=True)), + ], + options={ + 'verbose_name': 'Ruxsatnoma', + 'verbose_name_plural': 'Ruxsatnomalar', + }, + ), + migrations.CreateModel( + name='Role', + 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)), + ('name', models.CharField(max_length=200, unique=True)), + ('permissions', models.ManyToManyField(related_name='roles', to='accounts.permission')), + ], + options={ + 'verbose_name': 'Rol', + 'verbose_name_plural': 'Rollar', + }, + ), + migrations.AddField( + model_name='user', + name='role', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='users', to='accounts.role'), + ), + ] diff --git a/core/apps/accounts/models/__init__.py b/core/apps/accounts/models/__init__.py index e69de29..b7bb9be 100644 --- a/core/apps/accounts/models/__init__.py +++ b/core/apps/accounts/models/__init__.py @@ -0,0 +1 @@ +from .user import User \ No newline at end of file diff --git a/core/apps/accounts/models/permission.py b/core/apps/accounts/models/permission.py new file mode 100644 index 0000000..306508e --- /dev/null +++ b/core/apps/accounts/models/permission.py @@ -0,0 +1,16 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from core.apps.shared.models import BaseModel + + +class Permission(BaseModel): + name = models.CharField(max_length=200) + code = models.CharField(max_length=100, unique=True) + + def __str__(self): + return f'{self.name} - {self.code}' + + class Meta: + verbose_name = _('Ruxsatnoma') + verbose_name_plural = _('Ruxsatnomalar') diff --git a/core/apps/accounts/models/role.py b/core/apps/accounts/models/role.py new file mode 100644 index 0000000..4181bf1 --- /dev/null +++ b/core/apps/accounts/models/role.py @@ -0,0 +1,17 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from core.apps.shared.models import BaseModel +from core.apps.accounts.models.permission import Permission + + +class Role(BaseModel): + name = models.CharField(max_length=200, unique=True) + permissions = models.ManyToManyField(Permission, related_name='roles') + + def __str__(self): + return self.name + + class Meta: + verbose_name = _('Rol') + verbose_name_plural = _('Rollar') \ 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..bf79d50 --- /dev/null +++ b/core/apps/accounts/models/user.py @@ -0,0 +1,22 @@ +from django.db import models +from django.contrib.auth.models import AbstractUser +from django.utils.translation import gettext_lazy as _ + +from core.apps.shared.models import BaseModel +from core.apps.accounts.models.role import Role + + +class User(BaseModel, AbstractUser): + profile_image = models.ImageField( + upload_to="users/profile_images/", null=True, blank=True, verbose_name=_('profil rasmi') + ) + role = models.ForeignKey(Role, on_delete=models.DO_NOTHING, null=True, related_name="users") + + REQUIRED_FIELDS = [] + + def __str__(self): + return self.username + + class Meta: + verbose_name = _("Foydalanuvchi") + verbose_name_plural = _("Foydalanuvchilar") diff --git a/core/apps/accounts/permissions/permissions.py b/core/apps/accounts/permissions/permissions.py new file mode 100644 index 0000000..436b26c --- /dev/null +++ b/core/apps/accounts/permissions/permissions.py @@ -0,0 +1,18 @@ +from rest_framework.permissions import BasePermission + +class HasRolePermission(BasePermission): + def has_permission(self, request, view): + user = request.user + + if not user.is_authenticated: + return False + + required_permissions = getattr(view, 'required_permissions', []) + if not required_permissions: + return True + + if user.role: + user_permissions = user.role.permissions.values_list('code', flat=True) + return all(perm in user_permissions for perm in required_permissions) + + return False \ No newline at end of file diff --git a/core/apps/accounts/serializers/login.py b/core/apps/accounts/serializers/login.py new file mode 100644 index 0000000..8a75919 --- /dev/null +++ b/core/apps/accounts/serializers/login.py @@ -0,0 +1,17 @@ +from rest_framework import serializers + +from core.apps.accounts.models.user 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: + raise serializers.ValidationError("User not found with this credentials") + if not user.check_password(data['password']): + raise serializers.ValidationError("User not found with this credentials") + data['user'] = user + return data \ No newline at end of file diff --git a/core/apps/accounts/urls.py b/core/apps/accounts/urls.py index 0834b38..56a2925 100644 --- a/core/apps/accounts/urls.py +++ b/core/apps/accounts/urls.py @@ -1,6 +1,8 @@ from django.urls import path, include +from core.apps.accounts.views.login import LoginApiView, TestApiView urlpatterns = [ - + path('auth/login/', LoginApiView.as_view(), name='login'), + path('test/', TestApiView.as_view()), ] \ No newline at end of file diff --git a/core/apps/accounts/views/login.py b/core/apps/accounts/views/login.py new file mode 100644 index 0000000..c063c7a --- /dev/null +++ b/core/apps/accounts/views/login.py @@ -0,0 +1,27 @@ +from rest_framework import generics, status +from rest_framework.response import Response + +from rest_framework_simplejwt.tokens import RefreshToken + +from core.apps.accounts.models.user import User +from core.apps.accounts.serializers.login import LoginSerializer +from core.apps.accounts.permissions.permissions import HasRolePermission + + +class LoginApiView(generics.GenericAPIView): + serializer_class = LoginSerializer + queryset = User.objects.all() + + def post(self, request): + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(raise_exception=True): + user = serializer.validated_data.get('user') + token = RefreshToken.for_user(user) + user_data = { + 'role': user.role.name, + 'permissions': user.role.permissions.values_list('code', flat=True), + } + return Response( + {"access": str(token.access_token), "refresh": str(token), 'user_data': user_data}, + status=status.HTTP_200_OK + ) diff --git a/core/apps/shared/models/__init__.py b/core/apps/shared/models/__init__.py index e69de29..97953db 100644 --- a/core/apps/shared/models/__init__.py +++ b/core/apps/shared/models/__init__.py @@ -0,0 +1 @@ +from .base import BaseModel \ 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..5057bde --- /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(primary_key=True, editable=False, unique=True, default=uuid.uuid4) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True diff --git a/docker-compose.yaml b/docker-compose.yaml index 3b670f6..82ba3d4 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -26,7 +26,6 @@ services: build: context: . dockerfile: ./docker/Dockerfile.web - restart: always command: ${COMMAND:-sh ./resources/scripts/entrypoint.sh} environment: - PYTHONPYCACHEPREFIX=/var/cache/pycache @@ -40,7 +39,6 @@ services: image: postgres:17 networks: - uyqur - restart: always environment: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_USER: ${POSTGRES_USER} @@ -51,6 +49,5 @@ services: redis: networks: - uyqur - restart: always image: redis diff --git a/requirements.txt b/requirements.txt index dec505c..0676245 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,11 @@ +amqp==5.3.1 +asgiref==3.9.1 +billiard==4.2.1 celery==5.5.3 click==8.2.1 +click-didyoumean==0.3.1 +click-plugins==1.1.1.2 +click-repl==0.3.0 Django==5.2.4 django-cacheops==7.2 django-environ==0.12.0 @@ -8,8 +14,24 @@ django-redis==6.0.0 djangorestframework==3.16.0 djangorestframework_simplejwt==5.5.1 drf-yasg==1.21.10 +funcy==2.0 gunicorn==23.0.0 +h11==0.16.0 +inflection==0.5.1 +kombu==5.5.4 +packaging==25.0 +pillow==11.3.0 +prompt_toolkit==3.0.51 psycopg2-binary==2.9.10 +PyJWT==2.10.1 +python-dateutil==2.9.0.post0 +pytz==2025.2 +PyYAML==6.0.2 redis==6.2.0 +six==1.17.0 +sqlparse==0.5.3 +tzdata==2025.2 +uritemplate==4.2.0 uvicorn==0.35.0 -pillow \ No newline at end of file +vine==5.1.0 +wcwidth==0.2.13 diff --git a/resources/scripts/entrypoint.sh b/resources/scripts/entrypoint.sh index f92abc5..03b69ef 100644 --- a/resources/scripts/entrypoint.sh +++ b/resources/scripts/entrypoint.sh @@ -2,6 +2,6 @@ python3 manage.py collectstatic --noinput python3 manage.py migrate --noinput -uvicorn config.asgi:application --host 0.0.0.0 --port 8000 --reload-dir core --reload-dir config +uvicorn config.asgi:application --host 0.0.0.0 --port 8000 --reload --reload-dir core --reload-dir config exit $? \ No newline at end of file