forked from maik/IPFS-portal
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:
+186
@@ -0,0 +1,186 @@
|
||||
/* ── SWR Data Fetching Hooks ──
|
||||
*
|
||||
* Gelijktijdige data-fetching met auto-refresh, caching en revalidation.
|
||||
* Vervangt handmatige useEffect + useState + setInterval patronen.
|
||||
*
|
||||
* Alles fetched via de Next.js API proxy (krijgt cookies/sessie mee).
|
||||
*/
|
||||
|
||||
import useSWR from 'swr';
|
||||
import useSWRInfinite from 'swr/infinite';
|
||||
import type { IPFSPin, RepoStats, BWStats, IPFSNodeInfo, IPFSUser } from './api';
|
||||
|
||||
/* ════════════════════════════ Fetcher ════════════════════════════ */
|
||||
|
||||
const API_BASE = '';
|
||||
|
||||
async function fetcher<T>(path: string): Promise<T> {
|
||||
const res = await fetch(`${API_BASE}${path}`, { credentials: 'include' });
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new Error(`${res.status}: ${text.slice(0, 200)}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/* ════════════════════════════ Config ════════════════════════════ */
|
||||
|
||||
const DEFAULT_REFRESH = 15_000; // 15 seconden
|
||||
|
||||
/* ════════════════════════════ Pins ════════════════════════════ */
|
||||
|
||||
export function usePins(refreshInterval = DEFAULT_REFRESH) {
|
||||
return useSWR<IPFSPin[]>('/api/pins', fetcher, {
|
||||
refreshInterval,
|
||||
revalidateOnFocus: true,
|
||||
});
|
||||
}
|
||||
|
||||
/* ════════════════════════════ Peers ════════════════════════════ */
|
||||
|
||||
export interface Peer {
|
||||
id: string;
|
||||
addr: string;
|
||||
latency: string;
|
||||
}
|
||||
|
||||
export function usePeers(refreshInterval = DEFAULT_REFRESH) {
|
||||
return useSWR<Peer[]>('/api/peers', async (path) => {
|
||||
const raw = await fetcher<{ Peers?: { Peer: string; Addr: string; Latency: string }[] }>(path);
|
||||
return (raw.Peers || []).map((p) => ({
|
||||
id: p.Peer,
|
||||
addr: p.Addr,
|
||||
latency: p.Latency || '—',
|
||||
}));
|
||||
}, {
|
||||
refreshInterval,
|
||||
revalidateOnFocus: true,
|
||||
});
|
||||
}
|
||||
|
||||
/* ════════════════════════════ Node Info ════════════════════════════ */
|
||||
|
||||
export function useNodeInfo(refreshInterval?: number) {
|
||||
return useSWR<IPFSNodeInfo>('/api/node/info', fetcher, {
|
||||
refreshInterval,
|
||||
revalidateOnFocus: true,
|
||||
});
|
||||
}
|
||||
|
||||
/* ════════════════════════════ Repo Stats ════════════════════════════ */
|
||||
|
||||
export function useRepoStats(refreshInterval = DEFAULT_REFRESH) {
|
||||
return useSWR<RepoStats>('/api/repo/stats', async (path) => {
|
||||
const raw = await fetcher<{
|
||||
RepoSize?: number; StorageMax?: number; NumObjects?: number;
|
||||
RepoPath?: string; Version?: string;
|
||||
}>(path);
|
||||
return {
|
||||
repoSize: raw.RepoSize ?? 0,
|
||||
storageMax: raw.StorageMax ?? 0,
|
||||
numObjects: raw.NumObjects ?? 0,
|
||||
repoPath: raw.RepoPath ?? '',
|
||||
version: raw.Version ?? '',
|
||||
};
|
||||
}, {
|
||||
refreshInterval,
|
||||
revalidateOnFocus: true,
|
||||
});
|
||||
}
|
||||
|
||||
/* ════════════════════════════ Bandwidth ════════════════════════════ */
|
||||
|
||||
export function useBandwidth(refreshInterval = DEFAULT_REFRESH) {
|
||||
return useSWR<BWStats>('/api/bw/stats', async (path) => {
|
||||
const raw = await fetcher<{
|
||||
TotalIn?: number; TotalOut?: number; RateIn?: number; RateOut?: number;
|
||||
}>(path);
|
||||
return {
|
||||
totalIn: raw.TotalIn ?? 0,
|
||||
totalOut: raw.TotalOut ?? 0,
|
||||
rateIn: raw.RateIn ?? 0,
|
||||
rateOut: raw.RateOut ?? 0,
|
||||
};
|
||||
}, {
|
||||
refreshInterval,
|
||||
revalidateOnFocus: true,
|
||||
});
|
||||
}
|
||||
|
||||
/* ════════════════════════════ Dashboard (alles-in-1) ════════════════════════════ */
|
||||
|
||||
export interface DashboardData {
|
||||
peers: Peer[];
|
||||
pins: IPFSPin[];
|
||||
repo: RepoStats | null;
|
||||
bw: BWStats | null;
|
||||
health: { status: string; node: string } | null;
|
||||
}
|
||||
|
||||
export function useDashboard(refreshInterval = DEFAULT_REFRESH) {
|
||||
return useSWR<DashboardData>('/api/health', async (healthPath) => {
|
||||
const [health, peers, pins, repo, bw] = await Promise.all([
|
||||
fetcher<{ status: string; node: string }>(healthPath).catch(() => null as any),
|
||||
fetcher<{ Peers?: { Peer: string; Addr: string; Latency: string }[] }>('/api/peers')
|
||||
.then((raw) => (raw.Peers || []).map((p) => ({ id: p.Peer, addr: p.Addr, latency: p.Latency || '—' })))
|
||||
.catch(() => [] as Peer[]),
|
||||
fetcher<{ Keys?: Record<string, { Type: string }> }>('/api/pins')
|
||||
.then((raw) => raw.Keys ? Object.entries(raw.Keys).map(([cid]) => ({ cid, name: '', size: 0, created: '' })) : [])
|
||||
.catch(() => [] as IPFSPin[]),
|
||||
fetcher<{ RepoSize?: number; StorageMax?: number; NumObjects?: number }>('/api/repo/stats')
|
||||
.then((raw) => ({ repoSize: raw.RepoSize ?? 0, storageMax: raw.StorageMax ?? 0, numObjects: raw.NumObjects ?? 0, repoPath: '', version: '' }))
|
||||
.catch(() => null),
|
||||
fetcher<{ TotalIn?: number; TotalOut?: number }>('/api/bw/stats')
|
||||
.then((raw) => ({ totalIn: raw.TotalIn ?? 0, totalOut: raw.TotalOut ?? 0, rateIn: 0, rateOut: 0 }))
|
||||
.catch(() => null),
|
||||
]);
|
||||
|
||||
return { peers, pins, repo, bw, health };
|
||||
}, {
|
||||
refreshInterval,
|
||||
revalidateOnFocus: true,
|
||||
});
|
||||
}
|
||||
|
||||
/* ════════════════════════════ Daily Stats (paginated) ════════════════════════════ */
|
||||
|
||||
export interface DailyStat {
|
||||
date: string;
|
||||
uploads: number;
|
||||
bytesAdded: number;
|
||||
bytesRemoved: number;
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 30;
|
||||
|
||||
export function useDailyStats() {
|
||||
return useSWRInfinite<DailyStat[]>(
|
||||
(pageIndex, previousData) => {
|
||||
if (previousData && previousData.length < PAGE_SIZE) return null;
|
||||
return `/api/stats/daily?page=${pageIndex}&limit=${PAGE_SIZE}`;
|
||||
},
|
||||
fetcher,
|
||||
{ revalidateFirstPage: false },
|
||||
);
|
||||
}
|
||||
|
||||
/* ════════════════════════════ Health ════════════════════════════ */
|
||||
|
||||
export function useHealth() {
|
||||
return useSWR<{ status: string; node: string }>('/api/health', fetcher, {
|
||||
refreshInterval: 30_000,
|
||||
});
|
||||
}
|
||||
|
||||
/* ════════════════════════════ Users (admin) ════════════════════════════ */
|
||||
|
||||
export function useUsers() {
|
||||
return useSWR<IPFSUser[]>('/api/users', async (path) => {
|
||||
const raw = await fetcher<IPFSUser[] | { users: IPFSUser[] }>(path);
|
||||
if (Array.isArray(raw)) return raw;
|
||||
if (raw && Array.isArray(raw.users)) return raw.users;
|
||||
return [];
|
||||
}, {
|
||||
refreshInterval: 30_000,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user