Source code for users.views

import json

import cloudinary.uploader
from django.contrib.auth import login, logout
from django.http import JsonResponse
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods

from . import services
from .email_utils import send_password_reset_email, send_verification_email
from .models import User
from .serializers import serialize_user, serialize_user_with_deletion
from .services import UserServiceError


[docs] @csrf_exempt @require_http_methods(["POST"]) def register(request): """ Register a new user account. Parses ``email``, ``password``, and optional ``role`` from the request body, delegates creation to :func:`services.register_user`, and returns the new account's identifiers. A verification email is dispatched as a side-effect of the service call; the account cannot be used until the email address is confirmed. :param request: The incoming HTTP POST request containing a JSON body with ``email``, ``password``, and optionally ``role``. :type request: django.http.HttpRequest :returns: JSON response with ``userID``, ``email``, ``role``, and a reminder to verify the account (HTTP 201), or an error payload (HTTP 400). :rtype: django.http.JsonResponse :raises UserServiceError: Caught internally; returns the error message and its associated HTTP status code. **Request body** (JSON):: { "email": "jdoe@ucla.edu", "password": "secret123", "role": "MENTEE" // optional, defaults to "MENTEE" } **Success response** (HTTP 201):: { "message": "Registration successful. Please check your email to verify account.", "userID": "550e8400-e29b-...", "email": "jdoe@ucla.edu", "role": "MENTEE", "note": "You must verify your email before logging in." } """ try: data = json.loads(request.body) email = data.get('email') password = data.get('password') role = data.get('role', 'MENTEE') if not email or not password: return JsonResponse({'error': 'Email and password required'}, status=400) user = services.register_user(email, password, role) return JsonResponse({ 'message': 'Registation successful. Please check your email to verify account.', 'userID': str(user.userID), 'email': user.email, 'role': user.role, 'note': 'You must verify your email before logging in.' }, status=201) except UserServiceError as exc: return JsonResponse({'error': str(exc)}, status=exc.status) except Exception as exc: return JsonResponse({'error': str(exc)}, status=400)
[docs] @csrf_exempt @require_http_methods(["POST"]) def user_login(request): """ Authenticate a user and open a session. Validates credentials via :func:`services.authenticate_user`, enforces email-verification and soft-deletion guards, then calls Django's ``login()`` to establish a session. Also computes ``profile_complete`` based on whether the user has filled in their core profile fields. :param request: The incoming HTTP POST request containing a JSON body with ``email`` and ``password``. :type request: django.http.HttpRequest :returns: - HTTP 200 with serialized user data and ``profile_complete`` flag on success. - HTTP 200 with deletion metadata if the account is pending deletion but still within the grace period. - HTTP 403 if the email address has not yet been verified. - HTTP 404 if the account's grace period has expired and it has been purged. - HTTP 400 for missing fields or unexpected errors. :rtype: django.http.JsonResponse :raises UserServiceError: Caught internally; returns the error message and its associated HTTP status code. **Request body** (JSON):: { "email": "jdoe@ucla.edu", "password": "secret123" } **Success response** (HTTP 200):: { "message": "Login successful", "profile_complete": true, // ...serialized user fields } """ try: data = json.loads(request.body) email = data.get('email') password = data.get('password') if not email or not password: return JsonResponse({'error': 'Email and password required'}, status=400) user = services.authenticate_user(request, email, password) if not user.is_verified: return JsonResponse({ 'error': 'Email not verified', 'message': 'Please check your email and verify your account before logging in' }, status=403) if user.is_deleted and user.permanent_deletion_date: if timezone.now() > user.permanent_deletion_date: user.delete() return JsonResponse({'error': 'Account not found'}, status=404) login(request, user) profile_complete = bool( user.firstName and user.lastName and user.major and user.year > 0 ) if user.is_deleted: return JsonResponse({ 'message': 'Login successful (account pending deletion)', **serialize_user_with_deletion(user), 'profile_complete': profile_complete, }) return JsonResponse({ 'message': 'Login successful', **serialize_user(user), 'profile_complete': profile_complete, }, status=200) except UserServiceError as exc: return JsonResponse({'error': str(exc)}, status=exc.status) except Exception as exc: return JsonResponse({'error': str(exc)}, status=400)
[docs] @csrf_exempt @require_http_methods(["POST"]) def user_logout(request): """ Terminate the current user session. Calls Django's ``logout()`` to flush the session, regardless of whether the caller is authenticated. :param request: The incoming HTTP POST request. :type request: django.http.HttpRequest :returns: JSON confirmation message (HTTP 200). :rtype: django.http.JsonResponse """ logout(request) return JsonResponse({'message': 'Logged out successfully'}, status=200)
[docs] @csrf_exempt @require_http_methods(["GET"]) def get_current_user(request): """ Return the profile of the currently authenticated user. Computes ``profile_complete`` based on whether core profile fields have been populated. Returns deletion metadata instead of the standard payload when the account is pending deletion. :param request: The incoming HTTP GET request. Must have an authenticated session. :type request: django.http.HttpRequest :returns: - HTTP 200 with serialized user data and ``profile_complete`` on success. - HTTP 200 with deletion metadata if the account is pending deletion. - HTTP 401 if the request is unauthenticated. :rtype: django.http.JsonResponse **Success response** (HTTP 200):: { "profile_complete": true, // ...serialized user fields } """ if not request.user.is_authenticated: return JsonResponse({'error': 'Not authenticated'}, status=401) profile_complete = bool( request.user.firstName and request.user.lastName and request.user.major and request.user.year > 0 ) if request.user.is_deleted: return JsonResponse({ **serialize_user_with_deletion(request.user), 'profile_complete': profile_complete, }) return JsonResponse({ **serialize_user(request.user), 'profile_complete': profile_complete, })
[docs] @csrf_exempt @require_http_methods(["PUT"]) def update_profile(request): """ Update the authenticated user's profile fields. Accepts any subset of the fields permitted by :attr:`User.SURVEY_FIELDS`. Protected fields are silently ignored by the model layer. :param request: The incoming HTTP PUT request containing a JSON body with one or more profile field key-value pairs. :type request: django.http.HttpRequest :returns: - HTTP 200 with a success message on update. - HTTP 400 if the request body is not valid JSON. - HTTP 401 if the request is unauthenticated. :rtype: django.http.JsonResponse **Request body** (JSON):: { "firstName": "Jane", "year": 2, "major": ["Computer Science"] } """ if not request.user.is_authenticated: return JsonResponse({'error': 'Authentication required'}, status=401) try: data = json.loads(request.body) except json.JSONDecodeError: return JsonResponse({'error': 'Invalid JSON'}, status=400) request.user.update_profile(**data) return JsonResponse({'message': 'Profile updated successfully'})
[docs] @csrf_exempt @require_http_methods(["POST"]) def complete_survey(request): """ Submit the authenticated user's initial onboarding survey. Delegates to :func:`services.complete_survey`, which applies the same field-whitelist rules as :func:`update_profile`. :param request: The incoming HTTP POST request containing a JSON body with survey field key-value pairs. :type request: django.http.HttpRequest :returns: - HTTP 200 with a success message on completion. - HTTP 400 for unexpected errors. - HTTP 401 if the request is unauthenticated. :rtype: django.http.JsonResponse **Request body** (JSON):: { "firstName": "Jane", "lastName": "Doe", "year": 1, "major": ["Biology"], "international": false } """ try: if not request.user.is_authenticated: return JsonResponse({'error': 'Authentication required'}, status=401) data = json.loads(request.body) services.complete_survey(request.user, data) return JsonResponse({'message': 'Survey completed successfully'}, status=200) except Exception as exc: return JsonResponse({'error': str(exc)}, status=400)
[docs] @csrf_exempt @require_http_methods(["POST"]) def request_account_deletion(request): """ Initiate a soft-delete of the authenticated user's account. Requires the caller to supply a confirmation string in the request body. The account is not immediately removed; a grace period is applied during which the user may cancel via :func:`cancel_account_deletion`. :param request: The incoming HTTP POST request containing a JSON body with a ``confirmation`` string. :type request: django.http.HttpRequest :returns: - HTTP 200 with ``permanent_deletion_date`` on success. - HTTP 400 for unexpected errors. - HTTP 401 if the request is unauthenticated. - Appropriate error status from :exc:`UserServiceError` if the confirmation text is invalid. :rtype: django.http.JsonResponse :raises UserServiceError: Caught internally; returns the error message and its associated HTTP status code. **Request body** (JSON):: { "confirmation": "delete my account" } **Success response** (HTTP 200):: { "message": "Account deletion requested", "permanent_deletion_date": "2024-01-01T12:00:00+00:00", "note": "Your account will be permanently deleted in 1 hour. You can cancel before then." } """ if not request.user.is_authenticated: return JsonResponse({'error': 'Authentication required'}, status=401) try: data = json.loads(request.body) confirmation_text = data.get('confirmation', '').strip() services.request_deletion(request.user, confirmation_text) return JsonResponse({ 'message': 'Account deletion requested', 'permanent_deletion_date': request.user.permanent_deletion_date.isoformat(), 'note': 'Your account will be permanently deleted in 1 hour. You can cancel before then.', }, status=200) except UserServiceError as exc: return JsonResponse({'error': str(exc)}, status=exc.status) except Exception as exc: return JsonResponse({'error': str(exc)}, status=400)
[docs] @csrf_exempt @require_http_methods(["POST"]) def cancel_account_deletion(request): """ Cancel a pending account deletion for the currently authenticated user. Delegates to :func:`services.cancel_deletion`. The request will fail if the grace period has already elapsed. Use :func:`cancel_account_deletion_public` instead when the caller does not have an active session. :param request: The incoming HTTP POST request. No body is required. :type request: django.http.HttpRequest :returns: - HTTP 200 with a success message on cancellation. - HTTP 401 if the request is unauthenticated. - Appropriate error status from :exc:`UserServiceError` if the grace period has expired. :rtype: django.http.JsonResponse :raises UserServiceError: Caught internally; returns the error message and its associated HTTP status code. """ if not request.user.is_authenticated: return JsonResponse({'error': 'Authentication required'}, status=401) try: services.cancel_deletion(request.user) return JsonResponse({'message': 'Account deletion cancelled successfully'}, status=200) except UserServiceError as exc: return JsonResponse({'error': str(exc)}, status=exc.status)
[docs] @csrf_exempt @require_http_methods(["POST"]) def cancel_account_deletion_public(request): """ Cancel a pending account deletion without an active session. Accepts credentials in the request body and authenticates the user before delegating to :func:`services.cancel_deletion_by_credentials`. Intended for users who have been logged out but still want to recover their account within the grace period. :param request: The incoming HTTP POST request containing a JSON body with ``email`` and ``password``. :type request: django.http.HttpRequest :returns: - HTTP 200 with ``email`` and ``is_active`` on success. - HTTP 400 if ``email`` or ``password`` is missing, or for unexpected errors. - Appropriate error status from :exc:`UserServiceError` on auth failure or expired grace period. :rtype: django.http.JsonResponse :raises UserServiceError: Caught internally; returns the error message and its associated HTTP status code. **Request body** (JSON):: { "email": "jdoe@ucla.edu", "password": "secret123" } """ try: data = json.loads(request.body) email = data.get('email') password = data.get('password') if not email or not password: return JsonResponse({'error': 'Email and password required'}, status=400) user = services.cancel_deletion_by_credentials(request, email, password) return JsonResponse({ 'message': 'Account deletion cancelled successfully', 'email': user.email, 'is_active': user.is_active, }, status=200) except UserServiceError as exc: return JsonResponse({'error': str(exc)}, status=exc.status) except Exception as exc: return JsonResponse({'error': str(exc)}, status=400)
[docs] @csrf_exempt @require_http_methods(["GET"]) def verify_email(request, token): """ Verify a user's email address using a one-time token. Looks up the ``User`` whose ``email_verification_token`` matches the supplied ``token`` path parameter, then delegates to :meth:`User.verify_email`. Returns an appropriate response if the account is already verified or if the token has expired. :param request: The incoming HTTP GET request. :type request: django.http.HttpRequest :param token: The UUID verification token embedded in the confirmation link. :type token: str :returns: - HTTP 200 with a success message and ``email`` on successful verification. - HTTP 200 with ``'Email already verified'`` if the account was already confirmed. - HTTP 400 if the token is invalid or has expired. :rtype: django.http.JsonResponse **Success response** (HTTP 200):: { "message": "Email verified successfully. You can now log in.", "email": "jdoe@ucla.edu" } """ try: user = User.objects.filter(email_verification_token=token).first() if not user: return JsonResponse({'error': 'Invalid verification token'}, status=400) if user.is_verified: return JsonResponse({ 'message': 'Email already verified', 'email': user.email }, status=200) success = user.verify_email(token) if success: return JsonResponse({ 'message': 'Email verified successfully. You can now log in.', 'email': user.email }, status=200) else: return JsonResponse({ 'error': 'Verification token expired', 'message': 'Please request a new verification email' }, status=400) except Exception as e: return JsonResponse({'error': str(e)}, status=400)
[docs] @csrf_exempt @require_http_methods(["POST"]) def resend_verification(request): """ Resend the email verification link to a user. Looks up the account by ``email`` and issues a new verification token via :meth:`User.generate_verification_token`. To prevent email enumeration, the response is identical whether or not the address exists in the system. :param request: The incoming HTTP POST request containing a JSON body with ``email``. :type request: django.http.HttpRequest :returns: - HTTP 200 with ``'Verification email sent'`` on dispatch. - HTTP 200 with a non-committal message if the email is not found (prevents enumeration). - HTTP 200 with ``'Email already verified'`` if the account is already confirmed. - HTTP 400 if ``email`` is missing or an unexpected error occurs. :rtype: django.http.JsonResponse **Request body** (JSON):: { "email": "jdoe@ucla.edu" } """ try: data = json.loads(request.body) email = data.get('email') if not email: return JsonResponse({'error': 'Email required'}, status=400) user = User.objects.filter(email=email).first() if not user: return JsonResponse({ 'message': 'If that email exists, a verification link has been sent' }, status=200) if user.is_verified: return JsonResponse({'message': 'Email already verified'}, status=200) user.generate_verification_token() send_verification_email(user) return JsonResponse({'message': 'Verification email sent'}, status=200) except Exception as e: return JsonResponse({'error': str(e)}, status=400)
[docs] @csrf_exempt @require_http_methods(["POST"]) def change_role(request): """ Switch the authenticated user's role between ``MENTEE`` and ``MENTOR``. All existing matches, compatibility scores, and chat messages are cleared before the role transition is applied (delegated to :meth:`User.change_role`). :param request: The incoming HTTP POST request containing a JSON body with ``new_role``. :type request: django.http.HttpRequest :returns: - HTTP 200 with a success message and serialized user data on success. - HTTP 400 if ``new_role`` is missing, the JSON is invalid, or the role transition is not permitted. - HTTP 401 if the request is unauthenticated. :rtype: django.http.JsonResponse **Request body** (JSON):: { "new_role": "MENTOR" } """ if not request.user.is_authenticated: return JsonResponse({'error': 'Authentication required'}, status=401) try: data = json.loads(request.body) except json.JSONDecodeError: return JsonResponse({'error': 'Invalid JSON'}, status=400) new_role = data.get('new_role', '').strip().upper() if not new_role: return JsonResponse({'error': 'new_role is required'}, status=400) try: request.user.change_role(new_role) except ValueError as exc: return JsonResponse({'error': str(exc)}, status=400) return JsonResponse({ 'message': 'Role changed successfully', **serialize_user(request.user), })
[docs] @csrf_exempt @require_http_methods(["POST"]) def upload_profile_picture(request): """ Upload and store a profile picture for the authenticated user. Sends the supplied file to Cloudinary with a 400×400 face-crop transformation and returns the resulting secure URL. The caller is responsible for subsequently persisting the URL to the user's profile via :func:`update_profile`. :param request: The incoming HTTP POST request with a ``multipart/form-data`` body containing a ``file`` field. :type request: django.http.HttpRequest :returns: - HTTP 200 with ``url`` pointing to the uploaded image on success. - HTTP 400 if no file is provided. - HTTP 401 if the request is unauthenticated. :rtype: django.http.JsonResponse **Success response** (HTTP 200):: { "url": "https://res.cloudinary.com/.../profile_pictures/abc123.jpg" } """ if not request.user.is_authenticated: return JsonResponse({'error': 'Not authenticated'}, status=401) file = request.FILES.get('file') if not file: return JsonResponse({'error': 'No file provided'}, status=400) result = cloudinary.uploader.upload( file, folder="profile_pictures", transformation=[ {"width": 400, "height": 400, "crop": "fill", "gravity": "face"} ] ) return JsonResponse({'url': result['secure_url']})
[docs] @csrf_exempt @require_http_methods(["POST"]) def request_password_reset(request): """ Dispatch a password reset email to the specified address. Generates a one-time token via :meth:`User.generate_password_reset_token` and sends it via :func:`send_password_reset_email`. To prevent email enumeration, the response is identical whether or not the address exists. :param request: The incoming HTTP POST request containing a JSON body with ``email``. :type request: django.http.HttpRequest :returns: - HTTP 200 with a non-committal message regardless of whether the email exists (prevents enumeration). - HTTP 400 if ``email`` is missing or an unexpected error occurs. :rtype: django.http.JsonResponse **Request body** (JSON):: { "email": "jdoe@ucla.edu" } """ try: data = json.loads(request.body) email = data.get('email') if not email: return JsonResponse({'error': 'Email required'}, status=400) user = User.objects.filter(email=email).first() if not user: return JsonResponse({ 'message': 'If that email exists, a password reset link has been sent' }, status=200) user.generate_password_reset_token() send_password_reset_email(user) return JsonResponse({ 'message': 'If that email exists, a password reset link has been sent' }, status=200) except Exception as e: return JsonResponse({'error': str(e)}, status=400)
[docs] @csrf_exempt @require_http_methods(["POST"]) def reset_password(request, token): """ Reset a user's password using a one-time token. Looks up the ``User`` whose ``password_reset_token`` matches the supplied ``token`` path parameter, then delegates to :meth:`User.reset_password_with_token`. :param request: The incoming HTTP POST request containing a JSON body with ``new_password``. :type request: django.http.HttpRequest :param token: The UUID password reset token embedded in the reset link. :type token: str :returns: - HTTP 200 with a success message on reset. - HTTP 400 if ``new_password`` is missing, the token is invalid, the token has expired, or an unexpected error occurs. :rtype: django.http.JsonResponse **Request body** (JSON):: { "new_password": "newSecurePass!" } **Success response** (HTTP 200):: { "message": "Password reset successfully. You can now log in." } """ try: data = json.loads(request.body) new_password = data.get('new_password') if not new_password: return JsonResponse({'error': 'New password required'}, status=400) user = User.objects.filter(password_reset_token=token).first() if not user: return JsonResponse({'error': 'Invalid reset token'}, status=400) success = user.reset_password_with_token(token, new_password) if success: return JsonResponse({ 'message': 'Password reset successfully. You can now log in.' }, status=200) else: return JsonResponse({ 'error': 'Reset token expired', 'message': 'Please request a new password reset' }, status=400) except Exception as e: return JsonResponse({'error': str(e)}, status=400)