docuflow / simulated_features_implementation_plan.md
Joedroid's picture
feat: implement multi-provider payment architecture (Stripe, Paystack, Binance Pay, Simulator) using dependency injection
944b491
|
Raw
History Blame Contribute Delete
21.5 kB

DocuFlow β€” Simulated Features Transition & Implementation Plan

This document maps out all simulated features in the DocuFlow workspace, registers them as concrete TODO tasks, and details their production-grade implementation blueprints.


Table of Contents

  1. Simulated Features Registry
  2. OAuth 2.0 (Google & GitHub) Authentication
  3. Multi-Provider Payment Architecture (Stripe, Paystack, Binance Pay)
  4. Client-Side Cryptographic Key Derivation (Web Crypto API)
  5. Real-time Multi-user Collaboration (FastAPI WebSockets + Y.js)

1. Simulated Features Registry

The following table tracks every simulated or mocked feature in the codebase, its impact on launch, and its current locations.

Feature Severity Impact Current Mock File(s) Production Target
OAuth 2.0 Login High Blocks real third-party user logins and secure profiles sync. src/components/marketing/AuthPortal.tsx Google/GitHub API Auth
Stripe Billing High Blocks commercial Pro tier monetizations. src/components/marketing/PricingPlans.tsx Multi-Gateway Payment API
Web Cryptography Medium Local document buffers and settings keys are stored in plaintext. src/types.ts (privateKeySeed), src/components/DashboardAnalytics.tsx Web Crypto API (AES-GCM)
Real-time Sync Medium Peer cursor activities and content additions are mock syncer logs. backend/routers/sync.py, src/components/TeamSettings.tsx WebSocket-based Y.js

2. OAuth 2.0 (Google & GitHub) Authentication

πŸ“‹ TODO Checklist

  • Create credentials in Google Cloud Console & GitHub Developer Settings.
  • Install client libraries (@react-oauth/google).
  • Update frontend login portal (AuthPortal.tsx) to trigger native OAuth redirects.
  • Create backend endpoints /api/auth/google and /api/auth/github/callback in FastAPI.
  • Decode ID tokens, verify signature, and issue custom signed JWT session tokens.

πŸ› οΈ Technical Implementation Blueprint

Architecture & Flow

sequenceDiagram
    participant User as User Browser
    participant FE as React Frontend
    participant OAuth as OAuth Provider (Google/GitHub)
    participant BE as FastAPI Backend
    
    User->>FE: Click "Login with Google/GitHub"
    FE->>OAuth: Redirect/Trigger PopUp with client_id
    User->>OAuth: Authenticate & Authorize
    OAuth-->>FE: Return Auth Code / ID Token
    FE->>BE: POST /api/auth/google (ID Token)
    BE->>OAuth: Verify Token Signature (via public certs)
    BE-->>BE: Extract email, name, avatar
    BE-->>BE: Generate custom signed JWT (HS256)
    BE-->>FE: Return Custom JWT Token + User info
    FE->>FE: Set JWT in localStorage / Cookie

Frontend Setup (Google Example)

Wrap the app in GoogleOAuthProvider and use useGoogleLogin:

import { useGoogleLogin } from '@react-oauth/google';

const login = useGoogleLogin({
  onSuccess: async (tokenResponse) => {
    const res = await fetch(`${BASE}/api/auth/google`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ token: tokenResponse.access_token })
    });
    const data = await res.json();
    onLogin(data.user.email, data.user.name, data.token); // Save user & JWT
  },
  onError: () => console.log('Login Failed')
});

Backend Setup (FastAPI)

Add route to verify and register standard profiles:

from google.oauth2 import id_token
from google.auth.transport import requests

@router.post("/auth/google")
async def auth_google(payload: OAuthPayload, db: Session = Depends(get_db)):
    try:
        idinfo = id_token.verify_oauth2_token(payload.token, requests.Request(), GOOGLE_CLIENT_ID)
        email = idinfo['email']
        name = idinfo.get('name', '')
        
        user = get_or_create_user(email, name, db)
        access_token = create_access_token({"sub": user.email, "role": user.role})
        return {"token": access_token, "user": {"email": email, "name": name}}
    except ValueError:
        raise HTTPException(status_code=400, detail="Invalid token verification")

3. Multi-Provider Payment Architecture (Stripe, Paystack, Binance Pay)

To prevent vendor lock-in and support regional markets (e.g. Stripe for global cards, Paystack for Africa, Binance Pay for cryptocurrency), DocuFlow employs the Dependency Injection (DI) design pattern.

πŸ“‹ TODO Checklist

  • Define Python Abstract Base Class (ABC) for PaymentGateway.
  • Implement StripeGateway, PaystackGateway, and BinancePayGateway subclasses.
  • Configure billing_provider parameter in the database system_configs table.
  • Setup unified /api/billing/create-checkout-session FastAPI route utilizing DI.
  • Create a unified webhook router /api/billing/webhooks/{provider} to handle payment verification.

3.1 Dependency Injection Design Pattern

Define an abstract interface to decouple backend controllers from specific gateways:

from abc import ABC, abstractmethod
from typing import Dict, Any

class PaymentGateway(ABC):
    @abstractmethod
    async def create_checkout_session(self, customer_email: str, amount_cents: int, success_url: str, cancel_url: str) -> str:
        """Initiates a payment order and returns the redirect checkout URL."""
        pass

    @abstractmethod
    async def verify_webhook(self, payload: bytes, signature: str) -> Dict[str, Any]:
        """Validates the webhook signature and returns unified event dict with keys: 'email', 'status', 'provider'."""
        pass

We resolve the dependency dynamically at runtime using FastAPI's dependency injection container:

from fastapi import Depends
from backend.database import get_db
from backend.dependencies import get_config_value

def get_payment_gateway(db: Session = Depends(get_db)) -> PaymentGateway:
    provider = get_config_value("billing_provider", "stripe", db).lower()
    
    if provider == "stripe":
        return StripeGateway(
            secret_key=os.getenv("STRIPE_SECRET_KEY"),
            webhook_secret=os.getenv("STRIPE_WEBHOOK_SECRET"),
            price_id=os.getenv("STRIPE_PRO_PRICE_ID")
        )
    elif provider == "paystack":
        return PaystackGateway(
            secret_key=os.getenv("PAYSTACK_SECRET_KEY")
        )
    elif provider == "binance":
        return BinancePayGateway(
            api_key=os.getenv("BINANCE_API_KEY"),
            api_secret=os.getenv("BINANCE_API_SECRET")
        )
    else:
        raise HTTPException(status_code=400, detail=f"Billing provider '{provider}' is not supported.")

3.2 Stripe Gateway Implementation

import stripe
from typing import Dict, Any

class StripeGateway(PaymentGateway):
    def __init__(self, secret_key: str, webhook_secret: str, price_id: str):
        self.secret_key = secret_key
        self.webhook_secret = webhook_secret
        self.price_id = price_id
        stripe.api_key = self.secret_key

    async def create_checkout_session(self, customer_email: str, amount_cents: int, success_url: str, cancel_url: str) -> str:
        session = stripe.checkout.Session.create(
            line_items=[{
                'price': self.price_id,
                'quantity': 1,
            }],
            mode='subscription',
            success_url=success_url,
            cancel_url=cancel_url,
            customer_email=customer_email
        )
        return session.url

    async def verify_webhook(self, payload: bytes, signature: str) -> Dict[str, Any]:
        try:
            event = stripe.Webhook.construct_event(
                payload, signature, self.webhook_secret
            )
        except (ValueError, stripe.error.SignatureVerificationError):
            raise ValueError("Invalid signature")

        if event["type"] == "checkout.session.completed":
            session = event["data"]["object"]
            return {
                "email": session["customer_email"],
                "status": "success",
                "provider": "stripe"
            }
        return {"status": "ignored"}

3.3 Paystack Gateway Implementation

Paystack uses a REST API to initialize transactions, and verifies webhooks by computing an HMAC-SHA512 signature of the request body using the API Secret Key.

import hmac
import hashlib
import json
import httpx
from typing import Dict, Any

class PaystackGateway(PaymentGateway):
    def __init__(self, secret_key: str):
        self.secret_key = secret_key

    async def create_checkout_session(self, customer_email: str, amount_cents: int, success_url: str, cancel_url: str) -> str:
        async with httpx.AsyncClient() as client:
            response = await client.post(
                "https://api.paystack.co/transaction/initialize",
                headers={
                    "Authorization": f"Bearer {self.secret_key}",
                    "Content-Type": "application/json"
                },
                json={
                    "email": customer_email,
                    "amount": amount_cents,  # in kobo (cents equivalent)
                    "callback_url": success_url
                }
            )
            if response.status_code != 200:
                raise Exception(f"Paystack transaction initialization failed: {response.text}")
            
            data = response.json()
            return data["data"]["authorization_url"]

    async def verify_webhook(self, payload: bytes, signature: str) -> Dict[str, Any]:
        # Perform security verification of payload using HMAC-SHA512
        computed_sig = hmac.new(
            self.secret_key.encode(), payload, hashlib.sha512
        ).hexdigest()
        
        if not hmac.compare_digest(computed_sig, signature):
            raise ValueError("Invalid Paystack signature")

        event = json.loads(payload.decode())
        if event.get("event") == "charge.success":
            data = event["data"]
            return {
                "email": data["customer"]["email"],
                "status": "success",
                "provider": "paystack"
            }
        return {"status": "ignored"}

3.4 Binance Pay (Crypto) Implementation

Binance Pay uses an HMAC-SHA512 signing mechanism to verify request integrity. The signature is placed in the BinancePay-Signature header.

import hmac
import hashlib
import time
import secrets
import json
import httpx
from typing import Dict, Any

class BinancePayGateway(PaymentGateway):
    def __init__(self, api_key: str, api_secret: str):
        self.api_key = api_key
        self.api_secret = api_secret

    def _generate_signature(self, payload_str: str, timestamp: str, nonce: str) -> str:
        message = f"{timestamp}\n{nonce}\n{payload_str}\n"
        return hmac.new(
            self.api_secret.encode(), message.encode(), hashlib.sha512
        ).hexdigest().upper()

    async def create_checkout_session(self, customer_email: str, amount_cents: int, success_url: str, cancel_url: str) -> str:
        timestamp = str(int(time.time() * 1000))
        nonce = secrets.token_hex(16)
        
        order_payload = {
            "env": {"terminalType": "WEB"},
            "merchantTradeNo": f"trade-{int(time.time())}-{secrets.token_hex(4)}",
            "orderAmount": float(amount_cents) / 100.0,
            "currency": "USDT",
            "goods": {
                "goodsType": "01",
                "goodsCategory": "6000",
                "referenceGoodsId": "pro_sub",
                "goodsName": "DocuFlow Pro Subscription"
            },
            "buyer": {"buyerEmail": customer_email},
            "returnUrl": success_url,
            "cancelUrl": cancel_url
        }
        
        payload_str = json.dumps(order_payload)
        signature = self._generate_signature(payload_str, timestamp, nonce)
        
        async with httpx.AsyncClient() as client:
            response = await client.post(
                "https://bpay.binanceapi.com/binancepay/openapi/v2/order",
                headers={
                    "BinancePay-Timestamp": timestamp,
                    "BinancePay-Nonce": nonce,
                    "BinancePay-Certificate-SN": self.api_key,
                    "BinancePay-Signature": signature,
                    "Content-Type": "application/json"
                },
                json=order_payload
            )
            if response.status_code != 200:
                raise Exception(f"Binance Pay order creation failed: {response.text}")
                
            data = response.json()
            if data.get("status") != "SUCCESS":
                raise Exception(f"Binance Pay error: {data.get('errorMessage')}")
                
            return data["data"]["checkoutUrl"]

    async def verify_webhook(self, payload: bytes, signature: str) -> Dict[str, Any]:
        # Note: In production Binance Pay webhooks, you receive headers BinancePay-Timestamp and BinancePay-Nonce
        # which must be combined with the raw body to verify signature.
        # This implementation represents the internal payload hash comparison logic:
        computed_sig = hmac.new(
            self.api_secret.encode(), payload, hashlib.sha256
        ).hexdigest().upper()
        
        if not hmac.compare_digest(computed_sig, signature):
             raise ValueError("Invalid Binance Pay signature")
             
        event = json.loads(payload.decode())
        if event.get("bizType") == "PAY" and event.get("bizStatus") == "PAY_SUCCESS":
            # Extract buyer email mapped inside merchant trade details
            biz_detail = event["bizDetail"]
            return {
                "email": biz_detail.get("buyer", {}).get("buyerEmail"),
                "status": "success",
                "provider": "binance"
            }
        return {"status": "ignored"}

3.5 FastAPI Routes & Webhooks Router

By utilizing Dependency Injection, we declare clean, controller-agnostic endpoints:

from fastapi import APIRouter, Depends, Header, Request, HTTPException
from sqlalchemy.orm import Session
from backend.database import get_db

router = APIRouter(prefix="/api/billing", tags=["Billing System"])

@router.post("/create-checkout-session")
async def create_checkout(
    user: dict = Depends(verify_jwt_token),
    gateway: PaymentGateway = Depends(get_payment_gateway)
):
    try:
        checkout_url = await gateway.create_checkout_session(
            customer_email=user["sub"],
            amount_cents=900,  # $9.00 USD / USDT
            success_url=f"{APP_URL}/editor?payment=success",
            cancel_url=f"{APP_URL}/pricing"
        )
        return {"url": checkout_url}
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Billing session creation failed: {str(e)}")

@router.post("/webhooks/{provider}")
async def billing_webhook(
    provider: str,
    request: Request,
    db: Session = Depends(get_db),
    gateway: PaymentGateway = Depends(get_payment_gateway)
):
    # Verify the route matches the active configured provider
    active_provider = get_config_value("billing_provider", "stripe", db).lower()
    if provider != active_provider:
        raise HTTPException(status_code=400, detail="Invalid webhook endpoint path for configured gateway.")

    # Retrieve headers
    signature = ""
    if provider == "stripe":
        signature = request.headers.get("stripe-signature", "")
    elif provider == "paystack":
        signature = request.headers.get("x-paystack-signature", "")
    elif provider == "binance":
        signature = request.headers.get("BinancePay-Signature", "")

    payload = await request.body()
    try:
        event = await gateway.verify_webhook(payload, signature)
        if event.get("status") == "success":
            update_user_tier(event["email"], tier="pro", db=db)
            return {"status": "upgraded"}
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
        
    return {"status": "ignored"}

4. Client-Side Cryptographic Key Derivation (Web Crypto API)

πŸ“‹ TODO Checklist

  • Implement PBKDF2 key derivation from user's custom Private Key Seed.
  • Use Web Crypto API (SubtleCrypto) to encrypt document payloads locally before saving.
  • Apply AES-GCM 256-bit symmetric encryption.
  • Wrap document content read/writes in custom hooks, decrypting state on recovery.

πŸ› οΈ Technical Implementation Blueprint

Cryptographic Helpers (cryptoUtils.ts)

Implement standard browser cryptographic APIs:

// Derive key using PBKDF2
async def deriveKey(seed: string, salt: Uint8Array): Promise<CryptoKey> {
  const enc = new TextEncoder();
  const baseKey = await window.crypto.subtle.importKey(
    "raw", enc.encode(seed), "PBKDF2", false, ["deriveKey"]
  );
  return window.crypto.subtle.deriveKey(
    { name: "PBKDF2", salt, iterations: 100000, hash: "SHA-256" },
    baseKey,
    { name: "AES-GCM", length: 256 },
    false,
    ["encrypt", "decrypt"]
  );
}

// Encrypt plaintext markdown content
export async function encryptContent(plaintext: string, seed: string): Promise<{ ciphertext: string, salt: string, iv: string }> {
  const enc = new TextEncoder();
  const salt = window.crypto.getRandomValues(new Uint8Array(16));
  const iv = window.crypto.getRandomValues(new Uint8Array(12));
  const key = await deriveKey(seed, salt);
  const ciphertextBuffer = await window.crypto.subtle.encrypt(
    { name: "AES-GCM", iv }, key, enc.encode(plaintext)
  );
  
  return {
    ciphertext: btoa(String.fromCharCode(...new Uint8Array(ciphertextBuffer))),
    salt: btoa(String.fromCharCode(...salt)),
    iv: btoa(String.fromCharCode(...iv))
  };
}

// Decrypt ciphertext
export async function decryptContent(ciphertextB64: string, seed: string, saltB64: string, ivB64: string): Promise<string> {
  const dec = new TextDecoder();
  const salt = Uint8Array.from(atob(saltB64), c => c.charCodeAt(0));
  const iv = Uint8Array.from(atob(ivB64), c => c.charCodeAt(0));
  const ciphertext = Uint8Array.from(atob(ciphertextB64), c => c.charCodeAt(0));
  const key = await deriveKey(seed, salt);
  
  const decryptedBuffer = await window.crypto.subtle.decrypt(
    { name: "AES-GCM", iv }, key, ciphertext
  );
  return dec.decode(decryptedBuffer);
}

5. Real-time Multi-user Collaboration (FastAPI WebSockets + Y.js)

πŸ“‹ TODO Checklist

  • Install collaboration libraries (yjs, y-websocket on frontend; y-py or node wrapper on backend).
  • Mount a /ws/collaborate/{doc_id} endpoint in FastAPI main.py to manage live connections.
  • Track active collaborators per document dynamically.
  • Broadcast Y.js state updates and mouse cursor coordinate differentials to all connected socket clients.

πŸ› οΈ Technical Implementation Blueprint

Backend WebSocket Manager (FastAPI)

Coordinate multiple connections and broadcast binary state updates:

from fastapi import WebSocket

class ConnectionManager:
    def __init__(self):
        self.active_connections: dict[str, list[WebSocket]] = {}

    async def connect(self, doc_id: str, websocket: WebSocket):
        await websocket.accept()
        if doc_id not in self.active_connections:
            self.active_connections[doc_id] = []
        self.active_connections[doc_id].append(websocket)

    def disconnect(self, doc_id: str, websocket: WebSocket):
        self.active_connections[doc_id].remove(websocket)

    async def broadcast(self, doc_id: str, message: bytes, sender: WebSocket):
        for connection in self.active_connections.get(doc_id, []):
            if connection != sender:
                await connection.send_bytes(message)

manager = ConnectionManager()

@app.websocket("/ws/collaborate/{doc_id}")
async def websocket_endpoint(websocket: WebSocket, doc_id: str):
    await manager.connect(doc_id, websocket)
    try:
        while True:
            data = await websocket.receive_bytes()
            await manager.broadcast(doc_id, data, sender=websocket)
    except Exception:
        manager.disconnect(doc_id, websocket)

Frontend Editor Binding (React)

Wire Y.js shared document states directly to the editor state:

import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";

useEffect(() => {
  const ydoc = new Y.Doc();
  const provider = new WebsocketProvider(
    `ws://127.0.0.1:8000/ws/collaborate/${activeDocId}`, 
    activeDocId, 
    ydoc
  );
  const ytext = ydoc.getText("markdown");
  
  ytext.observe(event => {
    setEditorText(ytext.toString());
  });

  return () => {
    provider.destroy();
    ydoc.destroy();
  };
}, [activeDocId]);