services papkasi o'chirildi

This commit is contained in:
A'zamov Samandar
2025-04-19 15:18:13 +05:00
parent 291f31b09b
commit eff4606079
124 changed files with 7 additions and 7 deletions

View File

View File

View File

@@ -0,0 +1,2 @@
from .core import * # noqa
from .user import * # noqa

View File

@@ -0,0 +1,18 @@
"""
Admin panel register
"""
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.contrib.auth import models as db_models
from django_core.models import SmsConfirm
from ..admin import user
from .user import SmsConfirmAdmin
admin.site.unregister(db_models.Group)
admin.site.register(db_models.Group, user.GroupAdmin)
admin.site.register(db_models.Permission, user.PermissionAdmin)
admin.site.register(get_user_model(), user.CustomUserAdmin)
admin.site.register(SmsConfirm, SmsConfirmAdmin)

View File

@@ -0,0 +1,52 @@
from django.contrib.auth import admin
from django.utils.translation import gettext_lazy as _
from unfold.admin import ModelAdmin
from unfold.forms import AdminPasswordChangeForm # UserCreationForm,
from unfold.forms import UserChangeForm
class CustomUserAdmin(admin.UserAdmin, ModelAdmin):
change_password_form = AdminPasswordChangeForm
# add_form = UserCreationForm
form = UserChangeForm
list_display = (
"first_name",
"last_name",
"phone",
"role",
)
autocomplete_fields = ["groups", "user_permissions"]
fieldsets = ((None, {"fields": ("phone",)}),) + (
(None, {"fields": ("username", "password")}),
(_("Personal info"), {"fields": ("first_name", "last_name", "email")}),
(
_("Permissions"),
{
"fields": (
"is_active",
"is_staff",
"is_superuser",
"groups",
"user_permissions",
"role",
),
},
),
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
)
class PermissionAdmin(ModelAdmin):
list_display = ("name",)
search_fields = ("name",)
class GroupAdmin(ModelAdmin):
list_display = ["name"]
search_fields = ["name"]
autocomplete_fields = ("permissions",)
class SmsConfirmAdmin(ModelAdmin):
list_display = ["phone", "code", "resend_count", "try_count"]
search_fields = ["phone", "code"]

View File

@@ -0,0 +1,9 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "core.apps.accounts"
def ready(self):
from core.apps.accounts import signals # noqa

View File

@@ -0,0 +1 @@
from .user import * # noqa

View File

@@ -0,0 +1,12 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
class RoleChoice(models.TextChoices):
"""
User Role Choice
"""
SUPERUSER = "superuser", _("Superuser")
ADMIN = "admin", _("Admin")
USER = "user", _("User")

View File

@@ -0,0 +1 @@
from .user import * # noqa

View File

@@ -0,0 +1,23 @@
from django.contrib.auth import base_user
class UserManager(base_user.BaseUserManager):
def create_user(self, phone, password=None, **extra_fields):
if not phone:
raise ValueError("The phone number must be set")
user = self.model(phone=phone, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
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,60 @@
# Generated by Django 5.1.3 on 2024-12-13 19:04
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
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=[
('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')),
('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')),
('phone', models.CharField(max_length=255, unique=True)),
('username', models.CharField(blank=True, max_length=255, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('validated_at', models.DateTimeField(blank=True, null=True)),
('role', models.CharField(choices=[('superuser', 'Superuser'), ('admin', 'Admin'), ('user', 'User')], default='user', max_length=255)),
('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',
'abstract': False,
},
),
migrations.CreateModel(
name='ResetToken',
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)),
('token', models.CharField(max_length=255, unique=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Reset Token',
'verbose_name_plural': 'Reset Tokens',
},
),
]

View File

@@ -0,0 +1,3 @@
# isort: skip_file
from .user import * # noqa
from .reset_token import * # noqa

View File

@@ -0,0 +1,15 @@
from django.contrib.auth import get_user_model
from django.db import models
from django_core.models import AbstractBaseModel
class ResetToken(AbstractBaseModel):
token = models.CharField(max_length=255, unique=True)
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
def __str__(self):
return self.token
class Meta:
verbose_name = "Reset Token"
verbose_name_plural = "Reset Tokens"

View File

@@ -0,0 +1,24 @@
from django.contrib.auth import models as auth_models
from django.db import models
from ..choices import RoleChoice
from ..managers import UserManager
class User(auth_models.AbstractUser):
phone = models.CharField(max_length=255, unique=True)
username = models.CharField(max_length=255, null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
validated_at = models.DateTimeField(null=True, blank=True)
role = models.CharField(
max_length=255,
choices=RoleChoice,
default=RoleChoice.USER,
)
USERNAME_FIELD = "phone"
objects = UserManager()
def __str__(self):
return self.phone

View File

@@ -0,0 +1 @@
from .core import * # noqa

View File

@@ -0,0 +1,10 @@
"""
Create a new user/superuser
"""
from django.contrib.auth import get_user_model
class UserSeeder:
def run(self):
get_user_model().objects.create_superuser("998888112309", "2309")

View File

@@ -0,0 +1,4 @@
from .auth import * # noqa
from .change_password import * # noqa
from .set_password import * # noqa
from .user import * # noqa

View File

@@ -0,0 +1,59 @@
from django.contrib.auth import get_user_model
from django.utils.translation import gettext as _
from rest_framework import exceptions, serializers
class LoginSerializer(serializers.Serializer):
username = serializers.CharField(max_length=255)
password = serializers.CharField(max_length=255)
class RegisterSerializer(serializers.ModelSerializer):
phone = serializers.CharField(max_length=255)
def validate_phone(self, value):
user = get_user_model().objects.filter(phone=value, validated_at__isnull=False)
if user.exists():
raise exceptions.ValidationError(_("Phone number already registered."), code="unique")
return value
class Meta:
model = get_user_model()
fields = ["first_name", "last_name", "phone", "password"]
extra_kwargs = {
"first_name": {
"required": True,
},
"last_name": {"required": True},
}
class ConfirmSerializer(serializers.Serializer):
code = serializers.IntegerField(min_value=1000, max_value=9999)
phone = serializers.CharField(max_length=255)
class ResetPasswordSerializer(serializers.Serializer):
phone = serializers.CharField(max_length=255)
def validate_phone(self, value):
user = get_user_model().objects.filter(phone=value)
if user.exists():
return value
raise serializers.ValidationError(_("User does not exist"))
class ResetConfirmationSerializer(serializers.Serializer):
code = serializers.IntegerField(min_value=1000, max_value=9999)
phone = serializers.CharField(max_length=255)
def validate_phone(self, value):
user = get_user_model().objects.filter(phone=value)
if user.exists():
return value
raise serializers.ValidationError(_("User does not exist"))
class ResendSerializer(serializers.Serializer):
phone = serializers.CharField(max_length=255)

View File

@@ -0,0 +1,6 @@
from rest_framework import serializers
class ChangePasswordSerializer(serializers.Serializer):
old_password = serializers.CharField(required=True)
new_password = serializers.CharField(required=True, min_length=8)

View File

@@ -0,0 +1,6 @@
from rest_framework import serializers
class SetPasswordSerializer(serializers.Serializer):
password = serializers.CharField()
token = serializers.CharField(max_length=255)

View File

@@ -0,0 +1,23 @@
from django.contrib.auth import get_user_model
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
class Meta:
exclude = [
"created_at",
"updated_at",
"password",
"groups",
"user_permissions"
]
model = get_user_model()
class UserUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = get_user_model()
fields = [
"first_name",
"last_name"
]

View File

@@ -0,0 +1 @@
from .user import * # noqa

View File

@@ -0,0 +1,10 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth import get_user_model
@receiver(post_save, sender=get_user_model())
def user_signal(sender, created, instance, **kwargs):
if created and instance.username is None:
instance.username = "U%(id)s" % {"id": 1000 + instance.id}
instance.save()

View File

View File

@@ -0,0 +1,116 @@
import logging
from unittest.mock import patch
from django.test import TestCase
from django.urls import reverse
from pydantic import BaseModel
from rest_framework import status
from rest_framework.test import APIClient
from core.apps.accounts.models import ResetToken
from django_core.models import SmsConfirm
from core.services import SmsService
from django.contrib.auth import get_user_model
class TokenModel(BaseModel):
access: str
refresh: str
class SmsViewTest(TestCase):
def setUp(self):
self.client = APIClient()
self.phone = "998999999999"
self.password = "password"
self.code = "1111"
self.token = "token"
self.user = get_user_model().objects.create_user(
phone=self.phone, first_name="John", last_name="Doe", password=self.password
)
SmsConfirm.objects.create(phone=self.phone, code=self.code)
def test_reg_view(self):
"""Test register view."""
data = {
"phone": "998999999991",
"first_name": "John",
"last_name": "Doe",
"password": "password",
}
with patch.object(SmsService, "send_confirm", return_value=True):
response = self.client.post(reverse("auth-register"), data=data)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(
response.data["data"]["detail"],
"Sms %(phone)s raqamiga yuborildi" % {"phone": data["phone"]},
)
def test_confirm_view(self):
"""Test confirm view."""
data = {"phone": self.phone, "code": self.code}
response = self.client.post(reverse("auth-confirm"), data=data)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
def test_invalid_confirm_view(self):
"""Test confirm view."""
data = {"phone": self.phone, "code": "1112"}
response = self.client.post(reverse("auth-confirm"), data=data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_reset_confirmation_code_view(self):
"""Test reset confirmation code view."""
data = {"phone": self.phone, "code": self.code}
response = self.client.post(reverse("auth-confirm"), data=data)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertIn("token", response.data["data"])
def test_reset_confirmation_code_view_invalid_code(self):
"""Test reset confirmation code view with invalid code."""
data = {"phone": self.phone, "code": "123456"}
response = self.client.post(reverse("auth-confirm"), data=data)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_reset_set_password_view(self):
"""Test reset set password view."""
token = ResetToken.objects.create(user=self.user, token=self.token)
data = {"token": token.token, "password": "new_password"}
response = self.client.post(reverse("reset-password-reset-password-set"), data=data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_reset_set_password_view_invalid_token(self):
"""Test reset set password view with invalid token."""
token = "test_token"
data = {"token": token, "password": "new_password"}
with patch.object(get_user_model().objects, "filter", return_value=get_user_model().objects.none()):
response = self.client.post(reverse("reset-password-reset-password-set"), data=data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.data["data"]["detail"], "Invalid token")
def test_resend_view(self):
"""Test resend view."""
data = {"phone": self.phone}
response = self.client.post(reverse("auth-resend"), data=data)
logging.error(response.json())
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_reset_password_view(self):
"""Test reset password view."""
data = {"phone": self.phone}
response = self.client.post(reverse("reset-password-reset-password"), data=data)
logging.error(response.json())
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_me_view(self):
"""Test me view."""
self.client.force_authenticate(user=self.user)
response = self.client.get(reverse("me-me"))
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_me_update_view(self):
"""Test me update view."""
self.client.force_authenticate(user=self.user)
data = {"first_name": "Updated"}
response = self.client.patch(reverse("me-user-update"), data=data)
logging.error(response.json())
self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@@ -0,0 +1,58 @@
from core.apps.accounts.serializers import ChangePasswordSerializer
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
class ChangePasswordViewTest(TestCase):
def setUp(self):
self.client = APIClient()
self.phone = "9981111111"
self.password = "12345670"
self.path = reverse("change-password-change-password")
self.user = get_user_model().objects.create_user(
phone=self.phone, password=self.password, email="test@example.com"
)
self.client.force_authenticate(user=self.user)
def test_change_password_success(self):
data = {
"old_password": self.password,
"new_password": "newpassword",
}
response = self.client.post(self.path, data=data, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['data']["detail"], "password changed successfully")
self.assertTrue(self.user.check_password("newpassword"))
def test_change_password_invalid_old_password(self):
data = {
"old_password": "wrongpassword",
"new_password": "newpassword",
}
response = self.client.post(self.path, data=data, format="json")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.data['data']["detail"], "invalida password")
def test_change_password_serializer_validation(self):
data = {
"old_password": self.password,
"new_password": "newpassword",
}
serializer = ChangePasswordSerializer(data=data)
self.assertTrue(serializer.is_valid())
data = {
"old_password": self.password,
"new_password": "123",
}
serializer = ChangePasswordSerializer(data=data)
self.assertFalse(serializer.is_valid())
def test_change_password_view_permissions(self):
self.client.force_authenticate(user=None)
response = self.client.post(self.path, data={}, format="json")
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

View File

@@ -0,0 +1,26 @@
"""
Accounts app urls
"""
from django.urls import path, include
from rest_framework_simplejwt import views as jwt_views
from .views import RegisterView, ResetPasswordView, MeView, ChangePasswordView
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register("", RegisterView, basename="auth")
router.register("", ResetPasswordView, basename="reset-password")
router.register("", MeView, basename="me")
router.register("", ChangePasswordView, basename="change-password")
urlpatterns = [
path("", include(router.urls)),
path("token/", jwt_views.TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("token/verify/", jwt_views.TokenVerifyView.as_view(), name="token_verify"),
path(
"token/refresh/",
jwt_views.TokenRefreshView.as_view(),
name="token_refresh",
),
]

View File

@@ -0,0 +1 @@
from .auth import * # noqa

View File

@@ -0,0 +1,209 @@
import uuid
from typing import Type
from core.services import UserService, SmsService
from django.contrib.auth import get_user_model
from django.utils.translation import gettext_lazy as _
from django_core import exceptions
from drf_spectacular.utils import extend_schema
from rest_framework import status, throttling, request
from rest_framework.response import Response
from rest_framework.exceptions import PermissionDenied
from rest_framework.viewsets import GenericViewSet
from django_core.mixins import BaseViewSetMixin
from rest_framework.decorators import action
from ..serializers import (
RegisterSerializer,
ConfirmSerializer,
ResendSerializer,
ResetPasswordSerializer,
ResetConfirmationSerializer,
SetPasswordSerializer,
UserSerializer,
UserUpdateSerializer,
)
from rest_framework.permissions import AllowAny
from django.contrib.auth.hashers import make_password
from drf_spectacular.utils import OpenApiResponse
from rest_framework.permissions import IsAuthenticated
from ..serializers import ChangePasswordSerializer
from .. import models
@extend_schema(tags=["register"])
class RegisterView(BaseViewSetMixin, GenericViewSet, UserService):
throttle_classes = [throttling.UserRateThrottle]
permission_classes = [AllowAny]
def get_serializer_class(self):
match self.action:
case "register":
return RegisterSerializer
case "confirm":
return ConfirmSerializer
case "resend":
return ResendSerializer
case _:
return RegisterSerializer
@action(methods=["POST"], detail=False, url_path="register")
def register(self, request):
ser = self.get_serializer(data=request.data)
ser.is_valid(raise_exception=True)
data = ser.data
phone = data.get("phone")
# Create pending user
self.create_user(phone, data.get("first_name"), data.get("last_name"), data.get("password"))
self.send_confirmation(phone) # Send confirmation code for sms eskiz.uz
return Response(
{"detail": _("Sms %(phone)s raqamiga yuborildi") % {"phone": phone}},
status=status.HTTP_202_ACCEPTED,
)
@extend_schema(summary="Auth confirm.", description="Auth confirm user.")
@action(methods=["POST"], detail=False, url_path="confirm")
def confirm(self, request):
ser = self.get_serializer(data=request.data)
ser.is_valid(raise_exception=True)
data = ser.data
phone, code = data.get("phone"), data.get("code")
try:
if SmsService.check_confirm(phone, code=code):
token = self.validate_user(get_user_model().objects.filter(phone=phone).first())
return Response(
data={
"detail": _("Tasdiqlash ko'di qabul qilindi"),
"token": token,
},
status=status.HTTP_202_ACCEPTED,
)
except exceptions.SmsException as e:
raise PermissionDenied(e) # Response exception for APIException
except Exception as e:
raise PermissionDenied(e) # Api exception for APIException
@action(methods=["POST"], detail=False, url_path="resend")
def resend(self, rq: Type[request.Request]):
ser = self.get_serializer(data=rq.data)
ser.is_valid(raise_exception=True)
phone = ser.data.get("phone")
self.send_confirmation(phone)
return Response({"detail": _("Sms %(phone)s raqamiga yuborildi") % {"phone": phone}})
@extend_schema(tags=["reset-password"])
class ResetPasswordView(BaseViewSetMixin, GenericViewSet, UserService):
permission_classes = [AllowAny]
def get_serializer_class(self):
match self.action:
case "reset_password":
return ResetPasswordSerializer
case "reset_confirm":
return ResetConfirmationSerializer
case "reset_password_set":
return SetPasswordSerializer
case _:
return None
@action(methods=["POST"], detail=False, url_path="reset-password")
def reset_password(self, request):
ser = self.get_serializer(data=request.data)
ser.is_valid(raise_exception=True)
phone = ser.data.get("phone")
self.send_confirmation(phone)
return Response({"detail": _("Sms %(phone)s raqamiga yuborildi") % {"phone": phone}})
@action(methods=["POST"], detail=False, url_path="reset-password-confirm")
def reset_confirm(self, request):
ser = self.get_serializer(data=request.data)
ser.is_valid(raise_exception=True)
data = ser.data
code, phone = data.get("code"), data.get("phone")
try:
SmsService.check_confirm(phone, code)
token = models.ResetToken.objects.create(
user=get_user_model().objects.filter(phone=phone).first(),
token=str(uuid.uuid4()),
)
return Response(
data={
"token": token.token,
"created_at": token.created_at,
"updated_at": token.updated_at,
},
status=status.HTTP_200_OK,
)
except exceptions.SmsException as e:
raise PermissionDenied(str(e))
except Exception as e:
raise PermissionDenied(str(e))
@action(methods=["POST"], detail=False, url_path="reset-password-set")
def reset_password_set(self, request):
ser = self.get_serializer(data=request.data)
ser.is_valid(raise_exception=True)
data = ser.data
token = data.get("token")
password = data.get("password")
token = models.ResetToken.objects.filter(token=token)
if not token.exists():
raise PermissionDenied(_("Invalid token"))
phone = token.first().user.phone
token.delete()
self.change_password(phone, password)
return Response({"detail": _("password updated")}, status=status.HTTP_200_OK)
@extend_schema(tags=["me"])
class MeView(BaseViewSetMixin, GenericViewSet, UserService):
permission_classes = [IsAuthenticated]
def get_serializer_class(self):
match self.action:
case "me":
return UserSerializer
case "user_update":
return UserUpdateSerializer
case _:
return None
@action(methods=["GET", "OPTIONS"], detail=False, url_path="me")
def me(self, request):
return Response(self.get_serializer(request.user).data)
@action(methods=["PATCH", "PUT"], detail=False, url_path="user-update")
def user_update(self, request):
ser = self.get_serializer(instance=request.user, data=request.data, partial=True)
ser.is_valid(raise_exception=True)
ser.save()
return Response({"detail": _("Malumotlar yangilandi")})
@extend_schema(tags=["change-password"], description="Parolni o'zgartirish uchun")
class ChangePasswordView(BaseViewSetMixin, GenericViewSet):
serializer_class = ChangePasswordSerializer
permission_classes = (IsAuthenticated,)
@extend_schema(
request=serializer_class,
responses={200: OpenApiResponse(ChangePasswordSerializer)},
summary="Change user password.",
description="Change password of the authenticated user.",
)
@action(methods=["POST"], detail=False, url_path="change-password")
def change_password(self, request, *args, **kwargs):
user = self.request.user
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
if user.check_password(request.data["old_password"]):
user.password = make_password(request.data["new_password"])
user.save()
return Response(
data={"detail": "password changed successfully"},
status=status.HTTP_200_OK,
)
raise PermissionDenied(_("invalida password"))

2
auth/core/apps/logs/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore