CoolFace
Apppublic

docsift-backend-host/docsift-api

sourceHugging Faceupdated 4mo agoView on Hugging Face
0likes
main.py586 linesDownload Raw Back to root
1import os2import jwt3import json # تم إضافة الـ Import الناقص4import asyncio5import uuid6import smtplib7import random8import traceback9import easyocr10import hashlib11import hmac12import numpy as np13from PIL import Image14from fastapi import FastAPI, UploadFile, File, HTTPException, BackgroundTasks, Depends, Request, Form, Security, APIRouter, Header15from fastapi.middleware.cors import CORSMiddleware16from fastapi.security.api_key import APIKeyHeader17from email.mime.text import MIMEText18from typing import Optional19from email.mime.multipart import MIMEMultipart20from fastapi.responses import FileResponse, JSONResponse 21from datetime import datetime, timedelta22from dotenv import load_dotenv23from supabase import create_client, Client24from slowapi import Limiter, _rate_limit_exceeded_handler25from slowapi.util import get_remote_address26from slowapi.errors import RateLimitExceeded27from starlette.requests import Request28from starlette.status import HTTP_403_FORBIDDEN29 30# استيراد الدوال الـ Async والـ Processor المطور31from database import (32    save_file_info, 33    save_analysis_results, 34    get_dashboard_data, 35    get_files_from_db, 36    get_file_by_id, 37    delete_file_db,38    run_sync_in_async,39    supabase40)41from storage import upload_file_to_r2, delete_from_r2, R2_PUBLIC_URL42from processor import DocumentProcessor 43from pydantic import BaseModel, EmailStr44 45# مكتبات الـ PDF للتقارير46from reportlab.lib import colors47from reportlab.lib.units import inch48from reportlab.lib.pagesizes import letter49from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image, HRFlowable50from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle51from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT52 53load_dotenv()54 55app = FastAPI(title="DocSift AI - High Scale Edition")56limiter = Limiter(key_func=get_remote_address)57app.state.limiter = limiter58app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)59 60# نظام "العساس" (Semaphore) لضمان عدم انهيار السيرفر المجاني61analysis_semaphore = asyncio.Semaphore(2)62API_KEY_NAME = "X-API-Key"63api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)64 65# إعداد الـ CORS66app.add_middleware(67    CORSMiddleware,68    allow_origins=["*"],69    allow_credentials=True,70    allow_methods=["GET", "POST", "DELETE", "OPTIONS", "PUT"],71    allow_headers=["*"],72)73 74class UserSignUp(BaseModel):75    full_name: str76    email: EmailStr77    password: str78 79class UserLogin(BaseModel):80    email: EmailStr81    password: str82 83class VerifyOTP(BaseModel):84    email: str85    token: str86 87token = os.getenv("SUPABASE_KEY")88if token:89    try:90        decoded = jwt.decode(token, options={"verify_signature": False})91        print(f"[CHECK] Current Key Role: {decoded.get('role')}")92    except Exception:93        print("[CHECK] Cannot decode SUPABASE_KEY")94 95url = os.getenv("SUPABASE_URL")96supabase: Client = create_client(url, token)97 98PADDLE_WEBHOOK_SECRET = os.getenv("PADDLE_WEBHOOK_SECRET")99router = APIRouter(prefix="/webhooks", tags=["webhooks"])100 101@app.get("/")102async def read_root():103    return {"status": "online", "message": "Neural Engine OCR-Ready & Scalable"}104 105def send_auth_email(target_email: str, code: str):106    sender_email = os.getenv("GMAIL_USER")107    app_password = os.getenv("GMAIL_PASSWORD")108 109    message = MIMEMultipart()110    message["From"] = f"DocSift Official <{sender_email}>"111    message["To"] = target_email112    message["Subject"] = f"{code} is your DocSift verification code"113 114    html = f"""115    <div style="font-family: sans-serif; max-width: 400px; margin: auto; border: 1px solid #eee; padding: 20px; border-radius: 10px;">116        <h2 style="color: #333; text-align: center;">Verify your account</h2>117        <p style="color: #555;">Use the code below to access your DocSift dashboard. This code is valid for <b>5 minutes</b>.</p>118        <div style="background: #f4f4f4; font-size: 32px; font-weight: bold; text-align: center; padding: 15px; color: #4f46e5; letter-spacing: 5px; border-radius: 5px;">119            {code}120        </div>121        <p style="font-size: 12px; color: #888; margin-top: 20px; text-align: center;">If you didn't request this code, please ignore this email.</p>122    </div>123    """124    message.attach(MIMEText(html, "html"))125 126    try:127        with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:128            server.login(sender_email, app_password)129            server.sendmail(sender_email, target_email, message.as_string())130        print(f"✅ Email sent to {target_email}")131    except Exception as e:132        print(f"❌ SMTP Error: {str(e)}")133 134@app.post("/auth/signup")135@limiter.limit("5/hour")136async def signup(request: Request, user: UserSignUp, background_tasks: BackgroundTasks):137    try:138        auth_res = await run_sync_in_async(lambda: supabase.auth.sign_up({139            "email": user.email, "password": user.password140        }))141        142        otp_code = str(random.randint(100000, 999999))143        expiry_time = datetime.utcnow() + timedelta(minutes=5)144 145        await run_sync_in_async(lambda: supabase.table("otps").upsert({146            "email": user.email,147            "code": otp_code,148            "expires_at": expiry_time.isoformat()149        }, on_conflict="email").execute())150 151        background_tasks.add_task(send_auth_email, user.email, otp_code)152        return {"status": "success", "message": "Verification code sent!"}153    except Exception as e:154        print(f"🔥 Signup Error: {str(e)}")155        raise HTTPException(status_code=400, detail="Account creation failed.")156 157@app.post("/auth/verify")158async def verify_email(data: VerifyOTP):159    try:160        otp_res = await run_sync_in_async(lambda: supabase.table("otps").select("*").eq("email", data.email).single().execute())161        if not otp_res.data or otp_res.data['code'] != data.token:162            raise HTTPException(status_code=400, detail="Invalid verification code.")163 164        profile_res = await run_sync_in_async(lambda: supabase.table("profiles").select("id").eq("email", data.email).execute())165        if not profile_res.data:166            raise HTTPException(status_code=404, detail="Profile not found.")167        168        user_id = profile_res.data[0]['id']169        await run_sync_in_async(lambda: supabase.table("profiles").update({"is_verified": True, "credits": 10}).eq("id", user_id).execute())170        await run_sync_in_async(lambda: supabase.table("otps").delete().eq("email", data.email).execute())171 172        return {"status": "success", "message": "Verified!"}173    except Exception as e:174        raise HTTPException(status_code=400, detail="Verification failed.")175 176@app.post("/auth/login")177async def login(user: UserLogin):178    try:179        res = await run_sync_in_async(lambda: supabase.auth.sign_in_with_password({180            "email": user.email, "password": user.password181        }))182        183        profile = await run_sync_in_async(lambda: supabase.table("profiles").select("*").eq("email", user.email).single().execute())184        if not profile.data or not profile.data.get("is_verified"):185            raise HTTPException(status_code=401, detail="Please verify your email first!")186 187        return {188            "token": res.session.access_token,189            "user_id": res.user.id,190            "full_name": profile.data.get("full_name", ""),191            "credits": profile.data.get("credits", 0)192        }193    except Exception as e:194        raise HTTPException(status_code=401, detail="Invalid credentials.")195 196@app.post("/auth/forgot-password")197@limiter.limit("3/minute")198async def forgot_password(request: Request, data: dict, background_tasks: BackgroundTasks):199    email = data.get("email")200    if not email:201        raise HTTPException(status_code=400, detail="Email is required")202 203    otp = str(random.randint(100000, 999999))204    try:205        await run_sync_in_async(lambda: supabase.table("otps").upsert({206            "email": email, "code": otp,207            "expires_at": (datetime.utcnow() + timedelta(minutes=10)).isoformat()208        }, on_conflict="email").execute())209        210        background_tasks.add_task(send_auth_email, email, otp)211        return {"message": "Reset code sent successfully"}212    except Exception as e:213        raise HTTPException(status_code=500, detail="Could not process request")214 215@app.post("/auth/reset-password")216async def reset_password(data: dict):217    email, token, new_password = data.get("email"), data.get("token"), data.get("new_password")218    if not all([email, token, new_password]):219        raise HTTPException(status_code=400, detail="Missing fields")220 221    try:222        otp_res = await run_sync_in_async(lambda: supabase.table("otps").select("*").eq("email", email).single().execute())223        if not otp_res.data or otp_res.data['code'] != token:224            raise HTTPException(status_code=400, detail="Invalid code")225        226        user_list = await run_sync_in_async(lambda: supabase.auth.admin.list_users())227        target_user = next((u for u in user_list if u.email == email), None)228        229        if not target_user:230            raise HTTPException(status_code=404, detail="User not found")231 232        await run_sync_in_async(lambda: supabase.auth.admin.update_user_by_id(target_user.id, attributes={"password": new_password}))233        await run_sync_in_async(lambda: supabase.table("otps").delete().eq("email", email).execute())234        235        return {"status": "success", "message": "Password updated"}236    except Exception as e:237        raise HTTPException(status_code=500, detail="Reset failed")238 239async def process_and_analyze_task(file_id: str, filename: str, content: bytes, file_url: str):240    try:241        print(f"⚙️ Background Work Starting: {filename}")242        text = await DocumentProcessor.extract_text(content, filename.split('.')[-1])243        analysis = await DocumentProcessor.analyze_risk(text)244        245        update_data = {246            "risk_score": int(analysis.get("risk_score", 0)), 247            "compliance_score": int(analysis.get("compliance_score", 0)), 248            "status": "complete",249            "breakdown": analysis.get("breakdown", {}) 250        }251        252        print(f"📊 Sending Data to DB: {update_data}")253        await run_sync_in_async(lambda: supabase.table("files").update(update_data).eq("id", file_id).execute())254        await save_analysis_results(file_id, analysis)255        print(f"✅ Analysis finished for {filename}")256    except Exception as e:257        print(f"❌ Worker Error: {str(e)}")258        try:259            await run_sync_in_async(lambda: supabase.table("files").update({"status": "error"}).eq("id", file_id).execute())260        except:261            pass262 263@app.post('/upload')264async def upload(265    background_tasks: BackgroundTasks, 266    user_id: str = Form(...), 267    file: UploadFile = File(...)268):269    try:270        user_res = await run_sync_in_async(lambda: supabase.table("profiles").select("credits").eq("id", user_id).single().execute())271        current_credits = user_res.data.get("credits", 0) if user_res.data else 0272 273        if current_credits <= 0:274            raise HTTPException(status_code=403, detail="Out of credits.")275 276        file_content = await file.read()277        file_id = str(uuid.uuid4())278        r2_path = f"{user_id}/{file.filename}"279        280        upload_success = await upload_file_to_r2(file_content, r2_path)281        if not upload_success:282            raise HTTPException(status_code=500, detail="Storage error")283 284        await run_sync_in_async(lambda: supabase.table("files").insert({285            "id": file_id, "user_id": user_id, "name": file.filename,286            "url": f"{R2_PUBLIC_URL}/{r2_path}", "status": "processing"287        }).execute())288 289        await run_sync_in_async(lambda: supabase.table("profiles").update({"credits": current_credits - 1}).eq("id", user_id).execute())290        background_tasks.add_task(queued_analysis, file_id, file.filename, file_content, r2_path)291 292        return {"status": "processing", "file_id": file_id, "remaining_credits": current_credits - 1}293    except Exception as e:294        print(f"❌ Upload Error: {e}")295        raise HTTPException(status_code=500, detail=str(e))296 297async def queued_analysis(file_id, filename, content, r2_path):298    async with analysis_semaphore: 299        print(f"🚀 Semaphore granted for: {filename}. Starting analysis...")300        await process_and_analyze_task(file_id, filename, content, r2_path)301 302@app.get("/dashboard-stats")303async def get_dashboard(user_id: str): 304    try:305        profile_res = await run_sync_in_async(lambda: supabase.table("profiles").select("credits").eq("id", user_id).single().execute())306        credits = profile_res.data.get("credits", 0) if profile_res.data else 0307 308        files_res = await run_sync_in_async(lambda: supabase.table("files").select("*").eq("user_id", user_id).order("created_at", desc=True).execute())309        files = files_res.data or []310 311        total_docs = len(files)312        total_risks = sum(1 for f in files if f.get("risk_score", 0) > 50)313        314        compliance_scores = [f.get("compliance_score", 0) for f in files]315        compliance_rate = int(sum(compliance_scores) / total_docs) if total_docs > 0 else 100316 317        return {318            "total_docs": total_docs,319            "total_risks": total_risks,320            "compliance_rate": compliance_rate,321            "credits": credits, 322            "recent_activity": files[:5] 323        }324    except Exception as e:325        print(f"❌ Dashboard Error: {str(e)}")326        raise HTTPException(status_code=500, detail="Failed to fetch dashboard intelligence.")327 328# 🔥 تصحيح الـ الـ Vault: جلب ملفات المستخدم الحالي فقط بناءً على الـ user_id329@app.get("/files")330async def get_all_files(user_id: str):331    if not user_id:332        raise HTTPException(status_code=400, detail="Missing user_id parameter")333    res = await run_sync_in_async(lambda: supabase.table("files").select("*").eq("user_id", user_id).order("created_at", desc=True).execute())334    return res.data335 336@app.api_route("/files/{file_id}", methods=["DELETE", "OPTIONS"])337async def delete_file(file_id: str, request: Request):338    user_id = request.query_params.get("user_id")339    print(f"--- PURGE ATTEMPT --- Target ID: {file_id} | User ID: {user_id}")340    341    file_data = await get_file_by_id(file_id)342    if not file_data:343        raise HTTPException(status_code=404, detail="Asset not found in database")344    345    if str(file_data.get('user_id')) != user_id:346        raise HTTPException(status_code=403, detail="Unauthorized purge request")347        348    await asyncio.gather(349        delete_from_r2(file_data.get('name') or file_data.get('storage_path')),350        delete_file_db(file_id)351    )352    return {"status": "purged"}353 354@app.get("/generate-report/{file_id}")355async def generate_report(file_id: str):356    file_data = await get_file_by_id(file_id)357    if not file_data:358         raise HTTPException(status_code=404, detail="File not found")359 360    file_path = f"generated_reports/Audit_{file_id}.pdf"361    os.makedirs("generated_reports", exist_ok=True)362    report_hash = hashlib.sha256(f"{file_id}-{datetime.now()}".encode()).hexdigest()[:16].upper()363 364    doc = SimpleDocTemplate(file_path, pagesize=letter, rightMargin=40, leftMargin=40, topMargin=30, bottomMargin=30)365    styles = getSampleStyleSheet()366    elements = []367 368    def add_watermark(canvas, doc):369        canvas.saveState()370        canvas.setFont('Helvetica-Bold', 60)371        canvas.setStrokeColor(colors.lightgrey)372        canvas.setFillAlpha(0.1) 373        canvas.translate(300, 400)374        canvas.rotate(45)375        canvas.drawCentredString(0, 0, "OFFICIAL DOCSIFT AUDIT")376        canvas.rotate(-45)377        canvas.setFont('Helvetica', 7)378        canvas.setFillAlpha(0.5)379        canvas.drawString(-250, -380, f"AUTHENTICITY HASH: {report_hash} | VERIFY AT DOCSIFT.AI/VERIFY")380        canvas.restoreState()381 382    header_style = ParagraphStyle('MainTitle', fontSize=22, fontName='Helvetica-Bold', textColor=colors.HexColor("#1E1B4B"))383    legal_warning_style = ParagraphStyle('Warning', fontSize=7, textColor=colors.red, alignment=TA_CENTER, leading=8)384 385    logo_path = os.path.join("assets", "logo.png")386    logo_img = "DOCSIFT AI"387 388    if os.path.exists(logo_path):389        try:390            logo_img = Image(logo_path)391            desired_width = 1.2 * inch392            aspect = logo_img.imageHeight / float(logo_img.imageWidth)393            logo_img.drawWidth = desired_width394            logo_img.drawHeight = desired_width * aspect395            logo_img.hAlign = 'LEFT'396        except Exception as e:397            print(f"❌ Logo Load Error: {e}")398            logo_img = "DOCSIFT AI"399 400    header_data = [[logo_img, Paragraph("NEURAL AUDIT REPORT", header_style)]]401    header_table = Table(header_data, colWidths=[100, 420])402    header_table.setStyle(TableStyle([('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), ('ALIGN', (1, 0), (1, 0), 'RIGHT')]))403    elements.append(header_table)404    elements.append(HRFlowable(width="100%", thickness=2, color=colors.HexColor("#4F46E5"), spaceAfter=20))405 406    warning_text = "PROTECTED DOCUMENT: This audit is cryptographically linked to DocSift Neural Systems. Unauthorized alteration is a federal offense."407    elements.append(Paragraph(warning_text, legal_warning_style))408    elements.append(Spacer(1, 15))409 410    meta_data = [411        ["ASSET NAME:", file_data['name']],412        ["AUDIT ID:", f"DOC-{file_id[:8].upper()}"],413        ["TIMESTAMP:", datetime.now().strftime('%Y-%m-%d %H:%M:%S')],414        ["SECURITY HASH:", report_hash] 415    ]416    meta_table = Table(meta_data, colWidths=[120, 380])417    meta_table.setStyle(TableStyle([('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'), ('TEXTCOLOR', (0,0), (0,-1), colors.HexColor("#374151"))]))418    elements.append(meta_table)419    elements.append(Spacer(1, 20))420 421    data = [422        ["AUDIT CATEGORY", "SCORE", "RISK ASSESSMENT"],423        ["Legal Exposure", f"{int(file_data.get('breakdown', {}).get('legal', 0))}%", "VERIFIED"],424        ["Financial Liability", f"{int(file_data.get('breakdown', {}).get('financial', 0))}%", "SECURE"],425        ["Compliance Overall", f"{int(file_data.get('compliance_score', 0))}%", "CERTIFIED"],426    ]427    analysis_table = Table(data, colWidths=[200, 100, 200])428    analysis_table.setStyle(TableStyle([429        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#1E1B4B")),430        ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),431        ('ALIGN', (0, 0), (-1, -1), 'CENTER'),432        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),433        ('GRID', (0, 0), (-1, -1), 0.5, colors.grey),434        ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor("#F3F4F6")]),435    ]))436    elements.append(analysis_table)437 438    elements.append(Spacer(1, 80))439    elements.append(HRFlowable(width="100%", thickness=0.5, color=colors.black))440    441    strict_legal = f"""442    <b>CRITICAL LEGAL NOTICE:</b> This document is issued under the Digital Assets Security Act. Any attempt to modify, forge, or misrepresent the scores herein is strictly prohibited and punishable under international fraud and cybercrime laws. 443    This report is verified by Hash <b>{report_hash}</b>. Forgery will be detected upon verification against DocSift's central database. 444    DocSift AI assumes no liability for external use. All rights reserved © {datetime.now().year}.445    """446    elements.append(Paragraph(strict_legal, ParagraphStyle('Strict', fontSize=7, leading=9, textColor=colors.HexColor("#111827"))))447 448    doc.build(elements, onFirstPage=add_watermark, onLaterPages=add_watermark)449    return FileResponse(path=file_path, filename=f"SECURE_AUDIT_{file_data['name']}.pdf")450 451@app.get("/api-keys")452async def get_keys():453    res = await run_sync_in_async(lambda: supabase.table("api_keys").select("*").execute())454    return res.data455 456@app.post("/api-keys")457async def create_key(data: dict):458    new_key = f"ds_{uuid.uuid4().hex}"459    key_data = {"name": data['name'], "key_value": new_key}460    res = await run_sync_in_async(lambda: supabase.table("api_keys").insert(key_data).execute())461    return res.data[0]462 463# تعديل وتأمين الـ DELETE المكرر ليعمل بشكل سليم بـ الـ query params464@app.delete("/files/{file_id}")465async def delete_file_route(file_id: str, user_id: str):466    try:467        file_info = await get_file_by_id(file_id)468        if not file_info:469            raise HTTPException(status_code=404, detail="File not found")470            471        if str(file_info.get('user_id')) != user_id:472            raise HTTPException(status_code=403, detail="Unauthorized purge request")473 474        file_name_in_r2 = file_info.get('storage_path') or file_info.get('name')475        await delete_from_r2(file_name_in_r2)476        await delete_file_db(file_id)477        478        return {"message": "File deleted successfully"}479    except Exception as e:480        print(f"❌ DELETE ERROR: {e}")481        raise HTTPException(status_code=500, detail=str(e))482 483def verify_paddle_signature(signature: str, body: bytes) -> bool:484    try:485        if not signature or not PADDLE_WEBHOOK_SECRET:486            return False487        parts = dict(item.split('=') for item in signature.split(';'))488        ts = parts.get('ts')489        h = parts.get('h')490        if not ts or not h:491            return False492        signed_payload = f"{ts}:{body.decode('utf-8')}"493        computed_hash = hmac.new(494            PADDLE_WEBHOOK_SECRET.encode('utf-8'),495            signed_payload.encode('utf-8'),496            hashlib.sha256497        ).hexdigest()498        return hmac.compare_digest(computed_hash, h)499    except Exception:500        return False501 502async def fulfill_order(payload: dict):503    try:504        event_data = payload.get("data", {})505        custom_data = event_data.get("custom_data", {})506        user_id = custom_data.get("user_id")507        plan_type = custom_data.get("plan_type")508        509        if not user_id:510            return511        512        credits_map = {"pro": 200, "enterprise": 1500}513        amount = credits_map.get(plan_type, 0)514        515        if amount > 0:516            profile = await run_sync_in_async(lambda: supabase.table("profiles").select("credits").eq("id", user_id).single().execute())517            new_total = (profile.data.get("credits", 0) if profile.data else 0) + amount518            await run_sync_in_async(lambda: supabase.table("profiles").update({"credits": new_total, "plan": plan_type}).eq("id", user_id).execute())519            print(f"✅ User {user_id} upgraded! +{amount} credits added.")520    except Exception as e:521        print(f"❌ Fulfillment Error: {str(e)}")522 523@app.post("/webhooks/paddle")524async def paddle_webhook(525    request: Request, 526    background_tasks: BackgroundTasks,527    paddle_signature: Optional[str] = Header(None)528):529    body = await request.body()530    if not verify_paddle_signature(paddle_signature, body):531        raise HTTPException(status_code=401, detail="Invalid Signature")532 533    payload = json.loads(body)534    event_type = payload.get("event_type")535 536    if event_type == "transaction.completed":537        background_tasks.add_task(fulfill_order, payload)538        return {"status": "processing"}539 540    return {"status": "ignored"}541 542@app.middleware("http")543async def global_middleware(request: Request, call_next):544    path = request.url.path545    if path.startswith("/files/") and path.endswith("/"):546        request.scope['path'] = path[:-1]547    try:548        response = await call_next(request)549        if response.status_code == 404:550            print(f"🔍 404 ALERT: Route not found -> {request.method} {request.url.path}")551        return response552    except Exception as e:553        print("\n" + "🔥"*20 + "\nCRITICAL ERROR CAUGHT:\n" + traceback.format_exc() + "\n" + "🔥"*20 + "\n")554        return JSONResponse(555            status_code=500,556            content={"detail": "Neural Link Failure. Our engineers are on it!"}557        )558 559async def get_api_key(api_key_header: str = Security(api_key_header)):560    if not api_key_header:561        raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="API Key is missing. Check 'X-API-Key' header.")562    res = await run_sync_in_async(lambda: supabase.table("api_keys").select("*").eq("key_value", api_key_header).execute())563    if not res.data:564        raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Invalid or Revoked API Key.") # تم تصحيح الـ Typo هنا565    return res.data[0]566 567@app.post("/v1/analyze")568async def b2b_analyze(569    background_tasks: BackgroundTasks,570    file: UploadFile = File(...),571    key_info: dict = Depends(get_api_key)572):573    try:574        user_id = key_info.get("user_id")575        file_id = str(uuid.uuid4())576        content = await file.read()577        r2_path = f"uploads/{user_id}/{file_id}_{file.filename}"578        background_tasks.add_task(process_and_analyze_task, file_id, file.filename, content, r2_path)579        return {580            "status": "processing",581            "file_id": file_id,582            "message": "Document received. Analysis started in background.",583            "request_id": str(uuid.uuid4())[:8]584        }585    except Exception as e:586        raise HTTPException(status_code=500, detail=f"Server Error: {str(e)}")