# accounts/views.py import json import secrets from django.views.decorators.csrf import csrf_exempt from django.http import JsonResponse from django.utils.decorators import method_decorator from django.views import View from rest_framework.views import APIView from rest_framework.authtoken.models import Token from mobile_api.forms import RegisterForm, LoginForm, WebRegisterForm from rest_framework.authentication import TokenAuthentication from django.contrib.auth import logout from django.db import connection from mobile_api.utils import validate_token_and_get_user from utils.errors_json_convertor import simplify_form_errors from accounts.models import User from eventify_logger.services import log def _seed_gamification_profile(user): """Insert a gamification profile row for a newly registered user. Non-fatal: if the insert fails for any reason, registration still succeeds.""" try: with connection.cursor() as cursor: cursor.execute(""" INSERT INTO user_gamification_profiles (user_id, eventify_id) VALUES (%s, %s) ON CONFLICT (user_id) DO UPDATE SET eventify_id = COALESCE( user_gamification_profiles.eventify_id, EXCLUDED.eventify_id ) """, [user.email, user.eventify_id]) except Exception as e: log("warning", "Failed to seed gamification profile on registration", logger_data={"user": user.email, "error": str(e)}) @method_decorator(csrf_exempt, name='dispatch') class RegisterView(View): def post(self, request): try: data = json.loads(request.body) form = RegisterForm(data) if form.is_valid(): user = form.save() _seed_gamification_profile(user) token, _ = Token.objects.get_or_create(user=user) log("info", "API user registration", request=request, user=user) return JsonResponse({'message': 'User registered successfully', 'token': token.key}, status=201) log("warning", "API registration failed", request=request, logger_data=dict(errors=form.errors)) return JsonResponse({'errors': form.errors}, status=400) except Exception as e: log("error", "API registration exception", request=request, logger_data={"error": str(e)}) return JsonResponse({'error': 'An unexpected server error occurred. Please try again.'}, status=500) @method_decorator(csrf_exempt, name='dispatch') class WebRegisterView(View): def post(self, request): print('0') print('*' * 100) print(request.body) print('*' * 100) try: data = json.loads(request.body) form = WebRegisterForm(data) print('1') print('*' * 100) print(form.errors) print('*' * 100) if form.is_valid(): print('2') user = form.save() _seed_gamification_profile(user) token, _ = Token.objects.get_or_create(user=user) print('3') log("info", "Web user registration", request=request, user=user) response = { 'message': 'User registered successfully', 'token': token.key, 'username': user.username, 'email': user.email, 'phone_number': user.phone_number, 'district': user.district or '', 'district_changed_at': user.district_changed_at.isoformat() if user.district_changed_at else None, 'first_name': user.first_name, 'last_name': user.last_name, 'eventify_id': user.eventify_id or '', } return JsonResponse(response, status=201) log("warning", "Web registration failed", request=request, logger_data=dict(errors=form.errors)) return JsonResponse({'errors': form.errors}, status=400) except Exception as e: log("error", "Web registration exception", request=request, logger_data={"error": str(e)}) return JsonResponse({'error': 'An unexpected server error occurred. Please try again.'}, status=500) @method_decorator(csrf_exempt, name='dispatch') class LoginView(View): def post(self, request): print('0') try: data = json.loads(request.body) form = LoginForm(data) print('1') if form.is_valid(): print('2') user = form.cleaned_data['user'] token, _ = Token.objects.get_or_create(user=user) print('3') log("info", "API login", request=request, user=user) response = { 'message': 'Login successful', 'token': token.key, 'eventify_id': user.eventify_id, 'username': user.username, 'email': user.email, 'phone_number': user.phone_number, 'first_name': user.first_name, 'last_name': user.last_name, 'role': user.role, 'pincode': user.pincode, 'district': user.district, 'district_changed_at': user.district_changed_at.isoformat() if user.district_changed_at else None, 'state': user.state, 'country': user.country, 'place': user.place, 'latitude': user.latitude, 'longitude': user.longitude, 'profile_photo': user.profile_picture.url if user.profile_picture else '' } print('4') print(response) return JsonResponse(response, status=200) log("warning", "API login failed", request=request, logger_data=dict(errors=form.errors)) return JsonResponse(simplify_form_errors(form), status=401) except Exception as e: log("error", "API login exception", request=request, logger_data={"error": str(e)}) return JsonResponse({'error': 'An unexpected server error occurred. Please try again.'}, status=500) @method_decorator(csrf_exempt, name='dispatch') class StatusView(View): def post(self, request): try: user, token, data, error_response = validate_token_and_get_user(request, error_status_code=True) if error_response: return error_response return JsonResponse({ "status": "logged_in", "username": user.username, "email": user.email, "eventify_id": user.eventify_id or '', "district": user.district or '', "district_changed_at": user.district_changed_at.isoformat() if user.district_changed_at else None, "profile_photo": user.profile_picture.url if user.profile_picture else '', }) except Exception as e: log("error", "API status exception", request=request, logger_data={"error": str(e)}) return JsonResponse({"status": "error", "message": "An unexpected server error occurred."}, status=500) @method_decorator(csrf_exempt, name='dispatch') class LogoutView(View): def post(self, request): try: user, token, data, error_response = validate_token_and_get_user(request, error_status_code=True) if error_response: return error_response log("info", "API logout", request=request, user=user) # 🔍 Call Django's built-in logout logout(request) # 🗑 Delete the token to invalidate future access token.delete() return JsonResponse({ "status": "logged_out", "message": "Logout successful" }) except Exception as e: log("error", "API logout exception", request=request, logger_data={"error": str(e)}) return JsonResponse({"status": "error", "message": "An unexpected server error occurred."}, status=500) @method_decorator(csrf_exempt, name='dispatch') class UpdateProfileView(View): def post(self, request): try: # Authenticate user using validate_token_and_get_user user, token, data, error_response = validate_token_and_get_user(request, error_status_code=True) if error_response: # Convert error response format to match our API response format error_data = json.loads(error_response.content) return JsonResponse({ 'success': False, 'error': error_data.get('message', error_data.get('status', 'Authentication failed')) }, status=error_response.status_code) errors = {} updated_fields = [] # Get update data - handle both JSON and multipart/form-data is_multipart = request.content_type and 'multipart/form-data' in request.content_type if is_multipart: # For multipart, get data from POST (data already contains token/username from validation) json_data = request.POST.dict() else: # For JSON, use data from validate_token_and_get_user json_data = data if data else {} # Update first_name if 'first_name' in json_data: first_name = json_data.get('first_name', '').strip() if first_name: user.first_name = first_name updated_fields.append('first_name') elif first_name == '': user.first_name = '' updated_fields.append('first_name') # Update last_name if 'last_name' in json_data: last_name = json_data.get('last_name', '').strip() if last_name: user.last_name = last_name updated_fields.append('last_name') elif last_name == '': user.last_name = '' updated_fields.append('last_name') # Update phone_number if 'phone_number' in json_data: phone_number = json_data.get('phone_number', '').strip() if phone_number: # Check if phone number is already taken by another user if User.objects.filter(phone_number=phone_number).exclude(id=user.id).exists(): errors['phone_number'] = 'Phone number is already registered.' else: user.phone_number = phone_number updated_fields.append('phone_number') elif phone_number == '': user.phone_number = None updated_fields.append('phone_number') # Update email if 'email' in json_data: email = json_data.get('email', '').strip().lower() if email: # Validate email format if '@' not in email: errors['email'] = 'Invalid email format.' # Check if email is already taken by another user elif User.objects.filter(email=email).exclude(id=user.id).exists(): errors['email'] = 'Email is already registered.' else: user.email = email # Also update username if it was set to email if user.username == user.email or not user.username: user.username = email updated_fields.append('email') elif email == '': errors['email'] = 'Email cannot be empty.' # Update pincode if 'pincode' in json_data: pincode = json_data.get('pincode', '').strip() if pincode: user.pincode = pincode updated_fields.append('pincode') elif pincode == '': user.pincode = None updated_fields.append('pincode') # Update district (with 6-month cooldown) if 'district' in json_data: from django.utils import timezone from datetime import timedelta from accounts.models import VALID_DISTRICTS COOLDOWN = timedelta(days=183) # ~6 months new_district = json_data.get('district', '').strip() if new_district and new_district not in VALID_DISTRICTS: errors['district'] = 'Invalid district.' elif new_district and new_district != (user.district or ''): if user.district_changed_at and timezone.now() < user.district_changed_at + COOLDOWN: next_date = (user.district_changed_at + COOLDOWN).strftime('%d %b %Y') errors['district'] = f'District can only be changed once every 6 months. Next change: {next_date}.' else: user.district = new_district user.district_changed_at = timezone.now() updated_fields.append('district') elif new_district == '' and user.district: if user.district_changed_at and timezone.now() < user.district_changed_at + COOLDOWN: next_date = (user.district_changed_at + COOLDOWN).strftime('%d %b %Y') errors['district'] = f'District can only be changed once every 6 months. Next change: {next_date}.' else: user.district = None user.district_changed_at = timezone.now() updated_fields.append('district') # Update state if 'state' in json_data: state = json_data.get('state', '').strip() if state: user.state = state updated_fields.append('state') elif state == '': user.state = None updated_fields.append('state') # Update country if 'country' in json_data: country = json_data.get('country', '').strip() if country: user.country = country updated_fields.append('country') elif country == '': user.country = None updated_fields.append('country') # Update place if 'place' in json_data: place = json_data.get('place', '').strip() if place: user.place = place updated_fields.append('place') elif place == '': user.place = None updated_fields.append('place') # Handle profile_picture (multipart form-data only) if 'profile_photo' in request.FILES: # Handle file upload from multipart/form-data profile_photo = request.FILES['profile_photo'] # Validate file type if not profile_photo.content_type.startswith('image/'): errors['profile_photo'] = 'File must be an image.' else: user.profile_picture = profile_photo updated_fields.append('profile_photo') # Return errors if any if errors: return JsonResponse({ 'success': False, 'errors': errors }, status=400) # Save user if any fields were updated if updated_fields: user.save() return JsonResponse({ 'success': True, 'message': 'Profile updated successfully', 'updated_fields': updated_fields, 'user': { 'username': user.username, 'email': user.email, 'first_name': user.first_name, 'last_name': user.last_name, 'phone_number': user.phone_number, 'pincode': user.pincode, 'district': user.district, 'district_changed_at': user.district_changed_at.isoformat() if user.district_changed_at else None, 'state': user.state, 'country': user.country, 'place': user.place, 'profile_picture': user.profile_picture.url if user.profile_picture else None, } }, status=200) else: return JsonResponse({ 'success': False, 'error': 'No fields provided for update' }, status=400) except Exception as e: log("error", "API update profile exception", request=request, logger_data={"error": str(e)}) return JsonResponse({ 'success': False, 'error': 'An unexpected server error occurred. Please try again.' }, status=500) @method_decorator(csrf_exempt, name='dispatch') class BulkUserPublicInfoView(APIView): """Internal endpoint for Node.js gamification server to resolve user details. Accepts POST with { emails: [...] } (max 500). Returns { users: { email: { district, display_name, eventify_id } } } """ authentication_classes = [] permission_classes = [] def post(self, request): try: json_data = json.loads(request.body) emails = json_data.get('emails', []) if not emails or not isinstance(emails, list) or len(emails) > 500: return JsonResponse({'error': 'Provide 1-500 emails'}, status=400) users_qs = User.objects.filter(email__in=emails).values_list( 'email', 'first_name', 'last_name', 'district', 'eventify_id' ) result = {} for email, first, last, district, eid in users_qs: name = f"{first} {last}".strip() or email.split('@')[0] result[email] = { 'display_name': name, 'district': district or '', 'eventify_id': eid or '', } return JsonResponse({'users': result}) except Exception as e: log("error", "BulkUserPublicInfoView error", logger_data={"error": str(e)}) return JsonResponse({'error': 'An unexpected server error occurred.'}, status=500) @method_decorator(csrf_exempt, name='dispatch') class GoogleLoginView(View): """Verify a Google ID token, find or create the user, return the same response shape as LoginView.""" def post(self, request): try: from google.oauth2 import id_token as google_id_token from google.auth.transport import requests as google_requests from django.conf import settings data = json.loads(request.body) token = data.get('id_token') if not token: return JsonResponse({'error': 'id_token is required'}, status=400) if not settings.GOOGLE_CLIENT_ID: log("error", "GOOGLE_CLIENT_ID not configured", request=request) return JsonResponse({'error': 'Google login temporarily unavailable'}, status=503) idinfo = google_id_token.verify_oauth2_token( token, google_requests.Request(), settings.GOOGLE_CLIENT_ID, ) email = idinfo.get('email') if not email: return JsonResponse({'error': 'Email not found in Google token'}, status=400) user, created = User.objects.get_or_create( email=email, defaults={ 'username': email, 'first_name': idinfo.get('given_name', ''), 'last_name': idinfo.get('family_name', ''), 'role': 'customer', }, ) if created: user.set_password(secrets.token_urlsafe(32)) user.save() log("info", "Google OAuth new user created", request=request, user=user) auth_token, _ = Token.objects.get_or_create(user=user) log("info", "Google OAuth login", request=request, user=user) return JsonResponse({ 'message': 'Login successful', 'token': auth_token.key, 'eventify_id': user.eventify_id or '', 'username': user.username, 'email': user.email, 'phone_number': user.phone_number or '', 'first_name': user.first_name, 'last_name': user.last_name, 'role': user.role, 'pincode': user.pincode or '', 'district': user.district or '', 'district_changed_at': user.district_changed_at.isoformat() if user.district_changed_at else None, 'state': user.state or '', 'country': user.country or '', 'place': user.place or '', 'latitude': user.latitude or '', 'longitude': user.longitude or '', 'profile_photo': user.profile_picture.url if user.profile_picture else '', }, status=200) except ValueError as e: log("warning", "Google OAuth invalid token", request=request, logger_data={"error": str(e)}) return JsonResponse({'error': 'Invalid Google token'}, status=401) except Exception as e: log("error", "Google OAuth exception", request=request, logger_data={"error": str(e)}) return JsonResponse({'error': 'An unexpected server error occurred.'}, status=500) @method_decorator(csrf_exempt, name='dispatch') class ScheduleCallView(View): """Public endpoint for the 'Schedule a Call' form on the consumer app.""" def post(self, request): from admin_api.models import Lead try: data = json.loads(request.body) name = (data.get('name') or '').strip() email = (data.get('email') or '').strip() phone = (data.get('phone') or '').strip() event_type = (data.get('eventType') or '').strip() message = (data.get('message') or '').strip() errors = {} if not name: errors['name'] = ['This field is required.'] if not email: errors['email'] = ['This field is required.'] if not phone: errors['phone'] = ['This field is required.'] valid_event_types = [c[0] for c in Lead.EVENT_TYPE_CHOICES] if not event_type or event_type not in valid_event_types: errors['eventType'] = [f'Must be one of: {", ".join(valid_event_types)}'] if errors: return JsonResponse({'errors': errors}, status=400) # Auto-link to a consumer account if one exists with this email from django.contrib.auth import get_user_model _User = get_user_model() try: consumer_account = _User.objects.get(email=email) except _User.DoesNotExist: consumer_account = None lead = Lead.objects.create( name=name, email=email, phone=phone, event_type=event_type, message=message, status='new', source='schedule_call', priority='medium', user_account=consumer_account, ) log("info", f"New schedule-call lead #{lead.pk} from {email}", request=request) return JsonResponse({ 'status': 'success', 'message': 'Your request has been submitted. Our team will get back to you soon.', 'lead_id': lead.pk, }, status=201) except json.JSONDecodeError: return JsonResponse({'error': 'Invalid JSON body.'}, status=400) except Exception as e: log("error", "Schedule call exception", request=request, logger_data={"error": str(e)}) return JsonResponse({'error': 'An unexpected server error occurred.'}, status=500)