forked from maik/IPFS-portal
c9432a16ba
- 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
187 lines
7.0 KiB
TypeScript
187 lines
7.0 KiB
TypeScript
/* ── 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,
|
|
});
|
|
}
|