project initialization
Some checks failed
System Monitoring / Health Checks (push) Has been cancelled
System Monitoring / Performance Monitoring (push) Has been cancelled
System Monitoring / Database Monitoring (push) Has been cancelled
System Monitoring / Cache Monitoring (push) Has been cancelled
System Monitoring / Log Monitoring (push) Has been cancelled
System Monitoring / Resource Monitoring (push) Has been cancelled
System Monitoring / Uptime Monitoring (push) Has been cancelled
System Monitoring / Backup Monitoring (push) Has been cancelled
System Monitoring / Security Monitoring (push) Has been cancelled
System Monitoring / Monitoring Dashboard (push) Has been cancelled
System Monitoring / Alerting (push) Has been cancelled
Security Scanning / Dependency Scanning (push) Has been cancelled
Security Scanning / Code Security Scanning (push) Has been cancelled
Security Scanning / Secrets Scanning (push) Has been cancelled
Security Scanning / Container Security Scanning (push) Has been cancelled
Security Scanning / Compliance Checking (push) Has been cancelled
Security Scanning / Security Dashboard (push) Has been cancelled
Security Scanning / Security Remediation (push) Has been cancelled

This commit is contained in:
2025-10-05 02:37:33 +08:00
parent 2cbb6d5fa1
commit b3fff546e9
226 changed files with 97805 additions and 35 deletions

View File

@@ -0,0 +1,995 @@
"""
Authentication API endpoints for multi-tenant SaaS platform.
Provides REST API endpoints for:
- User registration and login
- Multi-method authentication
- MFA verification and management
- Token management and refresh
- Password management
- Social authentication
- Magic link authentication
"""
from datetime import datetime, timedelta
from typing import Dict, Any, Optional
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.utils import timezone
from django.core.cache import cache
from rest_framework import status, generics, viewsets
from rest_framework.decorators import action, api_view, permission_classes
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.response import Response
from rest_framework.request import Request
from rest_framework_simplejwt.tokens import RefreshToken
from drf_spectacular.utils import extend_schema, OpenApiParameter
from drf_spectacular.types import OpenApiTypes
from logging import getLogger
from ..models.tenant import Tenant
from ..auth.jwt_service import jwt_service
from ..auth.authentication import auth_backend
from ..auth.mfa import mfa_service
from ..auth.permissions import TenantPermission, HasPermission
from ..serializers.auth_serializers import (
LoginSerializer,
RegisterSerializer,
MFASerializer,
MFASetupSerializer,
TokenRefreshSerializer,
PasswordChangeSerializer,
PasswordResetSerializer,
PasswordResetConfirmSerializer,
MagicLinkSerializer,
SocialAuthSerializer,
BiometricAuthSerializer,
BackupCodeSerializer,
AuthStatusSerializer,
)
from ..services.user_service import user_service
from ..exceptions import AuthenticationError, ValidationError
User = get_user_model()
logger = getLogger(__name__)
class AuthViewSet(viewsets.GenericViewSet):
"""
Authentication API endpoints.
Provides comprehensive authentication functionality including:
- Multi-method login/logout
- User registration
- MFA setup and verification
- Token management
- Password management
- Social authentication
- Magic link authentication
"""
permission_classes = [AllowAny]
serializer_class = AuthStatusSerializer
def get_serializer_class(self):
"""Return appropriate serializer based on action."""
if self.action == 'login':
return LoginSerializer
elif self.action == 'register':
return RegisterSerializer
elif self.action == 'mfa_verify':
return MFASerializer
elif self.action == 'mfa_setup':
return MFASetupSerializer
elif self.action == 'refresh_token':
return TokenRefreshSerializer
elif self.action == 'change_password':
return PasswordChangeSerializer
elif self.action == 'reset_password':
return PasswordResetSerializer
elif self.action == 'confirm_password_reset':
return PasswordResetConfirmSerializer
elif self.action == 'magic_link':
return MagicLinkSerializer
elif self.action == 'social_auth':
return SocialAuthSerializer
elif self.action == 'biometric_auth':
return BiometricAuthSerializer
elif self.action == 'backup_codes':
return BackupCodeSerializer
return super().get_serializer_class()
@extend_schema(
summary="User Login",
description="Authenticate user with multiple methods",
responses={200: AuthStatusSerializer},
)
@action(detail=False, methods=['post'])
def login(self, request: Request) -> Response:
"""
Login user with specified authentication method.
Supported methods:
- password: Email/username + password
- ic: Malaysian IC number + password
- company: Company registration + password
- phone: Phone number + SMS code
- magic: Magic link token
- biometric: Biometric authentication
- google: Google OAuth
- facebook: Facebook OAuth
"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
method = serializer.validated_data['method']
credentials = serializer.validated_data['credentials']
# Perform authentication
auth_result = auth_backend.authenticate(request, method=method, **credentials)
# Handle MFA requirement
if isinstance(auth_result, dict) and auth_result.get('requires_mfa'):
user = auth_result['user']
return Response({
'requires_mfa': True,
'user_id': str(user.id),
'email': user.email,
'mfa_methods': user.get_available_mfa_methods(),
}, status=status.HTTP_200_OK)
# Generate JWT tokens
user = auth_result
tenant = getattr(request, 'tenant', None) or getattr(user, 'tenant', None)
device_info = {
'user_agent': request.META.get('HTTP_USER_AGENT', ''),
'ip_address': request.META.get('REMOTE_ADDR', ''),
'device_type': self._get_device_type(request),
}
tokens = jwt_service.generate_token_pair(user, tenant, device_info)
# Update user last login
user.last_login = timezone.now()
user.save(update_fields=['last_login'])
logger.info(f"User {user.id} logged in successfully via {method}")
return Response({
'user': self._serialize_user(user),
'tokens': tokens,
'mfa_status': mfa_service.get_mfa_status(user),
}, status=status.HTTP_200_OK)
except AuthenticationError as e:
logger.warning(f"Login failed: {str(e)}")
return Response(
{'error': str(e)},
status=status.HTTP_401_UNAUTHORIZED
)
except Exception as e:
logger.error(f"Login error: {str(e)}")
return Response(
{'error': 'Authentication failed'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@extend_schema(
summary="User Registration",
description="Register new user with tenant context",
responses={201: AuthStatusSerializer},
)
@action(detail=False, methods=['post'])
def register(self, request: Request) -> Response:
"""
Register new user account.
Supports individual and tenant-based registration with:
- Email verification
- Phone verification (optional)
- Malaysian IC validation
- Company registration validation
"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
# Create user
user = user_service.create_user(
email=serializer.validated_data['email'],
password=serializer.validated_data['password'],
first_name=serializer.validated_data.get('first_name', ''),
last_name=serializer.validated_data.get('last_name', ''),
phone_number=serializer.validated_data.get('phone_number'),
malaysian_ic=serializer.validated_data.get('malaysian_ic'),
tenant_id=serializer.validated_data.get('tenant_id'),
role=serializer.validated_data.get('role', 'user'),
)
# Generate verification codes if required
verification_codes = {}
if user.email and not user.email_verified:
email_code = auth_backend.generate_registration_otp(user.email)
verification_codes['email_otp'] = email_code['email_otp']
if user.phone_number and not user.phone_verified:
phone_code = auth_backend.generate_phone_verification_code(user.phone_number)
verification_codes['phone_otp'] = phone_code
logger.info(f"User {user.id} registered successfully")
return Response({
'user': self._serialize_user(user),
'verification_codes': verification_codes,
'message': 'User registered successfully. Please verify your email.',
}, status=status.HTTP_201_CREATED)
except ValidationError as e:
logger.warning(f"Registration validation error: {str(e)}")
return Response(
{'error': str(e)},
status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
logger.error(f"Registration error: {str(e)}")
return Response(
{'error': 'Registration failed'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@extend_schema(
summary="MFA Verification",
description="Verify MFA code for authentication",
responses={200: AuthStatusSerializer},
)
@action(detail=False, methods=['post'])
def mfa_verify(self, request: Request) -> Response:
"""
Verify MFA code to complete authentication.
Supported MFA methods:
- totp: Time-based One-Time Password
- sms: SMS verification code
- email: Email verification code
- backup: Backup code
"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
user_id = serializer.validated_data['user_id']
method = serializer.validated_data['method']
code = serializer.validated_data['code']
user = User.objects.get(id=user_id)
# Verify MFA code
if not mfa_service.validate_mfa_attempt(user, method, code):
return Response(
{'error': 'Invalid MFA code'},
status=status.HTTP_400_BAD_REQUEST
)
# Generate JWT tokens
tenant = getattr(request, 'tenant', None) or getattr(user, 'tenant', None)
device_info = {
'user_agent': request.META.get('HTTP_USER_AGENT', ''),
'ip_address': request.META.get('REMOTE_ADDR', ''),
'device_type': self._get_device_type(request),
}
tokens = jwt_service.generate_token_pair(user, tenant, device_info)
# Update user last login
user.last_login = timezone.now()
user.save(update_fields=['last_login'])
logger.info(f"User {user.id} MFA verification successful")
return Response({
'user': self._serialize_user(user),
'tokens': tokens,
'mfa_status': mfa_service.get_mfa_status(user),
}, status=status.HTTP_200_OK)
except User.DoesNotExist:
return Response(
{'error': 'User not found'},
status=status.HTTP_404_NOT_FOUND
)
except AuthenticationError as e:
return Response(
{'error': str(e)},
status=status.HTTP_401_UNAUTHORIZED
)
except Exception as e:
logger.error(f"MFA verification error: {str(e)}")
return Response(
{'error': 'MFA verification failed'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@extend_schema(
summary="MFA Setup",
description="Set up MFA for user account",
responses={200: MFASetupSerializer},
)
@action(detail=False, methods=['post'])
def mfa_setup(self, request: Request) -> Response:
"""
Set up MFA for user account.
Supports TOTP setup with QR code generation and verification.
"""
if not request.user.is_authenticated:
return Response(
{'error': 'Authentication required'},
status=status.HTTP_401_UNAUTHORIZED
)
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
user = request.user
method = serializer.validated_data.get('method', 'totp')
if method == 'totp':
# Generate TOTP setup
totp_data = mfa_service.setup_totp(user)
if 'verification_code' in serializer.validated_data:
# Verify TOTP setup
secret = serializer.validated_data['secret']
code = serializer.validated_data['verification_code']
if mfa_service.verify_totp_setup(user, secret, code):
return Response({
'message': 'MFA enabled successfully',
'backup_codes': mfa_service.generate_backup_codes(user),
})
else:
return Response(
{'error': 'Invalid verification code'},
status=status.HTTP_400_BAD_REQUEST
)
return Response(totp_data)
else:
return Response(
{'error': f'MFA method {method} not supported'},
status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
logger.error(f"MFA setup error: {str(e)}")
return Response(
{'error': 'MFA setup failed'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@extend_schema(
summary="Token Refresh",
description="Refresh access token using refresh token",
responses={200: TokenRefreshSerializer},
)
@action(detail=False, methods=['post'])
def refresh_token(self, request: Request) -> Response:
"""
Refresh access token using refresh token.
"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
refresh_token = serializer.validated_data['refresh_token']
new_tokens = jwt_service.refresh_access_token(refresh_token)
return Response(new_tokens, status=status.HTTP_200_OK)
except AuthenticationError as e:
return Response(
{'error': str(e)},
status=status.HTTP_401_UNAUTHORIZED
)
except Exception as e:
logger.error(f"Token refresh error: {str(e)}")
return Response(
{'error': 'Token refresh failed'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@extend_schema(
summary="Logout",
description="Logout user and blacklist tokens",
responses={200: {'type': 'object', 'properties': {'message': {'type': 'string'}}}},
)
@action(detail=False, methods=['post'])
def logout(self, request: Request) -> Response:
"""
Logout user and blacklist current tokens.
"""
if not request.user.is_authenticated:
return Response(
{'error': 'Authentication required'},
status=status.HTTP_401_UNAUTHORIZED
)
try:
# Get authorization header
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
if auth_header.startswith('Bearer '):
token = auth_header.split(' ')[1]
jwt_service.blacklist_token(token)
# Blacklist all user sessions if requested
blacklist_all = request.data.get('blacklist_all_sessions', False)
if blacklist_all:
jwt_service.blacklist_token(token, blacklist_all_sessions=True)
logger.info(f"User {request.user.id} logged out successfully")
return Response({'message': 'Logged out successfully'}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Logout error: {str(e)}")
return Response(
{'error': 'Logout failed'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@extend_schema(
summary="Change Password",
description="Change user password",
responses={200: {'type': 'object', 'properties': {'message': {'type': 'string'}}}},
)
@action(detail=False, methods=['post'])
def change_password(self, request: Request) -> Response:
"""
Change user password.
"""
if not request.user.is_authenticated:
return Response(
{'error': 'Authentication required'},
status=status.HTTP_401_UNAUTHORIZED
)
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
user = request.user
current_password = serializer.validated_data['current_password']
new_password = serializer.validated_data['new_password']
if not user.check_password(current_password):
return Response(
{'error': 'Current password is incorrect'},
status=status.HTTP_400_BAD_REQUEST
)
user.set_password(new_password)
user.save(update_fields=['password'])
# Blacklist all existing tokens
jwt_service.blacklist_token('', blacklist_all_sessions=True)
logger.info(f"Password changed for user {user.id}")
return Response({'message': 'Password changed successfully'}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Password change error: {str(e)}")
return Response(
{'error': 'Password change failed'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@extend_schema(
summary="Reset Password",
description="Request password reset",
responses={200: {'type': 'object', 'properties': {'message': {'type': 'string'}}}},
)
@action(detail=False, methods=['post'])
def reset_password(self, request: Request) -> Response:
"""
Request password reset email.
"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
email = serializer.validated_data['email']
try:
user = User.objects.get(email__iexact=email)
except User.DoesNotExist:
# Don't reveal whether email exists
return Response({'message': 'Password reset email sent if email exists'})
# Generate password reset token
token = secrets.token_urlsafe(32)
reset_key = f"password_reset:{token}"
cache.set(reset_key, str(user.id), timeout=3600) # 1 hour
# Send password reset email
# This would integrate with email service
logger.info(f"Password reset requested for user {user.id}")
return Response({'message': 'Password reset email sent if email exists'})
except Exception as e:
logger.error(f"Password reset request error: {str(e)}")
return Response(
{'error': 'Password reset request failed'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@extend_schema(
summary="Confirm Password Reset",
description="Confirm password reset with token",
responses={200: {'type': 'object', 'properties': {'message': {'type': 'string'}}}},
)
@action(detail=False, methods=['post'])
def confirm_password_reset(self, request: Request) -> Response:
"""
Confirm password reset with token.
"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
token = serializer.validated_data['token']
new_password = serializer.validated_data['new_password']
# Verify token
reset_key = f"password_reset:{token}"
user_id = cache.get(reset_key)
if not user_id:
return Response(
{'error': 'Invalid or expired token'},
status=status.HTTP_400_BAD_REQUEST
)
try:
user = User.objects.get(id=user_id)
except User.DoesNotExist:
return Response(
{'error': 'User not found'},
status=status.HTTP_404_NOT_FOUND
)
# Update password
user.set_password(new_password)
user.save(update_fields=['password'])
# Clear reset token
cache.delete(reset_key)
# Blacklist all existing tokens
jwt_service.blacklist_token('', blacklist_all_sessions=True)
logger.info(f"Password reset completed for user {user.id}")
return Response({'message': 'Password reset successfully'}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Password reset confirmation error: {str(e)}")
return Response(
{'error': 'Password reset failed'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@extend_schema(
summary="Magic Link",
description="Generate magic link for email authentication",
responses={200: {'type': 'object', 'properties': {'token': {'type': 'string'}}}},
)
@action(detail=False, methods=['post'])
def magic_link(self, request: Request) -> Response:
"""
Generate magic link for email authentication.
"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
email = serializer.validated_data['email']
tenant = getattr(request, 'tenant', None)
token = auth_backend.generate_magic_link(email, tenant)
# Send magic link email
# This would integrate with email service
logger.info(f"Magic link generated for {email}")
return Response({'token': token}, status=status.HTTP_200_OK)
except AuthenticationError as e:
return Response(
{'error': str(e)},
status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
logger.error(f"Magic link generation error: {str(e)}")
return Response(
{'error': 'Magic link generation failed'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@extend_schema(
summary="Social Authentication",
description="Authenticate with social providers",
responses={200: AuthStatusSerializer},
)
@action(detail=False, methods=['post'])
def social_auth(self, request: Request) -> Response:
"""
Authenticate with social providers (Google, Facebook).
"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
provider = serializer.validated_data['provider']
access_token = serializer.validated_data['access_token']
# Map provider to authentication method
method_map = {
'google': 'google',
'facebook': 'facebook',
}
method = method_map.get(provider)
if not method:
return Response(
{'error': f'Provider {provider} not supported'},
status=status.HTTP_400_BAD_REQUEST
)
# Authenticate with social provider
credentials = {'access_token': access_token}
if 'id_token' in serializer.validated_data:
credentials['id_token'] = serializer.validated_data['id_token']
user = auth_backend.authenticate(request, method=method, **credentials)
if not user:
return Response(
{'error': 'Social authentication failed'},
status=status.HTTP_401_UNAUTHORIZED
)
# Generate JWT tokens
tenant = getattr(request, 'tenant', None) or getattr(user, 'tenant', None)
device_info = {
'user_agent': request.META.get('HTTP_USER_AGENT', ''),
'ip_address': request.META.get('REMOTE_ADDR', ''),
'device_type': self._get_device_type(request),
}
tokens = jwt_service.generate_token_pair(user, tenant, device_info)
# Update user last login
user.last_login = timezone.now()
user.save(update_fields=['last_login'])
logger.info(f"User {user.id} logged in via {provider}")
return Response({
'user': self._serialize_user(user),
'tokens': tokens,
'mfa_status': mfa_service.get_mfa_status(user),
}, status=status.HTTP_200_OK)
except AuthenticationError as e:
return Response(
{'error': str(e)},
status=status.HTTP_401_UNAUTHORIZED
)
except Exception as e:
logger.error(f"Social authentication error: {str(e)}")
return Response(
{'error': 'Social authentication failed'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@extend_schema(
summary="Biometric Authentication",
description="Authenticate with biometric data",
responses={200: AuthStatusSerializer},
)
@action(detail=False, methods=['post'])
def biometric_auth(self, request: Request) -> Response:
"""
Authenticate with biometric data.
"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
user_id = serializer.validated_data['user_id']
biometric_token = serializer.validated_data['biometric_token']
user = auth_backend.authenticate(
request,
method='biometric',
user_id=user_id,
biometric_token=biometric_token
)
if not user:
return Response(
{'error': 'Biometric authentication failed'},
status=status.HTTP_401_UNAUTHORIZED
)
# Generate JWT tokens
tenant = getattr(request, 'tenant', None) or getattr(user, 'tenant', None)
device_info = {
'user_agent': request.META.get('HTTP_USER_AGENT', ''),
'ip_address': request.META.get('REMOTE_ADDR', ''),
'device_type': self._get_device_type(request),
}
tokens = jwt_service.generate_token_pair(user, tenant, device_info)
# Update user last login
user.last_login = timezone.now()
user.save(update_fields=['last_login'])
logger.info(f"User {user.id} logged in via biometric authentication")
return Response({
'user': self._serialize_user(user),
'tokens': tokens,
'mfa_status': mfa_service.get_mfa_status(user),
}, status=status.HTTP_200_OK)
except AuthenticationError as e:
return Response(
{'error': str(e)},
status=status.HTTP_401_UNAUTHORIZED
)
except Exception as e:
logger.error(f"Biometric authentication error: {str(e)}")
return Response(
{'error': 'Biometric authentication failed'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@extend_schema(
summary="MFA Status",
description="Get MFA status for current user",
responses={200: {'type': 'object'}},
)
@action(detail=False, methods=['get'])
def mfa_status(self, request: Request) -> Response:
"""
Get MFA status for current user.
"""
if not request.user.is_authenticated:
return Response(
{'error': 'Authentication required'},
status=status.HTTP_401_UNAUTHORIZED
)
try:
mfa_status = mfa_service.get_mfa_status(request.user)
return Response(mfa_status, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"MFA status error: {str(e)}")
return Response(
{'error': 'Failed to get MFA status'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@extend_schema(
summary="Backup Codes",
description="Generate new backup codes",
responses={200: BackupCodeSerializer},
)
@action(detail=False, methods=['post'])
def backup_codes(self, request: Request) -> Response:
"""
Generate new backup codes.
"""
if not request.user.is_authenticated:
return Response(
{'error': 'Authentication required'},
status=status.HTTP_401_UNAUTHORIZED
)
try:
backup_codes = mfa_service.generate_backup_codes(request.user)
return Response({'backup_codes': backup_codes}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Backup codes generation error: {str(e)}")
return Response(
{'error': 'Failed to generate backup codes'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@extend_schema(
summary="Disable MFA",
description="Disable MFA for current user",
responses={200: {'type': 'object', 'properties': {'message': {'type': 'string'}}}},
)
@action(detail=False, methods=['post'])
def disable_mfa(self, request: Request) -> Response:
"""
Disable MFA for current user.
"""
if not request.user.is_authenticated:
return Response(
{'error': 'Authentication required'},
status=status.HTTP_401_UNAUTHORIZED
)
try:
if mfa_service.disable_mfa(request.user):
return Response({'message': 'MFA disabled successfully'}, status=status.HTTP_200_OK)
else:
return Response(
{'error': 'Failed to disable MFA'},
status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
logger.error(f"MFA disable error: {str(e)}")
return Response(
{'error': 'Failed to disable MFA'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# Helper methods
def _serialize_user(self, user: User) -> Dict[str, Any]:
"""Serialize user data for API response."""
return {
'id': str(user.id),
'email': user.email,
'first_name': user.first_name,
'last_name': user.last_name,
'role': user.role,
'is_active': user.is_active,
'email_verified': user.email_verified,
'phone_number': user.phone_number,
'phone_verified': user.phone_verified,
'malaysian_ic': user.malaysian_ic,
'tenant_id': str(user.tenant.id) if user.tenant else None,
'last_login': user.last_login,
'created_at': user.created_at,
'updated_at': user.updated_at,
}
def _get_device_type(self, request: Request) -> str:
"""Detect device type from user agent."""
user_agent = request.META.get('HTTP_USER_AGENT', '').lower()
if 'mobile' in user_agent:
return 'mobile'
elif 'tablet' in user_agent:
return 'tablet'
elif 'desktop' in user_agent:
return 'desktop'
else:
return 'unknown'
# View functions for specific endpoints
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def auth_status(request: Request) -> Response:
"""
Get authentication status for current user.
"""
try:
user = request.user
return Response({
'authenticated': True,
'user': {
'id': str(user.id),
'email': user.email,
'first_name': user.first_name,
'last_name': user.last_name,
'role': user.role,
'tenant_id': str(user.tenant.id) if user.tenant else None,
},
'mfa_status': mfa_service.get_mfa_status(user),
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Auth status error: {str(e)}")
return Response(
{'error': 'Failed to get auth status'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['POST'])
@permission_classes([AllowAny])
def verify_email(request: Request) -> Response:
"""
Verify email address with OTP code.
"""
try:
email = request.data.get('email')
code = request.data.get('code')
if not email or not code:
return Response(
{'error': 'Email and code are required'},
status=status.HTTP_400_BAD_REQUEST
)
# Verify email OTP
if not auth_backend.verify_registration_otp(email, email_otp=code):
return Response(
{'error': 'Invalid verification code'},
status=status.HTTP_400_BAD_REQUEST
)
# Update user email verification status
try:
user = User.objects.get(email__iexact=email)
user.email_verified = True
user.save(update_fields=['email_verified'])
except User.DoesNotExist:
pass
return Response({'message': 'Email verified successfully'}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Email verification error: {str(e)}")
return Response(
{'error': 'Email verification failed'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['POST'])
@permission_classes([AllowAny])
def verify_phone(request: Request) -> Response:
"""
Verify phone number with OTP code.
"""
try:
phone_number = request.data.get('phone_number')
code = request.data.get('code')
if not phone_number or not code:
return Response(
{'error': 'Phone number and code are required'},
status=status.HTTP_400_BAD_REQUEST
)
# Verify phone OTP
if not auth_backend.verify_registration_otp('', phone_otp=code, phone_number=phone_number):
return Response(
{'error': 'Invalid verification code'},
status=status.HTTP_400_BAD_REQUEST
)
# Update user phone verification status
try:
user = User.objects.get(phone_number=phone_number)
user.phone_verified = True
user.save(update_fields=['phone_verified'])
except User.DoesNotExist:
pass
return Response({'message': 'Phone number verified successfully'}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Phone verification error: {str(e)}")
return Response(
{'error': 'Phone verification failed'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)