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)