alternative-backend-service/users/views.py
saani f06b5120e9 feat: add HIPAA-compliant email and OTP authentication system
Add comprehensive HIPAA compliance features and OTP-based authentication:

- Configure HIPAA email settings with AES-256 encryption standard
- Add secure portal URL and BAA verification configuration
- Implement OTP verification for user registration and password reset
- Add user model fields for email verification and password reset OTPs
- Configure templates directory in Django settings
- Add authentication flow endpoints with detailed documentation
- Update dependencies to support new security features
- Reorganize .gitignore for better structure

These changes ensure HIPAA compliance for healthcare data handling
with 6-year audit retention, secure email communications, and
multi-factor authentication capabilities.
2025-11-22 02:19:44 +00:00

538 lines
19 KiB
Python

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