SIWE auth, API proxy fixes, dashboard, login, usage pages

- SIWE auth flow: challenge/login/logout/me API routes + JWT middleware
- Fixed [...path] catch-all: Kubo proxy with matchRoute() + mapToKuboAPI()
- Fixed session_id → nonce field mismatch in AuthProvider
- Fixed dashboard pins crash: Kubo returns {Keys: {...}}, not array
- Added middleware route guard (307 redirect to /login)
- Added login, register, profile, usage, upload pages
- Added dashboard components (StorageGauge, FreeTierProgress)
- Added SWR hooks + AuthGuard + Providers + Toast/ErrorBoundary
- Added payment integration (smart contract ABI, tiers, tokens)
- Fixed IPNS key listing: Name/Id → name/id mapping
- Added PWA support (manifest, service worker, icons)
- Added tests for helpers, limits, search-index, wallet
This commit is contained in:
maikrolf
2026-06-28 18:31:05 +02:00
parent 1ddc89479c
commit c9432a16ba
84 changed files with 6679 additions and 1426 deletions
@@ -0,0 +1,54 @@
'use client';
/* ════════════════════════════ FreeTierProgress ════════════════════════════ */
interface FreeTierProgressProps {
used: number;
max: number;
label?: string;
}
export default function FreeTierProgress({
used,
max,
label = 'Free uploads vandaag',
}: FreeTierProgressProps) {
const pct = max > 0 ? Math.min((used / max) * 100, 100) : 0;
const remaining = Math.max(0, max - used);
const color =
remaining === 0
? 'bg-accent-rose'
: remaining < 5
? 'bg-accent-amber'
: 'bg-accent-green';
return (
<div className="glass rounded-xl p-5">
<div className="flex items-center justify-between mb-2">
<span className="text-xs text-surface-400">{label}</span>
<span className={`text-xs font-mono font-medium ${
remaining === 0 ? 'text-accent-rose' : 'text-accent-green'
}`}>
{remaining} / {max}
</span>
</div>
<div className="h-2.5 rounded-full bg-surface-700 overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-700 ease-out ${color}`}
style={{ width: `${pct}%` }}
/>
</div>
<div className="flex justify-between mt-1.5">
<span className="text-[10px] text-surface-500">
{used} gebruikt vandaag
</span>
{remaining <= 3 && remaining > 0 && (
<span className="text-[10px] text-accent-amber">Bijna op</span>
)}
{remaining === 0 && (
<span className="text-[10px] text-accent-rose">Limiet bereikt</span>
)}
</div>
</div>
);
}
@@ -0,0 +1,43 @@
'use client';
/* ════════════════════════════ StorageGauge ════════════════════════════ */
interface StorageGaugeProps {
usedGB: number;
maxGB: number;
label?: string;
}
export default function StorageGauge({ usedGB, maxGB, label = 'Storage' }: StorageGaugeProps) {
const pct = maxGB > 0 ? Math.min((usedGB / maxGB) * 100, 100) : 0;
const color =
pct > 90 ? 'bg-accent-rose' : pct > 70 ? 'bg-accent-amber' : 'bg-accent-cyan';
const textColor =
pct > 90 ? 'text-accent-rose' : pct > 70 ? 'text-accent-amber' : 'text-accent-cyan';
return (
<div className="glass rounded-xl p-5">
<div className="flex items-center justify-between mb-2">
<span className="text-xs text-surface-400">{label}</span>
<span className={`text-xs font-mono font-medium ${textColor}`}>
{usedGB.toFixed(1)} / {maxGB.toFixed(1)} GB
</span>
</div>
<div className="h-2.5 rounded-full bg-surface-700 overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-700 ease-out ${color}`}
style={{ width: `${pct}%` }}
/>
</div>
<div className="flex justify-between mt-1.5">
<span className="text-[10px] text-surface-500">{pct.toFixed(0)}% gebruikt</span>
{pct > 80 && (
<span className="text-[10px] text-accent-rose flex items-center gap-1">
Bijna vol
</span>
)}
</div>
</div>
);
}
+57 -47
View File
@@ -1,13 +1,12 @@
'use client';
import { useEffect, useState } from 'react';
import { getPeers, listPins, checkHealth, getRepoStats, getBWStats } from '@/lib/api';
import type { RepoStats, BWStats } from '@/lib/api';
import PortalLayout from '@/app/layout-portal';
import Link from 'next/link';
import { SkeletonCard, SkeletonRow } from '@/components/Skeleton';
import { useDashboard } from '@/lib/swr';
import { useRealtime } from '@/lib/useRealtime';
import {
HardDrive,
Globe,
Wifi,
Database,
Server,
@@ -17,49 +16,42 @@ import {
Fingerprint,
} from 'lucide-react';
interface PeerData { id: string; addr: string; latency: string }
interface PinData { cid: string; name: string; size: number }
export default function DashboardPage() {
const [peers, setPeers] = useState<PeerData[]>([]);
const [pins, setPins] = useState<PinData[]>([]);
const [health, setHealth] = useState<{ status: string; node: string } | null>(null);
const [repo, setRepo] = useState<RepoStats | null>(null);
const [bw, setBW] = useState<BWStats | null>(null);
const [loading, setLoading] = useState(true);
const { data, isValidating } = useDashboard();
const realtime = useRealtime();
useEffect(() => {
async function load() {
try {
const [h, p, pinList, r, b] = await Promise.all([
checkHealth().catch(() => null),
getPeers().catch(() => [] as PeerData[]),
listPins().catch(() => [] as PinData[]),
getRepoStats().catch(() => null),
getBWStats().catch(() => null),
]);
setHealth(h);
setPeers(p);
setPins(pinList);
setRepo(r);
setBW(b);
} finally {
setLoading(false);
// Use SWR data, fall back to SSE live data where available
const peers = data?.peers ?? [];
const pins = data?.pins ?? [];
const health = data?.health;
const repo = data?.repo;
// Bandwidth: prefer SSE live data, fallback to SWR
const bwLive = realtime.bandwidth;
const bw = bwLive
? {
totalIn: bwLive.totalIn,
totalOut: bwLive.totalOut,
rateIn: bwLive.rateIn,
rateOut: bwLive.rateOut,
}
}
load();
const iv = setInterval(load, 15000);
return () => clearInterval(iv);
}, []);
: data?.bw;
const loading = isValidating && !data;
const repoSizeGb = repo ? (repo.repoSize / 1e9).toFixed(2) : '—';
const bwIn = bw ? (bw.totalIn / 1e6).toFixed(1) : '—';
const bwOut = bw ? (bw.totalOut / 1e6).toFixed(1) : '—';
// Peer count from SSE (live) or SWR
const peerCount = realtime.peers?.count ?? peers.length;
// Pin count from SWR
const pinCount = pins.length;
const stats = [
{ label: 'Peers', value: peers.length, icon: Wifi, color: 'text-accent-cyan', bg: 'bg-accent-cyan/10' },
{ label: 'Pins', value: pins.length, icon: HardDrive, color: 'text-accent-purple', bg: 'bg-accent-purple/10' },
{ label: 'Node', value: health?.status ?? '—', icon: Server, color: 'text-accent-green', bg: 'bg-accent-green/10' },
{ label: 'Peers', value: peerCount, icon: Wifi, color: 'text-accent-cyan', bg: 'bg-accent-cyan/10' },
{ label: 'Pins', value: pinCount, icon: HardDrive, color: 'text-accent-purple', bg: 'bg-accent-purple/10' },
{ label: 'Node', value: health?.status ?? (realtime.connected ? 'connected' : '—'), icon: Server, color: 'text-accent-green', bg: 'bg-accent-green/10' },
{ label: 'Repo', value: `${repoSizeGb} GB`, icon: Database, color: 'text-accent-amber', bg: 'bg-accent-amber/10' },
];
@@ -71,17 +63,33 @@ export default function DashboardPage() {
<h1 className="text-2xl font-bold text-white">Dashboard</h1>
<p className="text-sm text-surface-400 mt-1">IPFS node overview, storage, and bandwidth</p>
</div>
{loading && (
<div className="flex items-center gap-2 text-xs text-surface-500">
<div className="w-3 h-3 rounded-full border border-surface-600 border-t-transparent animate-spin" />
refreshing
</div>
)}
<div className="flex items-center gap-3">
{realtime.connected && (
<span className="flex items-center gap-1.5 text-xs text-accent-green">
<span className="w-1.5 h-1.5 rounded-full bg-accent-green animate-pulse" />
Live
</span>
)}
{!realtime.connected && (
<span className="flex items-center gap-1.5 text-xs text-surface-500">
<span className="w-1.5 h-1.5 rounded-full bg-surface-600" />
Polling
</span>
)}
{loading && (
<div className="flex items-center gap-2 text-xs text-surface-500">
<div className="w-3 h-3 rounded-full border border-surface-600 border-t-transparent animate-spin" />
loading
</div>
)}
</div>
</div>
{/* Stat cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
{stats.map((s) => (
{loading
? Array.from({ length: 4 }).map((_, i) => <SkeletonCard key={i} />)
: stats.map((s) => (
<div key={s.label} className="glass rounded-xl p-5 animate-fade-in">
<div className="flex items-center justify-between mb-3">
<div className={`p-2 rounded-lg ${s.bg}`}>
@@ -101,9 +109,11 @@ export default function DashboardPage() {
<div className="glass rounded-xl p-5 animate-fade-in">
<h2 className="text-sm font-semibold text-white mb-4 flex items-center justify-between">
<span className="flex items-center gap-2"><Wifi className="w-4 h-4 text-accent-cyan" />Connected Peers</span>
<span className="text-xs text-surface-500">{peers.length} peer{peers.length !== 1 ? 's' : ''}</span>
<span className="text-xs text-surface-500">{peerCount} peer{peerCount !== 1 ? 's' : ''}</span>
</h2>
{peers.length === 0 ? (
{loading
? Array.from({ length: 5 }).map((_, i) => <SkeletonRow key={i} cols={2} />)
: peers.length === 0 ? (
<p className="text-sm text-surface-500">No peers connected</p>
) : (
<div className="space-y-1.5 max-h-56 overflow-y-auto">