1
0
forked from maik/IPFS-portal

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()
This commit is contained in:
maikrolf
2026-07-05 16:57:29 +02:00
parent c9432a16ba
commit 99c113b6f5
42 changed files with 10081 additions and 263 deletions
+306
View File
@@ -0,0 +1,306 @@
# 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)