# 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](#1-simulated-features-registry) 2. [OAuth 2.0 (Google & GitHub) Authentication](#2-oauth-20-google--github-authentication) 3. [Multi-Provider Payment Architecture (Stripe, Paystack, Binance Pay)](#3-multi-provider-payment-architecture-stripe-paystack-binance-pay) - [3.1 Dependency Injection Design Pattern](#31-dependency-injection-design-pattern) - [3.2 Stripe Gateway Implementation](#32-stripe-gateway-implementation) - [3.3 Paystack Gateway Implementation](#33-paystack-gateway-implementation) - [3.4 Binance Pay (Crypto) Implementation](#34-binance-pay-crypto-implementation) - [3.5 FastAPI Routes & Webhooks Router](#35-fastapi-routes--webhooks-router) 4. [Client-Side Cryptographic Key Derivation (Web Crypto API)](#4-client-side-cryptographic-key-derivation-web-crypto-api) 5. [Real-time Multi-user Collaboration (FastAPI WebSockets + Y.js)](#5-real-time-multi-user-collaboration-fastapi-websockets--yjs) --- ## 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 ```mermaid 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`: ```typescript 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: ```python 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: ```python 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: ```python 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 ```python 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. ```python 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. ```python 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: ```python 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: ```typescript // Derive key using PBKDF2 async def deriveKey(seed: string, salt: Uint8Array): Promise { 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 { 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: ```python 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: ```typescript 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]); ```