Files
maikrolf 99c113b6f5 fix: bw/stats 404, gateway links, history crash
- swr.ts: remove useBandwidth(), drop bw from useDashboard()
- events/route.ts: skip bandwidth SSE (Kubo 0.28.0 unsupported)
- usage/page.tsx: remove getBWStats call + bw references
- helpers.ts: gatewayLink -> ipfs.maos.dedyn.io (subdomain only)
- upload/page.tsx: fix local gatewayLink (subdomain only)
- storage.ts: update default gatewayUrl, add defensive getHistory()
2026-07-05 16:57:29 +02:00

307 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# IPFS Portal — Architecture
## Overview
IPFS Portal is a **Next.js SPA** (Single Page Application) that provides a browser-based interface for managing an IPFS node, uploading files, and handling crypto payments via a smart contract. All pages are `'use client'` — no server-side rendering.
### Stack
| Layer | Technology |
|---|---|
| Frontend | Next.js 14+ (React, `'use client'` SPA) |
| Styling | Tailwind CSS + custom `glass`/`surface` design tokens |
| Icons | Lucide React |
| Wallet | EIP-6963 + viem |
| Blockchain | zkSync Local (chain 270) |
| IPFS | Kubo RPC API (port 5001) |
| User Mgmt | Python backend (port 8444) |
| IPFS Gateway | `https://maos.dedyn.io/ipfs/<CID>` |
---
## System Architecture
```
BROWSER (React SPA)
┌─────────────────────┼──────────────────────┐
│ │ │
▼ ▼ ▼
11 Pages (SPA) WalletProvider PaymentService
┌──────────────┐ (EIP-6963) (viem, chain 270)
│ apiFetch() │ │ │
│ wrapper │ │ │
└──────┬───────┘ │ │
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────┐
│ Next.js API Route: [...path]/route.ts │
│ │
│ matchRoute(path) → │
│ 'health' → inline JSON │
│ 'users' → proxyUserAPI() → :8444 (Python) │
│ default → proxyIPFS() → :5001 (Kubo) │
│ │
│ mapToKuboAPI(path) → /api/v0/* translation │
└─────────────────────┬───────────────────────────────┘
┌─────────────┴─────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Kubo IPFS │ │ Python User API │
│ Daemon │ │ (port 8444) │
│ (port 5001) │ │ │
│ │ │ htpasswd mgmt │
│ /api/v0/id │ │ Auth: X-Admin- │
│ /api/v0/swarm │ │ Key header │
│ /api/v0/pin/* │ │ │
│ /api/v0/add │ └──────────────────┘
│ /api/v0/ls │
│ /api/v0/name/ │
│ /api/v0/repo │ CHAIN 270
│ /api/v0/bw │ ┌──────────────────┐
└─────────────────┘ │ Smart Contract │
│ 0xCBc6... │
│ zkSync Local │
│ RPC :3050 │
│ │
│ payWithETH() │
│ payWithToken() │
│ getPrice() │
│ configureToken()│
└──────────────────┘
```
---
## Pages
| Page | Route | Lines | Backend Dependencies | Description |
|---|---|---|---|---|
| Landing | `/` | ~130 | Kubo (health) | Hero + connect wallet + quick stats |
| Dashboard | `/dashboard` | 253 | Kubo (peers, pins, health, repo, bw) | Node overview, stats, bandwidth |
| Upload | `/upload` | **590** | Kubo + Smart Contract | Quick + crypto file upload |
| Explorer | `/explorer` | 244 | Kubo (ls, cat) | IPFS content browser |
| IPNS | `/ipns` | 285 | Kubo (keys, publish, resolve) | IPNS name management |
| Users | `/users` | 275 | Python API + Smart Contract | htpasswd users + wallet lookup |
| Peers | `/peers` | 63 | Kubo (swarm/peers) | Connected peers list |
| Pins | `/pins` | 167 | Kubo (pin/ls, add, rm) | Pin management |
| Admin/Payment | `/admin/payment` | **536** | Smart Contract | Owner payment dashboard |
| Settings | `/settings` | 319 | localStorage only | App configuration |
| History | `/history` | 393 | localStorage only | Upload history viewer |
**Note**: IPNS page does NOT wrap in `PortalLayout` — appears standalone. All other pages use the shared layout.
---
## Data Flow
### 1. API Flow (all pages except Settings, History)
```
Page Component
→ apiFetch('/api/endpoint')
→ fetch('/api/endpoint', { credentials: 'include' })
→ Next.js [...path]/route.ts
→ matchRoute(path)
→ proxyIPFS() → Kubo RPC :5001
→ proxyUserAPI() → Python :8444
```
### 2. Upload Flow (Quick)
```
User drags file
→ UploadPage state: FileEntry[]
→ startUpload() — sequential per file
→ uploadFile(file) → apiFetch POST /api/files/upload (FormData)
→ proxyIPFS → Kubo /api/v0/add
→ addToHistory({ cid, name, size }) → localStorage
→ Show results (CID, gateway link, copy)
```
### 3. Upload Flow (Crypto)
```
User selects file
→ PaymentPanel shown
→ WalletProvider.connectWallet() → get address
→ PaymentService.getPrice(bytes, addr) → { finalWei, isFree }
→ PaymentService.getSupportedTokens() → [ETH, USDC, MAOS]
→ PaymentService.getMAOSDiscountTiers() → discount %
→ User pays
→ Step 1: uploadFile(file) → Kubo → get CID
→ Step 2a (free): onPaid({ method: 'free' })
→ Step 2b (ETH): payWithETH(provider, cid, size, wei)
→ sendTransaction → contract.payWithETH(cid, bytesSize)
→ Step 2c (token): approveAndPayWithToken(...)
→ check allowance → approve if needed → contract.payWithToken(...)
→ addToHistory({ method: 'eth' }) → localStorage
```
### 4. Payment Admin Flow
```
Admin connects wallet
→ Check isOwner (compare connected address to contract.owner)
→ Load: getOwner, getBasePricePerMB, getFreeTierBytes,
getSupportedTokens, getMAOSDiscountTiers, getTotalUploads
→ Per token: getTotalRevenue, getBalance (ETH) / balanceOf (ERC20)
→ Owner can: setPrice, setFreeTier, configureToken,
add/remove discount tiers, withdraw funds
```
---
## Key Modules
### `src/lib/api.ts` (217 lines)
Central API layer. Exports:
| Function | HTTP | Endpoint | Category |
|---|---|---|---|
| `getNodeInfo()` | GET | `/api/node/info` | Node |
| `getPeers()` | GET | `/api/peers` | Peers |
| `listPins()` / `addPin()` / `removePin()` | GET/POST/DELETE | `/api/pins` | Pins |
| `uploadFile(file)` | POST (FormData) | `/api/files/upload` | Files |
| `listUsers()` / `createUser()` / `deleteUser()` | GET/POST/DELETE | `/api/users` | Users |
| `explorerLs()` / `explorerCat()` / `explorerStat()` | POST | `/api/explorer/*` | Explorer |
| `listIPNSKeys()` / `ipnsPublish()` / `ipnsResolve()` / `ipnsGenKey()` | GET/POST | `/api/ipns/*` | IPNS |
| `getRepoStats()` / `getBWStats()` | GET | `/api/repo` / `/api/bw` | Stats |
| `checkHealth()` | GET | `/api/health` | Health |
### `src/lib/wallet.ts` (196 lines)
Wallet connection layer.
- **EIP-6963** event-based provider discovery (`discoverWallets`)
- Legacy `window.ethereum` fallback
- MAOS Wallet preferred (`window.maosv6`)
- Chain 270 (zkSync Local) switch + add
- Helpers: `formatWeiToETH`, `formatBytes`, `signMessage`, `getBalance`
### `src/lib/payment.ts` (539 lines)
Smart contract interaction via viem.
- 28 methods wrapping ABI functions
- Supports ETH and ERC20 token payments
- Read: `getPrice`, `getUserStats`, `getSupportedTokens`, `getMAOSDiscountTiers`
- Write: `payWithETH`, `approveAndPayWithToken`
- Admin: `adminSetPricePerMB`, `adminConfigureToken`, `adminWithdrawETH`, etc.
- Contract: `0xCBc6b8aeea129c206F4836799621C833Bf8B9BDe` on chain 270
### `src/lib/storage.ts` (98 lines)
localStorage persistence.
- `PortalSettings`: gateway URL, API endpoint, storage max, refresh, theme
- `UploadRecord`: history with dedup by CID+name, capped at 500 entries
### `src/app/api/[...path]/route.ts` (~320 lines)
Catch-all API proxy. Routes:
| Path | Handler | Backend |
|---|---|---|
| `/api/health` | `handleHealth()` | Inline JSON |
| `/api/users*` | `proxyUserAPI()` | Python :8444 |
| `/api/explorer/*` / `/api/ipns/*` / default | `proxyIPFS()` | Kubo :5001 |
**`mapToKuboAPI(path)`** translates Portal API paths to Kubo RPC endpoints:
- `/node/info``/api/v0/id`
- `/peers``/api/v0/swarm/peers`
- `/pins``/api/v0/pin/ls|add|rm`
- `/files/upload``/api/v0/add`
- `/explorer/ls``/api/v0/ls`
- `/ipns/publish``/api/v0/name/publish`
- `/repo/stats``/api/v0/repo/stat`
- `/bw/stats``/api/v0/bw`
---
## Smart Contract (Chain 270 — zkSync Local)
**Address**: `0xCBc6b8aeea129c206F4836799621C833Bf8B9BDe`
**RPC**: `http://tom1687.no-ip.org:3050`
### Functions
| Category | Functions |
|---|---|
| **Payment** | `payWithETH(cid, bytesSize)` (payable), `payWithToken(cid, bytes, symbol)` |
| **Pricing** | `getPrice(bytes, user)``(totalWei, discountBps, finalWei, isFree)` |
| **Free Tier** | `freeTierBytes()`, `userTotalBytes(user)` |
| **Token Mgmt** | `configureToken(symbol, addr, decimals, weiPerToken, enabled)` |
| **Discounts** | `getMAOSDiscountTiers()`, `setMAOSDiscountTier(minBal, bps)`, `removeMAOSDiscountTier(index)` |
| **Admin** | `setPricePerMB(wei)`, `setFreeTierBytes(bytes)`, `withdrawETH(to)`, `withdrawToken(symbol, to)`, `transferOwnership(to)` |
| **Stats** | `totalRevenue(symbol)`, `totalUploads()`, `getUserStats(user)` |
| **Events** | `UploadPaidETH`, `UploadPaidToken`, `FreeUploadUsed` |
### Known Bug
Contract returns duplicate token entries — admin page deduplicates on symbol.
---
## Graph Analysis
### Static (AST) View
- **251 nodes**, **401 edges**, **17 communities**
- Graphify knowledge graph at `graphify-out/graph.json`
- **79 isolated nodes** (type definitions, interfaces, scripts)
### Bridge Nodes
| Node | Edges | Role |
|---|---|---|
| `PaymentService` | 31 | Bridges Smart Contract ↔ UI (Payments) |
| `WalletProvider` | 15 | Bridges Wallet ↔ PaymentService |
### Graph Gap
The AST parser cannot trace URL-routed connections. The runtime flow:
```
apiFetch('/api/node/info') → fetch('/api/node/info')
→ [...path]/route.ts → proxyIPFS() → Kubo :5001
```
Appears as 3 disconnected communities in the graph but is one continuous flow at runtime.
---
## Code Quality Summary
### Issues Found (24 total)
| Severity | Count | Key Items |
|---|---|---|
| 🔴 Critical | 4 | XSS, unhandled promises, silent catch blocks, oversized files |
| 🟡 Medium | 5 | 15× `@ts-expect-error`, 8× `as any`, duplicate helpers, precision loss |
| 🟢 Low | 3 | Unused imports, console.* logging, IPNS layout inconsistency |
### Priority Fixes
1. **Window type augmentation** — Declare `Window` interface in `wallet.ts` to eliminate 15 `@ts-expect-error`
2. **Extract duplicate patterns** — Consolidate `formatBytes`, `gatewayLink`, `truncateCid` into `lib/helpers.ts`
3. **Add error logging** — Every empty catch block gets `console.error` or structured logging
4. **Split oversized files**`upload/page.tsx` (590→2 components), `admin/payment/page.tsx` (536→4 views), `payment.ts` (539→abi/types/service)
5. **Fix XSS** — Add DOMPurify or replace `dangerouslySetInnerHTML` in `FilePreview.tsx`
6. **Fix unhandled promises** — Add `.catch()` to `isDeployed().then()`
7. **Replace `as any` casts** — Use proper viem `parseAbi` types instead of `as unknown as X`
---
## Configuration
- **API_BASE**: `''` (empty — same origin; nginx routes `/api/*` to backend)
- **Kubo RPC**: `http://127.0.0.1:5001`
- **Python API**: `http://192.168.1.176:8444` (htpasswd + admin key)
- **Smart Contract**: `0xCBc6...` on chain 270 (zkSync Local)
- **IPFS Gateway**: `https://maos.dedyn.io/ipfs/`
- **Refresh Interval**: 15s (Dashboard)
- **Max Uploads**: 20 files, 100MB each
- **Storage Max**: 30 GB (localStorage setting)
- **Admin Key**: `maos-admin-2024` (X-Admin-Key header)