mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 11:11:10 -05:00
- Implemented PrimeProgress component with support for labels, helper text, and various styles (size, variant, color). - Created PrimeSelect component with dropdown functionality, custom templates, and validation states. - Developed PrimeSkeleton component for loading placeholders with different shapes and animations. - Updated index.ts to export new components for easy import. - Enhanced PrimeVueTest.vue to include tests for new components and their functionalities. - Introduced a custom ThrillWiki theme for PrimeVue with tailored color schemes and component styles. - Added ambient type declarations for various components to improve TypeScript support.
498 lines
15 KiB
Python
498 lines
15 KiB
Python
"""
|
|
Authentication domain serializers for ThrillWiki API v1.
|
|
|
|
This module contains all serializers related to user authentication,
|
|
registration, password management, and social authentication.
|
|
"""
|
|
|
|
from rest_framework import serializers
|
|
from django.contrib.auth import get_user_model, authenticate
|
|
from django.contrib.auth.password_validation import validate_password
|
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
|
from drf_spectacular.utils import (
|
|
extend_schema_serializer,
|
|
OpenApiExample,
|
|
)
|
|
|
|
UserModel = get_user_model()
|
|
|
|
|
|
# === USER SERIALIZERS ===
|
|
|
|
|
|
@extend_schema_serializer(
|
|
examples=[
|
|
OpenApiExample(
|
|
"User Output Example",
|
|
summary="Example user response",
|
|
description="A typical user object in API responses",
|
|
value={
|
|
"id": 1,
|
|
"username": "thrillseeker",
|
|
"email": "user@example.com",
|
|
"first_name": "John",
|
|
"last_name": "Doe",
|
|
"is_active": True,
|
|
"date_joined": "2024-01-01T00:00:00Z",
|
|
},
|
|
)
|
|
]
|
|
)
|
|
class UserOutputSerializer(serializers.ModelSerializer):
|
|
"""Output serializer for user data."""
|
|
|
|
class Meta:
|
|
model = UserModel
|
|
fields = [
|
|
"id",
|
|
"username",
|
|
"email",
|
|
"first_name",
|
|
"last_name",
|
|
"is_active",
|
|
"date_joined",
|
|
]
|
|
read_only_fields = ["id", "date_joined"]
|
|
|
|
|
|
# === LOGIN SERIALIZERS ===
|
|
|
|
|
|
@extend_schema_serializer(
|
|
examples=[
|
|
OpenApiExample(
|
|
"Login Input Example",
|
|
summary="Example login request",
|
|
description="Login with username or email and password",
|
|
value={
|
|
"username": "thrillseeker",
|
|
"password": "securepassword123",
|
|
},
|
|
)
|
|
]
|
|
)
|
|
class LoginInputSerializer(serializers.Serializer):
|
|
"""Input serializer for user login."""
|
|
|
|
username = serializers.CharField(
|
|
max_length=150,
|
|
help_text="Username or email address",
|
|
)
|
|
password = serializers.CharField(
|
|
write_only=True,
|
|
style={"input_type": "password"},
|
|
help_text="User password",
|
|
)
|
|
|
|
def validate(self, attrs):
|
|
"""Validate login credentials."""
|
|
username = attrs.get("username")
|
|
password = attrs.get("password")
|
|
|
|
if username and password:
|
|
# Try to authenticate with the provided credentials
|
|
user = authenticate(
|
|
request=self.context.get("request"),
|
|
username=username,
|
|
password=password,
|
|
)
|
|
|
|
if not user:
|
|
# Try email-based authentication if username failed
|
|
if "@" in username:
|
|
try:
|
|
user_obj = UserModel.objects.get(email=username)
|
|
user = authenticate(
|
|
request=self.context.get("request"),
|
|
username=user_obj.username, # type: ignore[attr-defined]
|
|
password=password,
|
|
)
|
|
except UserModel.DoesNotExist:
|
|
pass
|
|
|
|
if not user:
|
|
raise serializers.ValidationError("Invalid credentials")
|
|
|
|
if not user.is_active:
|
|
raise serializers.ValidationError("Account is disabled")
|
|
|
|
attrs["user"] = user
|
|
else:
|
|
raise serializers.ValidationError("Must include username and password")
|
|
|
|
return attrs
|
|
|
|
|
|
@extend_schema_serializer(
|
|
examples=[
|
|
OpenApiExample(
|
|
"Login Output Example",
|
|
summary="Example login response",
|
|
description="Successful login response with token and user data",
|
|
value={
|
|
"token": "abc123def456ghi789",
|
|
"user": {
|
|
"id": 1,
|
|
"username": "thrillseeker",
|
|
"email": "user@example.com",
|
|
"first_name": "John",
|
|
"last_name": "Doe",
|
|
},
|
|
"message": "Login successful",
|
|
},
|
|
)
|
|
]
|
|
)
|
|
class LoginOutputSerializer(serializers.Serializer):
|
|
"""Output serializer for login response."""
|
|
|
|
token = serializers.CharField(help_text="Authentication token")
|
|
user = UserOutputSerializer(help_text="User information")
|
|
message = serializers.CharField(help_text="Success message")
|
|
|
|
|
|
# === SIGNUP SERIALIZERS ===
|
|
|
|
|
|
@extend_schema_serializer(
|
|
examples=[
|
|
OpenApiExample(
|
|
"Signup Input Example",
|
|
summary="Example registration request",
|
|
description="Register a new user account",
|
|
value={
|
|
"username": "newuser",
|
|
"email": "newuser@example.com",
|
|
"password": "securepassword123",
|
|
"password_confirm": "securepassword123",
|
|
"first_name": "Jane",
|
|
"last_name": "Smith",
|
|
},
|
|
)
|
|
]
|
|
)
|
|
class SignupInputSerializer(serializers.ModelSerializer):
|
|
"""Input serializer for user registration."""
|
|
|
|
password = serializers.CharField(
|
|
write_only=True,
|
|
style={"input_type": "password"},
|
|
help_text="User password",
|
|
)
|
|
password_confirm = serializers.CharField(
|
|
write_only=True,
|
|
style={"input_type": "password"},
|
|
help_text="Password confirmation",
|
|
)
|
|
|
|
class Meta:
|
|
model = UserModel
|
|
fields = [
|
|
"username",
|
|
"email",
|
|
"password",
|
|
"password_confirm",
|
|
"first_name",
|
|
"last_name",
|
|
]
|
|
|
|
def validate_email(self, value):
|
|
"""Validate email uniqueness."""
|
|
if UserModel.objects.filter(email=value).exists():
|
|
raise serializers.ValidationError("Email already registered")
|
|
return value
|
|
|
|
def validate_username(self, value):
|
|
"""Validate username uniqueness."""
|
|
if UserModel.objects.filter(username=value).exists():
|
|
raise serializers.ValidationError("Username already taken")
|
|
return value
|
|
|
|
def validate_password(self, value):
|
|
"""Validate password strength."""
|
|
try:
|
|
validate_password(value)
|
|
except DjangoValidationError as e:
|
|
raise serializers.ValidationError(list(e.messages))
|
|
return value
|
|
|
|
def validate(self, attrs):
|
|
"""Cross-field validation."""
|
|
password = attrs.get("password")
|
|
password_confirm = attrs.get("password_confirm")
|
|
|
|
if password != password_confirm:
|
|
raise serializers.ValidationError("Passwords do not match")
|
|
|
|
return attrs
|
|
|
|
def create(self, validated_data):
|
|
"""Create new user."""
|
|
validated_data.pop("password_confirm")
|
|
password = validated_data.pop("password")
|
|
|
|
user = UserModel.objects.create_user( # type: ignore[attr-defined]
|
|
password=password,
|
|
**validated_data,
|
|
)
|
|
return user
|
|
|
|
|
|
@extend_schema_serializer(
|
|
examples=[
|
|
OpenApiExample(
|
|
"Signup Output Example",
|
|
summary="Example registration response",
|
|
description="Successful registration response with token and user data",
|
|
value={
|
|
"token": "abc123def456ghi789",
|
|
"user": {
|
|
"id": 2,
|
|
"username": "newuser",
|
|
"email": "newuser@example.com",
|
|
"first_name": "Jane",
|
|
"last_name": "Smith",
|
|
},
|
|
"message": "Registration successful",
|
|
},
|
|
)
|
|
]
|
|
)
|
|
class SignupOutputSerializer(serializers.Serializer):
|
|
"""Output serializer for registration response."""
|
|
|
|
token = serializers.CharField(help_text="Authentication token")
|
|
user = UserOutputSerializer(help_text="User information")
|
|
message = serializers.CharField(help_text="Success message")
|
|
|
|
|
|
# === LOGOUT SERIALIZERS ===
|
|
|
|
|
|
@extend_schema_serializer(
|
|
examples=[
|
|
OpenApiExample(
|
|
"Logout Output Example",
|
|
summary="Example logout response",
|
|
description="Successful logout response",
|
|
value={
|
|
"message": "Logout successful",
|
|
},
|
|
)
|
|
]
|
|
)
|
|
class LogoutOutputSerializer(serializers.Serializer):
|
|
"""Output serializer for logout response."""
|
|
|
|
message = serializers.CharField(help_text="Success message")
|
|
|
|
|
|
# === PASSWORD RESET SERIALIZERS ===
|
|
|
|
|
|
@extend_schema_serializer(
|
|
examples=[
|
|
OpenApiExample(
|
|
"Password Reset Input Example",
|
|
summary="Example password reset request",
|
|
description="Request password reset email",
|
|
value={
|
|
"email": "user@example.com",
|
|
},
|
|
)
|
|
]
|
|
)
|
|
class PasswordResetInputSerializer(serializers.Serializer):
|
|
"""Input serializer for password reset request."""
|
|
|
|
email = serializers.EmailField(help_text="Email address for password reset")
|
|
|
|
def validate_email(self, value):
|
|
"""Validate email exists."""
|
|
if not UserModel.objects.filter(email=value).exists():
|
|
# Don't reveal if email exists for security
|
|
pass
|
|
return value
|
|
|
|
def save(self, **kwargs): # type: ignore[override]
|
|
"""Send password reset email."""
|
|
email = self.validated_data["email"] # type: ignore[index]
|
|
try:
|
|
_user = UserModel.objects.get(email=email)
|
|
# Here you would typically send a password reset email
|
|
# For now, we'll just pass
|
|
pass
|
|
except UserModel.DoesNotExist:
|
|
# Don't reveal if email exists for security
|
|
pass
|
|
|
|
|
|
@extend_schema_serializer(
|
|
examples=[
|
|
OpenApiExample(
|
|
"Password Reset Output Example",
|
|
summary="Example password reset response",
|
|
description="Password reset email sent response",
|
|
value={
|
|
"detail": "Password reset email sent",
|
|
},
|
|
)
|
|
]
|
|
)
|
|
class PasswordResetOutputSerializer(serializers.Serializer):
|
|
"""Output serializer for password reset response."""
|
|
|
|
detail = serializers.CharField(help_text="Success message")
|
|
|
|
|
|
# === PASSWORD CHANGE SERIALIZERS ===
|
|
|
|
|
|
@extend_schema_serializer(
|
|
examples=[
|
|
OpenApiExample(
|
|
"Password Change Input Example",
|
|
summary="Example password change request",
|
|
description="Change current user's password",
|
|
value={
|
|
"old_password": "oldpassword123",
|
|
"new_password": "newpassword456",
|
|
"new_password_confirm": "newpassword456",
|
|
},
|
|
)
|
|
]
|
|
)
|
|
class PasswordChangeInputSerializer(serializers.Serializer):
|
|
"""Input serializer for password change."""
|
|
|
|
old_password = serializers.CharField(
|
|
write_only=True,
|
|
style={"input_type": "password"},
|
|
help_text="Current password",
|
|
)
|
|
new_password = serializers.CharField(
|
|
write_only=True,
|
|
style={"input_type": "password"},
|
|
help_text="New password",
|
|
)
|
|
new_password_confirm = serializers.CharField(
|
|
write_only=True,
|
|
style={"input_type": "password"},
|
|
help_text="New password confirmation",
|
|
)
|
|
|
|
def validate_old_password(self, value):
|
|
"""Validate current password."""
|
|
user = self.context["request"].user
|
|
if not user.check_password(value):
|
|
raise serializers.ValidationError("Current password is incorrect")
|
|
return value
|
|
|
|
def validate_new_password(self, value):
|
|
"""Validate new password strength."""
|
|
try:
|
|
validate_password(value, user=self.context["request"].user)
|
|
except DjangoValidationError as e:
|
|
raise serializers.ValidationError(list(e.messages))
|
|
return value
|
|
|
|
def validate(self, attrs):
|
|
"""Cross-field validation."""
|
|
new_password = attrs.get("new_password")
|
|
new_password_confirm = attrs.get("new_password_confirm")
|
|
|
|
if new_password != new_password_confirm:
|
|
raise serializers.ValidationError("New passwords do not match")
|
|
|
|
return attrs
|
|
|
|
def save(self, **kwargs): # type: ignore[override]
|
|
"""Change user password."""
|
|
user = self.context["request"].user
|
|
user.set_password(self.validated_data["new_password"]) # type: ignore[index]
|
|
user.save()
|
|
return user
|
|
|
|
|
|
@extend_schema_serializer(
|
|
examples=[
|
|
OpenApiExample(
|
|
"Password Change Output Example",
|
|
summary="Example password change response",
|
|
description="Password changed successfully response",
|
|
value={
|
|
"detail": "Password changed successfully",
|
|
},
|
|
)
|
|
]
|
|
)
|
|
class PasswordChangeOutputSerializer(serializers.Serializer):
|
|
"""Output serializer for password change response."""
|
|
|
|
detail = serializers.CharField(help_text="Success message")
|
|
|
|
|
|
# === SOCIAL PROVIDER SERIALIZERS ===
|
|
|
|
|
|
@extend_schema_serializer(
|
|
examples=[
|
|
OpenApiExample(
|
|
"Social Provider Example",
|
|
summary="Example social provider",
|
|
description="Available social authentication provider",
|
|
value={
|
|
"id": "google",
|
|
"name": "Google",
|
|
"authUrl": "https://example.com/accounts/google/login/",
|
|
},
|
|
)
|
|
]
|
|
)
|
|
class SocialProviderOutputSerializer(serializers.Serializer):
|
|
"""Output serializer for social authentication providers."""
|
|
|
|
id = serializers.CharField(help_text="Provider ID")
|
|
name = serializers.CharField(help_text="Provider display name")
|
|
authUrl = serializers.URLField(help_text="Authentication URL")
|
|
|
|
|
|
# === AUTH STATUS SERIALIZERS ===
|
|
|
|
|
|
@extend_schema_serializer(
|
|
examples=[
|
|
OpenApiExample(
|
|
"Auth Status Authenticated Example",
|
|
summary="Example authenticated status",
|
|
description="Response when user is authenticated",
|
|
value={
|
|
"authenticated": True,
|
|
"user": {
|
|
"id": 1,
|
|
"username": "thrillseeker",
|
|
"email": "user@example.com",
|
|
"first_name": "John",
|
|
"last_name": "Doe",
|
|
},
|
|
},
|
|
),
|
|
OpenApiExample(
|
|
"Auth Status Unauthenticated Example",
|
|
summary="Example unauthenticated status",
|
|
description="Response when user is not authenticated",
|
|
value={
|
|
"authenticated": False,
|
|
"user": None,
|
|
},
|
|
),
|
|
]
|
|
)
|
|
class AuthStatusOutputSerializer(serializers.Serializer):
|
|
"""Output serializer for authentication status."""
|
|
|
|
authenticated = serializers.BooleanField(help_text="Whether user is authenticated")
|
|
user = UserOutputSerializer(
|
|
allow_null=True, help_text="User information if authenticated"
|
|
)
|