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