from rest_framework import status, generics from rest_framework.decorators import api_view, permission_classes from rest_framework.response import Response from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework_simplejwt.tokens import RefreshToken from django.contrib.auth import authenticate from .models import CustomUser, UserProfile from .serializers import UserRegistrationSerializer, UserSerializer, ResetPasswordSerializer, ForgotPasswordSerializer, VerifyPasswordResetOTPSerializer from .utils import send_otp_via_email, is_otp_expired, generate_otp from django.utils import timezone from datetime import timedelta from rest_framework.reverse import reverse @api_view(['GET']) @permission_classes([AllowAny]) def api_root(request, format=None): """ # Authentication API Documentation Welcome to the Authentication API. This service provides complete user authentication functionality including registration, email verification, login, and password reset using OTP. ## Base URL ``` {{ request.build_absolute_uri }} ``` ## Quick Start 1. **Register** a new user account 2. **Verify** email with OTP sent to your email 3. **Login** with your credentials 4. Use the **access token** for authenticated requests ## API Endpoints """ endpoints = { 'documentation': { 'description': 'This API documentation', 'url': request.build_absolute_uri(), 'methods': ['GET'] }, 'register': { 'description': 'Register a new user and send verification OTP', 'url': request.build_absolute_uri('register/'), 'methods': ['POST'], 'required_fields': ['email', 'first_name', 'last_name', 'password', 'password2'], 'example_request': { 'email': 'user@example.com', 'first_name': 'John', 'last_name': 'Doe', 'phone_number': '+1234567890', 'password': 'SecurePassword123', 'password2': 'SecurePassword123' } }, 'verify_otp': { 'description': 'Verify email address using OTP', 'url': request.build_absolute_uri('verify-otp/'), 'methods': ['POST'], 'required_fields': ['email', 'otp'], 'example_request': { 'email': 'user@example.com', 'otp': '123456' } }, 'login': { 'description': 'Authenticate user and return JWT tokens', 'url': request.build_absolute_uri('login/'), 'methods': ['POST'], 'required_fields': ['email', 'password'], 'example_request': { 'email': 'user@example.com', 'password': 'SecurePassword123' } }, 'resend_otp': { 'description': 'Resend OTP for email verification or password reset', 'url': request.build_absolute_uri('resend-otp/'), 'methods': ['POST'], 'required_fields': ['email'], 'optional_fields': ['context (registration/password_reset)'], 'example_request': { 'email': 'user@example.com', 'context': 'registration' } }, 'forgot_password': { 'description': 'Initiate password reset process', 'url': request.build_absolute_uri('forgot-password/'), 'methods': ['POST'], 'required_fields': ['email'], 'example_request': { 'email': 'user@example.com' } }, 'verify_password_reset_otp': { 'description': 'Verify OTP for password reset', 'url': request.build_absolute_uri('verify-password-reset-otp/'), 'methods': ['POST'], 'required_fields': ['email', 'otp'], 'example_request': { 'email': 'user@example.com', 'otp': '123456' } }, 'reset_password': { 'description': 'Reset password after OTP verification', 'url': request.build_absolute_uri('reset-password/'), 'methods': ['POST'], 'required_fields': ['email', 'otp', 'new_password', 'confirm_password'], 'example_request': { 'email': 'user@example.com', 'otp': '123456', 'new_password': 'NewSecurePassword123', 'confirm_password': 'NewSecurePassword123' } }, 'token_refresh': { 'description': 'Refresh access token using refresh token', 'url': request.build_absolute_uri('token/refresh/'), 'methods': ['POST'], 'required_fields': ['refresh'], 'example_request': { 'refresh': 'your_refresh_token_here' } } } return Response({ 'message': 'Authentication API', 'version': '1.0.0', 'endpoints': endpoints, 'authentication_flows': { 'registration_flow': [ '1. POST /register/ - Register user and send OTP', '2. POST /verify-otp/ - Verify email with OTP', '3. POST /login/ - Login with credentials' ], 'password_reset_flow': [ '1. POST /forgot-password/ - Request password reset OTP', '2. POST /verify-password-reset-otp/ - Verify OTP', '3. POST /reset-password/ - Set new password' ], 'login_flow_unverified': [ '1. POST /login/ - Returns email not verified error', '2. POST /resend-otp/ - Resend verification OTP', '3. POST /verify-otp/ - Verify email', '4. POST /login/ - Successful login' ] }, 'specifications': { 'otp': { 'length': 6, 'expiry_minutes': 10, 'delivery_method': 'email' }, 'tokens': { 'access_token_lifetime': '5 minutes', 'refresh_token_lifetime': '24 hours' }, 'password_requirements': [ 'Minimum 8 characters', 'Cannot be entirely numeric', 'Cannot be too common', 'Should include uppercase, lowercase, and numbers' ] }, 'error_handling': { 'common_status_codes': { '200': 'Success', '201': 'Created', '400': 'Bad Request (validation errors)', '401': 'Unauthorized (invalid credentials)', '403': 'Forbidden (unverified email, inactive account)', '404': 'Not Found', '500': 'Internal Server Error' }, 'error_response_format': { 'error': 'Error description', 'message': 'User-friendly message' } }, 'security_notes': [ 'Always use HTTPS in production', 'Store tokens securely (httpOnly cookies recommended)', 'Implement token refresh logic', 'Validate all inputs on frontend and backend', 'Handle token expiration gracefully' ] }) @api_view(['POST']) @permission_classes([AllowAny]) def register_user(request): serializer = UserRegistrationSerializer(data=request.data) if serializer.is_valid(): user = serializer.save() UserProfile.objects.create(user=user) otp = generate_otp() user.verify_otp = otp user.verify_otp_expiry = timezone.now() + timedelta(minutes=10) user.save() user_name = f"{user.first_name} {user.last_name}".strip() or user.email email_sent = send_otp_via_email(user.email, otp, user_name, 'registration') if not email_sent: return Response({ 'error': 'Failed to send OTP. Please try again later.' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) return Response({ 'user': UserSerializer(user).data, 'otp_sent': email_sent, 'otp_expires_in': 10 }, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @api_view(['POST']) @permission_classes([AllowAny]) def verify_otp(request): email = request.data.get('email') otp = request.data.get('otp') if not email or not otp: return Response({ 'error': 'Email and OTP are required' }, status=status.HTTP_400_BAD_REQUEST) try: user = CustomUser.objects.get(email=email) if user.isVerified: return Response({ 'error': 'User is already verified' }, status=status.HTTP_400_BAD_REQUEST) if (user.verify_otp == otp and not is_otp_expired(user.verify_otp_expiry)): user.isVerified = True user.verify_otp = None user.verify_otp_expiry = None user.save() refresh = RefreshToken.for_user(user) return Response({ 'message': 'Email verified successfully', 'verified': True, }, status=status.HTTP_200_OK) else: return Response({ 'error': 'Invalid or expired OTP' }, status=status.HTTP_400_BAD_REQUEST) except CustomUser.DoesNotExist: return Response({ 'error': 'User not found' }, status=status.HTTP_404_NOT_FOUND) @api_view(['POST']) @permission_classes([AllowAny]) def resend_otp(request): email = request.data.get('email') context = request.data.get('context', 'registration') if not email: return Response({ 'error': 'Email is required' }, status=status.HTTP_400_BAD_REQUEST) try: user = CustomUser.objects.get(email=email) if user.isVerified and context == 'registration': return Response({ 'error': 'Already verified', 'message': 'Your email is already verified. You can login now.' }, status=status.HTTP_400_BAD_REQUEST) otp = generate_otp() if context == 'password_reset': user.forgot_password_otp = otp user.forgot_password_otp_expiry = timezone.now() + timedelta(minutes=10) else: user.verify_otp = otp user.verify_otp_expiry = timezone.now() + timedelta(minutes=10) user.save() user_name = f"{user.first_name} {user.last_name}".strip() or user.email email_sent = send_otp_via_email(user.email, otp, user_name, context) if not email_sent: return Response({ 'error': 'Failed to send OTP', 'message': 'Please try again later.' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) return Response({ 'message': f'OTP resent to your email successfully', 'otp_sent': email_sent, 'otp_expires_in': 10, 'context': context }, status=status.HTTP_200_OK) except CustomUser.DoesNotExist: return Response({ 'error': 'User not found', 'message': 'No account found with this email address.' }, status=status.HTTP_404_NOT_FOUND) @api_view(['POST']) @permission_classes([AllowAny]) def login_user(request): email = request.data.get('email') password = request.data.get('password') user = authenticate(request, email=email, password=password) if user is not None: if not user.isVerified: return Response({ 'error': 'Email not verified', 'message': 'Please verify your email address before logging in.', 'email': user.email, 'can_resend_otp': True }, status=status.HTTP_403_FORBIDDEN) if not user.is_active: return Response({ 'error': 'Account deactivated', 'message': 'Your account has been deactivated. Please contact support.' }, status=status.HTTP_403_FORBIDDEN) refresh = RefreshToken.for_user(user) return Response({ 'user': UserSerializer(user).data, 'refresh': str(refresh), 'access': str(refresh.access_token), 'message': 'Login successful' }) else: return Response( {'error': 'Invalid credentials'}, status=status.HTTP_401_UNAUTHORIZED ) @api_view(['POST']) @permission_classes([AllowAny]) def forgot_password(request): serializer = ForgotPasswordSerializer(data=request.data) if serializer.is_valid(): email = serializer.validated_data['email'] try: user = CustomUser.objects.get(email=email) if not user.isVerified: return Response({ 'error': 'Email not verified', 'message': 'Please verify your email address first.' }, status=status.HTTP_400_BAD_REQUEST) if not user.is_active: return Response({ 'error': 'Account deactivated', 'message': 'Your account has been deactivated.' }, status=status.HTTP_400_BAD_REQUEST) otp = generate_otp() user.forgot_password_otp = otp user.forgot_password_otp_expiry = timezone.now() + timedelta(minutes=10) user.save() user_name = f"{user.first_name} {user.last_name}".strip() or user.email email_sent = send_otp_via_email(user.email, otp, user_name, 'password_reset') if not email_sent: return Response({ 'error': 'Failed to send OTP', 'message': 'Please try again later.' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) return Response({ 'message': 'Password reset OTP sent to your email', 'otp_sent': True, 'otp_expires_in': 10, 'email': user.email }, status=status.HTTP_200_OK) except CustomUser.DoesNotExist: return Response({ 'message': 'If the email exists, a password reset OTP has been sent.' }, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @api_view(['POST']) @permission_classes([AllowAny]) def verify_password_reset_otp(request): serializer = VerifyPasswordResetOTPSerializer(data=request.data) if serializer.is_valid(): email = serializer.validated_data['email'] otp = serializer.validated_data['otp'] try: user = CustomUser.objects.get(email=email) if (user.forgot_password_otp == otp and not is_otp_expired(user.forgot_password_otp_expiry)): return Response({ 'message': 'OTP verified successfully', 'verified': True, 'email': user.email }, status=status.HTTP_200_OK) else: return Response({ 'error': 'Invalid or expired OTP' }, status=status.HTTP_400_BAD_REQUEST) except CustomUser.DoesNotExist: return Response({ 'error': 'User not found' }, status=status.HTTP_404_NOT_FOUND) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @api_view(['POST']) @permission_classes([AllowAny]) def reset_password(request): serializer = ResetPasswordSerializer(data=request.data) if serializer.is_valid(): email = serializer.validated_data['email'] otp = serializer.validated_data['otp'] new_password = serializer.validated_data['new_password'] try: user = CustomUser.objects.get(email=email) if (user.forgot_password_otp == otp and not is_otp_expired(user.forgot_password_otp_expiry)): # Set new password user.set_password(new_password) user.forgot_password_otp = None user.forgot_password_otp_expiry = None user.save() return Response({ 'message': 'Password reset successfully', 'success': True }, status=status.HTTP_200_OK) else: return Response({ 'error': 'Invalid or expired OTP' }, status=status.HTTP_400_BAD_REQUEST) except CustomUser.DoesNotExist: return Response({ 'error': 'User not found' }, status=status.HTTP_404_NOT_FOUND) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @api_view(['POST']) @permission_classes([AllowAny]) def resend_password_reset_otp(request): serializer = ForgotPasswordSerializer(data=request.data) if serializer.is_valid(): email = serializer.validated_data['email'] try: user = CustomUser.objects.get(email=email) otp = generate_otp() user.forgot_password_otp = otp user.forgot_password_otp_expiry = timezone.now() + timedelta(minutes=10) user.save() user_name = f"{user.first_name} {user.last_name}".strip() or user.email email_sent = send_otp_via_email(user.email, otp, user_name, 'password_reset') if not email_sent: return Response({ 'error': 'Failed to send OTP', 'message': 'Please try again later.' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) return Response({ 'message': 'Password reset OTP resent to your email', 'otp_sent': True, 'otp_expires_in': 10 }, status=status.HTTP_200_OK) except CustomUser.DoesNotExist: return Response({ 'message': 'If the email exists, a password reset OTP has been sent.' }, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @api_view(['GET']) @permission_classes([IsAuthenticated]) def get_user_profile(request): serializer = UserSerializer(request.user) return Response(serializer.data) @api_view(['PUT']) @permission_classes([IsAuthenticated]) def update_user_profile(request): serializer = UserSerializer(request.user, data=request.data, partial=True) if serializer.is_valid(): serializer.save() return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class UserDetailView(generics.RetrieveAPIView): serializer_class = UserSerializer permission_classes = [IsAuthenticated] def get_object(self): return self.request.user