open-navigator / api /routes /data_deletion.py
jcbowyer's picture
Deploy: Consolidated gold tables, fixed nginx docs routing
a8cf0c8 verified
"""
Data deletion endpoints for OAuth provider compliance (Facebook, etc.)
"""
from fastapi import APIRouter, Request, HTTPException, Depends
from pydantic import BaseModel
from sqlalchemy.orm import Session
from loguru import logger
import hmac
import hashlib
import base64
import json
import os
from datetime import datetime
from api.database import get_db
from api.models import User
router = APIRouter()
class DataDeletionRequest(BaseModel):
"""Data deletion request from OAuth provider"""
signed_request: str # Facebook's signed request
class DataDeletionResponse(BaseModel):
"""Response to data deletion request"""
url: str
confirmation_code: str
def parse_signed_request(signed_request: str, app_secret: str) -> dict:
"""
Parse and validate Facebook's signed_request parameter.
Format: base64url(signature).base64url(payload)
"""
try:
encoded_sig, payload = signed_request.split('.', 1)
# Decode signature
sig = base64.urlsafe_b64decode(encoded_sig + '==') # Add padding
# Decode payload
data = json.loads(base64.urlsafe_b64decode(payload + '=='))
# Verify signature
expected_sig = hmac.new(
app_secret.encode('utf-8'),
msg=payload.encode('utf-8'),
digestmod=hashlib.sha256
).digest()
if sig != expected_sig:
raise ValueError("Invalid signature")
return data
except Exception as e:
logger.error(f"Error parsing signed_request: {e}")
raise HTTPException(status_code=400, detail="Invalid signed request")
@router.post("/data-deletion/facebook", response_model=DataDeletionResponse)
async def facebook_data_deletion_callback(
request: Request,
db: Session = Depends(get_db)
):
"""
Facebook Data Deletion Request Callback
Facebook requires apps to provide a data deletion callback URL.
This endpoint receives deletion requests and processes them.
Docs: https://developers.facebook.com/docs/development/create-an-app/app-dashboard/data-deletion-callback
"""
# Get signed_request from form data
form = await request.form()
signed_request = form.get("signed_request")
if not signed_request:
raise HTTPException(status_code=400, detail="Missing signed_request")
# Get Facebook app secret
app_secret = os.getenv("FACEBOOK_APP_SECRET")
if not app_secret:
logger.error("FACEBOOK_APP_SECRET not configured")
raise HTTPException(status_code=500, detail="Server configuration error")
# Parse and validate signed request
data = parse_signed_request(signed_request, app_secret)
user_id = data.get("user_id") # Facebook user ID
if not user_id:
raise HTTPException(status_code=400, detail="Missing user_id in signed request")
logger.info(f"Data deletion request from Facebook user: {user_id}")
# Find user by OAuth provider ID
user = db.query(User).filter(
User.oauth_provider == "facebook",
User.oauth_id == user_id
).first()
if user:
# Delete user data
logger.info(f"Deleting user account: {user.email} (Facebook ID: {user_id})")
db.delete(user)
db.commit()
status = "deleted"
else:
logger.info(f"No user found for Facebook ID: {user_id}")
status = "not_found"
# Generate confirmation code (can be used for tracking)
confirmation_code = f"DEL-{user_id}-{int(datetime.now().timestamp())}"
# Return deletion status URL
# Users will be redirected here to see status
base_url = os.getenv("FRONTEND_URL", "https://www.communityone.com")
status_url = f"{base_url}/data-deletion-status?code={confirmation_code}&status={status}"
logger.info(f"Data deletion request processed: {confirmation_code}")
return DataDeletionResponse(
url=status_url,
confirmation_code=confirmation_code
)
@router.get("/data-deletion/status")
async def data_deletion_status(code: str = None, status: str = None):
"""
Data deletion status page (informational)
Users are redirected here after Facebook processes their deletion request.
"""
return {
"confirmation_code": code,
"status": status,
"message": "Your data deletion request has been processed." if status == "deleted" else "No account found.",
"timestamp": datetime.now().isoformat()
}