Browse Source
feat(account): preserve and merge guest user data on registration, login, and support chat
feat(account): preserve and merge guest user data on registration, login, and support chat
- Add guest_merge.py to resolve guest accounts via token, device_id, and web IP+UA fingerprint - Implement merge_guest_user_data for support rooms, messages, bookmarks, and ratings - Trigger guest merging in UserVerifyView on verification and UserLoginView on authentication - Automatically merge guest support conversations in resolve_user_support_room for logged-in usersmaster
3 changed files with 196 additions and 22 deletions
@ -0,0 +1,165 @@ |
|||
import hashlib |
|||
import logging |
|||
from django.contrib.auth import get_user_model |
|||
from django.db import transaction |
|||
from rest_framework.authtoken.models import Token |
|||
from utils.ip_helper import get_client_ip |
|||
|
|||
logger = logging.getLogger(__name__) |
|||
User = get_user_model() |
|||
|
|||
|
|||
def _extract_token_from_auth_header(request) -> str: |
|||
auth_header = (request.META.get('HTTP_AUTHORIZATION') or '').strip() |
|||
if not auth_header: |
|||
return '' |
|||
parts = auth_header.split() |
|||
if len(parts) == 2 and parts[0].lower() in ('token', 'bearer'): |
|||
return parts[1].strip() |
|||
return '' |
|||
|
|||
|
|||
def compute_web_guest_device_id(request) -> str: |
|||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') |
|||
if x_forwarded_for: |
|||
client_ip = x_forwarded_for.split(',')[0] |
|||
else: |
|||
client_ip = request.META.get('REMOTE_ADDR') or get_client_ip(request) or '' |
|||
user_agent = request.META.get('HTTP_USER_AGENT', '') |
|||
ua_hash = hashlib.md5(user_agent.encode('utf-8')).hexdigest()[:8] |
|||
return f"{client_ip}_{ua_hash}" |
|||
|
|||
|
|||
def resolve_guest_users_from_request(request, device_id: str = None) -> list: |
|||
""" |
|||
Finds any Guest User(s) (`email__isnull=True`) associated with the current request |
|||
via X-Guest-Token header, Authorization token header, explicit device_id, or Web IP+UA device_id. |
|||
Returns a deduplicated list of guest User instances in priority order. |
|||
""" |
|||
candidates = [] |
|||
seen_ids = set() |
|||
|
|||
def _add(u): |
|||
if u and getattr(u, 'email', None) is None and u.id not in seen_ids: |
|||
seen_ids.add(u.id) |
|||
candidates.append(u) |
|||
|
|||
# 1. Explicit X-Guest-Token header |
|||
x_guest_key = (request.headers.get('X-Guest-Token') or '').strip() |
|||
if x_guest_key: |
|||
token_obj = Token.objects.select_related('user').filter(key=x_guest_key).first() |
|||
if token_obj: |
|||
_add(token_obj.user) |
|||
|
|||
# 2. Authorization: token <guest_token> header (sent automatically by frontend before auth_token is set) |
|||
auth_key = _extract_token_from_auth_header(request) |
|||
if auth_key and auth_key != x_guest_key: |
|||
token_obj = Token.objects.select_related('user').filter(key=auth_key).first() |
|||
if token_obj: |
|||
_add(token_obj.user) |
|||
|
|||
# 3. Explicit device_id parameter |
|||
if device_id: |
|||
guest_by_device = User.objects.filter(device_id=device_id, email__isnull=True).first() |
|||
_add(guest_by_device) |
|||
|
|||
# 4. Web IP + User-Agent hash fallback (matches WebUserGuestView) |
|||
try: |
|||
web_device_id = compute_web_guest_device_id(request) |
|||
if web_device_id and web_device_id != device_id: |
|||
guest_by_web_id = User.objects.filter(device_id=web_device_id, email__isnull=True).first() |
|||
_add(guest_by_web_id) |
|||
except Exception as exc: |
|||
logger.debug("Could not compute web guest device_id fallback: %s", exc) |
|||
|
|||
return candidates |
|||
|
|||
|
|||
@transaction.atomic |
|||
def merge_guest_user_data(guest_user, target_user) -> None: |
|||
""" |
|||
Merges all guest activity (SupportRoom, SupportMessage, Bookmark, Rate) |
|||
from `guest_user` into `target_user`. |
|||
Safe to call multiple times (idempotent). |
|||
""" |
|||
if not guest_user or not target_user or guest_user.id == target_user.id: |
|||
return |
|||
|
|||
# 1. Merge Support Chat (SupportRoom & SupportMessage) |
|||
try: |
|||
from apps.support.models import SupportRoom, SupportMessage |
|||
|
|||
guest_room = SupportRoom.objects.select_for_update().filter(user=guest_user).first() |
|||
if guest_room: |
|||
target_room = SupportRoom.objects.select_for_update().filter(user=target_user).first() |
|||
if not target_room: |
|||
guest_room.user = target_user |
|||
guest_room.save(update_fields=['user', 'updated_at']) |
|||
SupportMessage.objects.filter(room=guest_room, sender=guest_user).update(sender=target_user) |
|||
else: |
|||
# Both guest and target user already have support rooms: merge messages into target_room |
|||
SupportMessage.objects.filter(room=guest_room).update(room=target_room) |
|||
SupportMessage.objects.filter(room=target_room, sender=guest_user).update(sender=target_user) |
|||
|
|||
target_room.unread_by_admin = (target_room.unread_by_admin or 0) + (guest_room.unread_by_admin or 0) |
|||
target_room.unread_by_user = (target_room.unread_by_user or 0) + (guest_room.unread_by_user or 0) |
|||
|
|||
last_msg = target_room.messages.order_by('-id').first() |
|||
if last_msg: |
|||
target_room.last_message_at = last_msg.created_at |
|||
if last_msg.sender_type == SupportMessage.SenderType.USER: |
|||
target_room.status = SupportRoom.StatusChoices.PENDING |
|||
else: |
|||
target_room.status = SupportRoom.StatusChoices.ANSWERED |
|||
|
|||
target_room.save( |
|||
update_fields=[ |
|||
'unread_by_admin', |
|||
'unread_by_user', |
|||
'last_message_at', |
|||
'status', |
|||
'updated_at', |
|||
] |
|||
) |
|||
guest_room.delete() |
|||
except Exception as exc: |
|||
logger.error("Error merging support chat from guest %s to user %s: %s", guest_user.id, target_user.id, exc) |
|||
|
|||
# 2. Merge Bookmarks |
|||
try: |
|||
from apps.bookmark.models import Bookmark |
|||
|
|||
for bm in Bookmark.objects.filter(user=guest_user): |
|||
existing = Bookmark.objects.filter( |
|||
user=target_user, |
|||
service=bm.service, |
|||
content_id=bm.content_id, |
|||
).first() |
|||
if existing: |
|||
if bm.status and not existing.status: |
|||
existing.status = True |
|||
existing.save(update_fields=['status', 'updated_at']) |
|||
bm.delete() |
|||
else: |
|||
bm.user = target_user |
|||
bm.save(update_fields=['user', 'updated_at']) |
|||
except Exception as exc: |
|||
logger.error("Error merging bookmarks from guest %s to user %s: %s", guest_user.id, target_user.id, exc) |
|||
|
|||
# 3. Merge Rates |
|||
try: |
|||
from apps.bookmark.models import Rate |
|||
|
|||
for rt in Rate.objects.filter(user=guest_user): |
|||
existing = Rate.objects.filter( |
|||
user=target_user, |
|||
service=rt.service, |
|||
content_id=rt.content_id, |
|||
).first() |
|||
if existing: |
|||
rt.delete() |
|||
else: |
|||
rt.user = target_user |
|||
rt.save(update_fields=['user', 'updated_at']) |
|||
except Exception as exc: |
|||
logger.error("Error merging rates from guest %s to user %s: %s", guest_user.id, target_user.id, exc) |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue