import base64 import json import re from django.conf import settings from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods from .models import EBook, EBookProgress, EBookHighlights, EBookBookmarks def _require_auth(request): if not request.user.is_authenticated: return JsonResponse({'error': 'authentication required'}, status=401) return None @require_http_methods(['GET']) def book_list(request): err = _require_auth(request) if err: return err books = list( request.user.ebooks.values('id', 'meta_ct', 'meta_iv', 'uploaded_at') ) for b in books: b['uploaded_at'] = b['uploaded_at'].isoformat() # Include saved scroll_fraction for each book progress_map = { p.book_id: (p.scroll_fraction, p.updated_at, p.position_anchor) for p in EBookProgress.objects.filter(user=request.user) } for b in books: prog = progress_map.get(b['id']) b['scroll_fraction'] = prog[0] if prog else 0.0 b['last_read'] = prog[1].isoformat() if prog else None b['position_anchor'] = prog[2] if prog else '' return JsonResponse(books, safe=False) @csrf_exempt @require_http_methods(['POST']) def upload_book(request): err = _require_auth(request) if err: return err try: body = json.loads(request.body) except (json.JSONDecodeError, ValueError): return JsonResponse({'error': 'invalid JSON'}, status=400) meta_ct = body.get('meta_ct', '') meta_iv = body.get('meta_iv', '') data_ct = body.get('data_ct', '') data_iv = body.get('data_iv', '') if not all([meta_ct, meta_iv, data_ct, data_iv]): return JsonResponse({'error': 'meta_ct, meta_iv, data_ct, data_iv required'}, status=400) # Enforce size limit: ciphertext is plaintext + 16-byte GCM tag max_bytes = getattr(settings, 'EBOOK_MAX_BYTES', 10 * 1024 * 1024) + 32 try: raw_size = len(base64.b64decode(data_ct)) except Exception: return JsonResponse({'error': 'invalid base64 in data_ct'}, status=400) if raw_size > max_bytes: return JsonResponse({'error': 'file too large (max 10 MB)'}, status=400) book = EBook.objects.create( user=request.user, meta_ct=meta_ct, meta_iv=meta_iv, data_ct=data_ct, data_iv=data_iv, ) return JsonResponse({'ok': True, 'id': book.id}) @require_http_methods(['GET']) def get_book_data(request, pk): err = _require_auth(request) if err: return err try: book = EBook.objects.get(pk=pk, user=request.user) except EBook.DoesNotExist: return JsonResponse({'error': 'not found'}, status=404) return JsonResponse({'data_ct': book.data_ct, 'data_iv': book.data_iv}) @csrf_exempt @require_http_methods(['POST']) def delete_book(request, pk): err = _require_auth(request) if err: return err try: book = EBook.objects.get(pk=pk, user=request.user) except EBook.DoesNotExist: return JsonResponse({'error': 'not found'}, status=404) book.delete() return JsonResponse({'ok': True}) @csrf_exempt @require_http_methods(['POST']) def save_progress(request, pk): err = _require_auth(request) if err: return err try: book = EBook.objects.get(pk=pk, user=request.user) except EBook.DoesNotExist: return JsonResponse({'error': 'not found'}, status=404) try: body = json.loads(request.body) except (json.JSONDecodeError, ValueError): return JsonResponse({'error': 'invalid JSON'}, status=400) scroll_fraction = float(body.get('scroll_fraction', 0.0)) scroll_fraction = max(0.0, min(1.0, scroll_fraction)) raw_anchor = body.get('position_anchor', '') position_anchor = '' if isinstance(raw_anchor, str) and re.fullmatch(r'\d{1,7}:\d(\.\d{1,6})?', raw_anchor): position_anchor = raw_anchor progress, _ = EBookProgress.objects.get_or_create( user=request.user, book=book, ) progress.scroll_fraction = scroll_fraction progress.position_anchor = position_anchor progress.save(update_fields=['scroll_fraction', 'position_anchor', 'updated_at']) return JsonResponse({'ok': True}) @csrf_exempt @require_http_methods(['GET', 'POST']) def book_highlights(request, pk): err = _require_auth(request) if err: return err try: book = EBook.objects.get(pk=pk, user=request.user) except EBook.DoesNotExist: return JsonResponse({'error': 'not found'}, status=404) if request.method == 'GET': try: row = EBookHighlights.objects.get(user=request.user, book=book) return JsonResponse({'ct': row.ct, 'iv': row.iv}) except EBookHighlights.DoesNotExist: return JsonResponse({'ct': None, 'iv': None}) # POST — upsert try: body = json.loads(request.body) except (json.JSONDecodeError, ValueError): return JsonResponse({'error': 'invalid JSON'}, status=400) ct = body.get('ct', '') iv = body.get('iv', '') if not ct or not iv: return JsonResponse({'error': 'ct and iv required'}, status=400) # Size guard: highlights ≤ 700 KB base64 try: raw_size = len(base64.b64decode(ct)) except Exception: return JsonResponse({'error': 'invalid base64 in ct'}, status=400) if raw_size > 700 * 1024: return JsonResponse({'error': 'highlights data too large (max 700 KB)'}, status=400) row, _ = EBookHighlights.objects.get_or_create(user=request.user, book=book) row.ct = ct row.iv = iv row.save(update_fields=['ct', 'iv', 'updated_at']) return JsonResponse({'ok': True}) @csrf_exempt @require_http_methods(['GET', 'POST']) def book_bookmarks(request, pk): err = _require_auth(request) if err: return err try: book = EBook.objects.get(pk=pk, user=request.user) except EBook.DoesNotExist: return JsonResponse({'error': 'not found'}, status=404) if request.method == 'GET': try: row = EBookBookmarks.objects.get(user=request.user, book=book) return JsonResponse({'ct': row.ct, 'iv': row.iv}) except EBookBookmarks.DoesNotExist: return JsonResponse({'ct': None, 'iv': None}) # POST — upsert try: body = json.loads(request.body) except (json.JSONDecodeError, ValueError): return JsonResponse({'error': 'invalid JSON'}, status=400) ct = body.get('ct', '') iv = body.get('iv', '') if not ct or not iv: return JsonResponse({'error': 'ct and iv required'}, status=400) # Size guard: bookmarks ≤ 100 KB base64 try: raw_size = len(base64.b64decode(ct)) except Exception: return JsonResponse({'error': 'invalid base64 in ct'}, status=400) if raw_size > 100 * 1024: return JsonResponse({'error': 'bookmarks data too large (max 100 KB)'}, status=400) row, _ = EBookBookmarks.objects.get_or_create(user=request.user, book=book) row.ct = ct row.iv = iv row.save(update_fields=['ct', 'iv', 'updated_at']) return JsonResponse({'ok': True})