initial commit
This commit is contained in:
174
apps/users/services/auth.py
Normal file
174
apps/users/services/auth.py
Normal file
@@ -0,0 +1,174 @@
|
||||
import random
|
||||
import string
|
||||
|
||||
from django.utils import timezone
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import transaction
|
||||
|
||||
from django_redis import get_redis_connection
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
from rest_framework_simplejwt.exceptions import TokenError
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from apps.users.tasks import send_verification_sms
|
||||
from apps.users.utils import record_login_attempt
|
||||
from apps.users.models import LoginAttempt
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
def get_tokens_for_user(user):
|
||||
"""Helper service to generate JWT tokens."""
|
||||
refresh = RefreshToken.for_user(user)
|
||||
return {
|
||||
"access": str(refresh.access_token),
|
||||
"refresh": str(refresh),
|
||||
}
|
||||
|
||||
|
||||
def register_user_with_password(mobile, password):
|
||||
"""Business logic for registering a user with just a password."""
|
||||
user, created, restored = User.get_or_restore(mobile=mobile)
|
||||
|
||||
if not created and not restored:
|
||||
raise ValidationError({"detail": "User already exists."})
|
||||
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
|
||||
return get_tokens_for_user(user)
|
||||
|
||||
@transaction.atomic
|
||||
def register_user_with_otp(mobile, code, password, first_name="", last_name=""):
|
||||
"""Business logic for verifying OTP and registering a user."""
|
||||
# 1. Check if user already exists
|
||||
if User.objects.filter(mobile=mobile).exists():
|
||||
raise ValidationError({"mobile": "این شماره قبلاً ثبت شده است."})
|
||||
|
||||
# 2. Verify OTP in Redis
|
||||
redis_conn = get_redis_connection("default")
|
||||
stored_code = redis_conn.get(f"verification_code:{mobile}")
|
||||
|
||||
if not stored_code:
|
||||
raise ValidationError({"code": "کد تأیید یافت نشد."})
|
||||
if stored_code.decode("utf-8") != code:
|
||||
raise ValidationError({"code": "کد تأیید اشتباه است."})
|
||||
|
||||
# 3. Create User
|
||||
user = User.objects.create_user(
|
||||
mobile=mobile,
|
||||
password=password,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
is_verified=True,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
# 4. Clean up Redis
|
||||
redis_conn.delete(f"verification_code:{mobile}")
|
||||
|
||||
return get_tokens_for_user(user)
|
||||
|
||||
|
||||
def generate_and_send_otp(mobile, mode):
|
||||
"""Business logic for generating OTP, checking existence rules, and sending SMS."""
|
||||
user_exists = User.objects.filter(mobile=mobile).exists()
|
||||
|
||||
# Apply business rules based on mode
|
||||
if mode == "register" and user_exists:
|
||||
raise ValidationError({"mobile": "این شماره قبلاً ثبتنام شده است."})
|
||||
|
||||
if mode in ["login", "forget_password"] and not user_exists:
|
||||
raise ValidationError({"mobile": "این شماره یافت نشد."})
|
||||
|
||||
# Generate OTP
|
||||
verification_code = "".join(random.choices(string.digits, k=5))
|
||||
|
||||
# Store in Redis (Assuming 2 minutes / 120 seconds expiry)
|
||||
redis_conn = get_redis_connection("default")
|
||||
redis_conn.setex(f"verification_code:{mobile}", 120, verification_code)
|
||||
|
||||
# Trigger async SMS task
|
||||
send_verification_sms.delay(mobile, verification_code)
|
||||
|
||||
|
||||
def login_with_password(mobile, password, request=None):
|
||||
"""Authenticate user with password and record the attempt."""
|
||||
user = User.objects.filter(mobile=mobile).first()
|
||||
|
||||
if not user or not user.check_password(password):
|
||||
record_login_attempt(request, user, LoginAttempt.StatusType.FAILED)
|
||||
raise ValidationError({"detail": "شماره موبایل یا رمز عبور اشتباه است."})
|
||||
|
||||
if not user.is_active:
|
||||
raise ValidationError({"detail": "حساب کاربری شما غیرفعال شده است."})
|
||||
|
||||
record_login_attempt(request, user, LoginAttempt.StatusType.SUCCESS)
|
||||
return get_tokens_for_user(user)
|
||||
|
||||
|
||||
def login_with_otp(mobile, code, request=None):
|
||||
"""Authenticate or implicitly register user via OTP."""
|
||||
redis_conn = get_redis_connection("default")
|
||||
stored_code = redis_conn.get(f"verification_code:{mobile}")
|
||||
|
||||
if not stored_code or stored_code.decode("utf-8") != code:
|
||||
record_login_attempt(request, None, LoginAttempt.StatusType.FAILED)
|
||||
raise ValidationError({"code": "کد تایید نامعتبر است یا منقضی شده است."})
|
||||
|
||||
# Fixed Bug: Use `mobile=mobile`, not `username=mobile`
|
||||
user, created = User.objects.get_or_create(mobile=mobile)
|
||||
if created:
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
|
||||
if not user.is_active:
|
||||
raise ValidationError({"detail": "حساب کاربری شما غیرفعال شده است."})
|
||||
|
||||
record_login_attempt(request, user, LoginAttempt.StatusType.SUCCESS)
|
||||
|
||||
# Clean up Redis
|
||||
redis_conn.delete(f"verification_code:{mobile}")
|
||||
|
||||
return get_tokens_for_user(user)
|
||||
|
||||
|
||||
def reset_password_with_otp(mobile, code, password):
|
||||
"""Verify OTP and change forgotten password."""
|
||||
user = User.objects.filter(mobile=mobile).first()
|
||||
if not user:
|
||||
raise ValidationError({"mobile": "کاربری با این شماره یافت نشد."})
|
||||
|
||||
redis_conn = get_redis_connection("default")
|
||||
stored_code = redis_conn.get(f"verification_code:{mobile}")
|
||||
|
||||
if not stored_code or stored_code.decode("utf-8") != code:
|
||||
raise ValidationError({"code": "کد تایید نامعتبر است یا منقضی شده است."})
|
||||
|
||||
# Update password
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
|
||||
# Fixed Bug: Ensure we delete the correct key
|
||||
redis_conn.delete(f"verification_code:{mobile}")
|
||||
|
||||
|
||||
def change_password(user, old_password, new_password):
|
||||
"""Change password for an already authenticated user."""
|
||||
if not user.check_password(old_password):
|
||||
raise ValidationError({"old_password": "رمز عبور فعلی اشتباه است."})
|
||||
|
||||
user.set_password(new_password)
|
||||
user.password_updated_at = timezone.now()
|
||||
# Save only the fields that changed for DB performance
|
||||
user.save(update_fields=["password", "password_updated_at"])
|
||||
|
||||
|
||||
def logout_user(refresh_token_str):
|
||||
"""Blacklist the user's refresh token."""
|
||||
if not refresh_token_str:
|
||||
raise ValidationError({"refresh": "توکن رفرش الزامی است."})
|
||||
try:
|
||||
token = RefreshToken(refresh_token_str)
|
||||
token.blacklist()
|
||||
except TokenError:
|
||||
raise ValidationError({"detail": "توکن نامعتبر است یا قبلا منقضی شده است."})
|
||||
15
apps/users/services/forms.py
Normal file
15
apps/users/services/forms.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
|
||||
|
||||
from apps.users.models import User
|
||||
|
||||
|
||||
class CustomUserCreationForm(UserCreationForm):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ("mobile", "first_name", "last_name")
|
||||
|
||||
|
||||
class CustomUserChangeForm(UserChangeForm):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ("mobile", "is_verified")
|
||||
25
apps/users/services/managers.py
Normal file
25
apps/users/services/managers.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from django.contrib.auth.models import BaseUserManager
|
||||
|
||||
from core.models.base import SoftDeleteManager
|
||||
|
||||
|
||||
class UserManager(BaseUserManager, SoftDeleteManager):
|
||||
use_in_migrations = True
|
||||
|
||||
def _create_user(self, mobile, password, **extra_fields):
|
||||
if not mobile:
|
||||
raise ValueError("Mobile must be set")
|
||||
user = self.model(mobile=mobile, **extra_fields)
|
||||
user.set_password(password)
|
||||
user.save(using=self._db)
|
||||
return user
|
||||
|
||||
def create_user(self, mobile, password=None, **extra_fields):
|
||||
extra_fields.setdefault("is_staff", False)
|
||||
extra_fields.setdefault("is_superuser", False)
|
||||
return self._create_user(mobile, password, **extra_fields)
|
||||
|
||||
def create_superuser(self, mobile, password, **extra_fields):
|
||||
extra_fields.setdefault("is_staff", True)
|
||||
extra_fields.setdefault("is_superuser", True)
|
||||
return self._create_user(mobile, password, **extra_fields)
|
||||
Reference in New Issue
Block a user