| # 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<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: |
| ```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]); |
| ``` |
|
|