# main.py from fastapi import FastAPI, HTTPException, Response, Depends, status from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, EmailStr import base64 from PIL import Image import io import traceback from CrimeSceneAnalyzer import CrimeSceneAnalyzer # Import your analyzer class from pymongo import MongoClient from dotenv import load_dotenv import os from passlib.context import CryptContext from datetime import datetime, timedelta from typing import Optional import jwt from email_validator import validate_email, EmailNotValidError from fastapi_mail import FastMail, MessageSchema, ConnectionConfig import httpx import random import uuid from fastapi.security import OAuth2PasswordBearer from jose import JWTError load_dotenv() from fastapi.staticfiles import StaticFiles app = FastAPI() os.makedirs("public", exist_ok=True) app.mount("/static", StaticFiles(directory="public"), name="static") # Configure CORS to allow your React Native app to access the API app.add_middleware( CORSMiddleware, allow_origins=["*"], # In production, replace with specific origins allow_credentials=True, allow_methods=["*"], allow_headers=["*"], expose_headers=["*"], max_age=3600, ) # MongoDB Connection MONGO_URI = os.getenv("MONGO_URI") try: client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=5000) db = client.mobile # Database name # Test connection client.admin.command('ping') print("✓ MongoDB connected successfully.") except Exception as e: print(f"⚠️ MongoDB connection failed: {e}") client = None db = None # Password Hashing pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") # JWT Settings SECRET_KEY = os.getenv("SECRET_KEY") ALGORITHM = os.getenv("ALGORITHM") ACCESS_TOKEN_EXPIRE_MINUTES = 20 # Changed from 30 to 20 minutes # Email Configuration conf = ConnectionConfig( MAIL_USERNAME=os.getenv("MAIL_USERNAME"), MAIL_PASSWORD=os.getenv("MAIL_PASSWORD"), MAIL_FROM=os.getenv("MAIL_FROM"), MAIL_PORT=int(os.getenv("MAIL_PORT", 587)), MAIL_SERVER=os.getenv("MAIL_SERVER"), MAIL_STARTTLS=os.getenv("MAIL_STARTTLS").lower() == 'true', MAIL_SSL_TLS=os.getenv("MAIL_SSL_TLS").lower() == 'true', USE_CREDENTIALS=os.getenv("USE_CREDENTIALS").lower() == 'true', VALIDATE_CERTS=os.getenv("VALIDATE_CERTS").lower() == 'true', ) analyzer = CrimeSceneAnalyzer() class ImageRequest(BaseModel): image: str # Base64 encoded image string class UserCreate(BaseModel): username: str email: EmailStr password: str phoneNumber: Optional[str] = None class UserLogin(BaseModel): email: EmailStr password: str class ForgotPasswordRequest(BaseModel): email: EmailStr class VerifyOTPRequest(BaseModel): email: EmailStr otp: str class Token(BaseModel): access_token: str token_type: str class TokenData(BaseModel): email: Optional[str] = None def verify_password(plain_password, hashed_password): return pwd_context.verify(plain_password, hashed_password) def get_password_hash(password): return pwd_context.hash(password) def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt async def send_otp_email(email: str, otp: str): """Send OTP via Brevo HTTP API (HF Spaces blocks outbound SMTP).""" html_content = f"""
Dear User,
Your OTP for verification is: {otp}
This OTP is valid for 10 minutes.
If you did not request this, please ignore this email.
""" brevo_api_key = os.getenv("BREVO_API_KEY") if not brevo_api_key: # Fallback: log OTP to console so deployment isn't blocked print(f"[OTP for {email}] {otp} (BREVO_API_KEY not set)") return sender_email = os.getenv("BREVO_SENDER_EMAIL", os.getenv("MAIL_FROM", "noreply@example.com")) sender_name = os.getenv("BREVO_SENDER_NAME", "SceneX") payload = { "sender": {"name": sender_name, "email": sender_email}, "to": [{"email": email}], "subject": "OTP Verification for SceneXMobile", "htmlContent": html_content, } headers = { "api-key": brevo_api_key, "Content-Type": "application/json", "Accept": "application/json", } async with httpx.AsyncClient(timeout=15) as client_http: resp = await client_http.post( "https://api.brevo.com/v3/smtp/email", json=payload, headers=headers ) if resp.status_code >= 400: raise Exception(f"Brevo API error {resp.status_code}: {resp.text}") @app.post("/signup", response_model=Token) async def signup(user: UserCreate): try: if db is None: raise HTTPException(status_code=503, detail="Database not connected. Please check MongoDB URI.") try: validate_email(user.email) except EmailNotValidError: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid email format") if db.users.find_one({"email": user.email}): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered") if db.users.find_one({"username": user.username}): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Username already taken") hashed_password = get_password_hash(user.password) otp = str(random.randint(100000, 999999)) otp_expiry = datetime.utcnow() + timedelta(minutes=10) user_data = { "username": user.username, "email": user.email, "password": hashed_password, "is_verified": False, "otp": otp, "otp_expiry": otp_expiry, "phone": user.phoneNumber if user.phoneNumber else str(uuid.uuid4()) } try: db.users.insert_one(user_data) except Exception as e: print(f"Error inserting user: {e}") raise HTTPException(status_code=500, detail=f"Failed to insert user: {str(e)}") try: await send_otp_email(user.email, otp) print(f"OTP email sent to {user.email}") except Exception as e: print(f"Error sending OTP email: {e}") traceback.print_exc() access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( data={"sub": user.email}, expires_delta=access_token_expires ) return {"access_token": access_token, "token_type": "bearer"} except HTTPException: raise except Exception as e: print(f"UNEXPECTED ERROR in signup: {e}") traceback.print_exc() raise HTTPException(status_code=500, detail=f"Internal error: {str(e)}") @app.post("/login") async def login(user: UserLogin): db_user = db.users.find_one({"email": user.email}) if not db_user or not verify_password(user.password, db_user["password"]): raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect email or password") if not db_user.get("is_verified"): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Account not verified. Please verify your email with OTP.") # Generate OTP for 2FA otp = str(random.randint(100000, 999999)) otp_expiry = datetime.utcnow() + timedelta(minutes=10) db.users.update_one({"email": user.email}, {"$set": {"otp": otp, "otp_expiry": otp_expiry}}) try: await send_otp_email(user.email, otp) except Exception as e: print(f"Error sending OTP email: {e}") raise HTTPException(status_code=500, detail=f"Failed to send OTP email: {str(e)}") return {"message": "OTP sent to your email for verification."} @app.post("/forgot-password") async def forgot_password(request: ForgotPasswordRequest): db_user = db.users.find_one({"email": request.email}) if not db_user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User with this email not found") otp = str(random.randint(100000, 999999)) otp_expiry = datetime.utcnow() + timedelta(minutes=10) db.users.update_one({"email": request.email}, {"$set": {"otp": otp, "otp_expiry": otp_expiry}}) await send_otp_email(request.email, otp) return {"message": "OTP sent to your email for password reset."} @app.post("/verify-otp") async def verify_otp(request: VerifyOTPRequest): db_user = db.users.find_one({"email": request.email}) if not db_user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") if db_user.get("otp") != request.otp: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid OTP") if datetime.utcnow() > db_user.get("otp_expiry", datetime.utcnow()): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="OTP expired") db.users.update_one({"email": request.email}, {"$set": {"is_verified": True, "otp": None, "otp_expiry": None}}) return {"message": "Email verified successfully!"} @app.post("/login/verify-otp", response_model=Token) async def login_verify_otp(request: VerifyOTPRequest): db_user = db.users.find_one({"email": request.email}) if not db_user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") if db_user.get("otp") != request.otp: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid OTP") if datetime.utcnow() > db_user.get("otp_expiry", datetime.utcnow()): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="OTP expired") # Clear OTP after successful verification db.users.update_one({"email": request.email}, {"$set": {"otp": None, "otp_expiry": None}}) # Generate access token access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( data={"sub": request.email}, expires_delta=access_token_expires ) return {"access_token": access_token, "token_type": "bearer"} @app.post("/analyze") async def analyze_image_endpoint(request: ImageRequest): """ Analyzes a crime scene image and returns structured JSON results. The full PDF report can be downloaded via the /download_report endpoint. """ try: # Decode base64 image image_bytes = base64.b64decode(request.image) image_pil = Image.open(io.BytesIO(image_bytes)).convert("RGB") # Call the analyzer to get both analysis data and PDF bytes # We need to temporarily modify `analyze_scene` in your class to return more. # For this example, let's assume `analyze_scene` is modified to return # a dictionary containing 'analysis_data' and 'pdf_bytes'. # --- REFACTORING YOUR CrimeSceneAnalyzer.analyze_scene METHOD --- # Modify CrimeSceneAnalyzer.py's analyze_scene to return a structured dict: # { "analysis_data": {...}, "pdf_bytes":