refactor: codebase opschoning — type safety, error handling, central config, page splits

- categorie A: alle catch(e: any) → catch(e: unknown) met instanceof check
- categorie B: stille .catch() logging toegevoegd (SWRegister, admin, upload, auth)
- categorie C: hardcoded 192.168.1.176 IPs vervangen door env var defaults
- categorie D: page files >250L gesplitst (history, settings, explorer, admin/payment,
  ipns, users, upload, dashboard) in helpers/components
- categorie E: eslint-disable vervangen in SearchInput.tsx
- src/lib/config.ts centrale config module (localhost defaults)
- CSP in next.config.ts dynamisch via env var
- deploy.sh: geen hardcoded IP meer (REQUIRED arg)
- lib/api/ lib/auth/ lib/wallet/ lib/search-index/ lib/storage/ gesplitst in modules
- ongebruikte bestanden verwijderd: api.ts, auth.tsx, wallet.ts, search-index.ts,
  PaymentPanel.tsx, SearchBar.tsx, proxy.ts, serve-static.js
This commit is contained in:
maikrolf
2026-07-19 15:35:18 +02:00
parent 88ce38a49e
commit f72b775379
144 changed files with 4644 additions and 10230 deletions
+60
View File
@@ -0,0 +1,60 @@
"use client";
import type { ReactNode } from "react";
import { Save, RotateCcw, Check, AlertTriangle } from "lucide-react";
interface SettingsActionsProps {
dirty: boolean;
saved: boolean;
error: string | null;
onSave: () => void;
onReset: () => void;
}
export function errorMsg(field: string, validation: Record<string, string | null>): ReactNode {
return validation[field] ? (
<p className="mt-1 text-xs text-accent-rose">{validation[field]}</p>
) : null;
}
export function SettingsActions({ dirty, saved, error, onSave, onReset }: SettingsActionsProps) {
return (
<div className="glass rounded-xl p-5 animate-fade-in">
<div className="flex flex-wrap items-center gap-3">
<button
onClick={onSave}
disabled={!dirty && !error}
className="flex items-center gap-2 px-5 py-2.5 rounded-xl bg-brand-600 hover:bg-brand-500 disabled:opacity-40 disabled:cursor-not-allowed text-white text-sm font-medium transition-colors"
>
{saved ? (
<><Check className="w-4 h-4 text-accent-green" /> Saved!</>
) : (
<><Save className="w-4 h-4" /> Save Settings</>
)}
</button>
<button
onClick={onReset}
className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-surface-800 hover:bg-surface-700 text-surface-300 text-sm font-medium transition-colors"
>
<RotateCcw className="w-4 h-4" />
Reset Defaults
</button>
{dirty && !saved && (
<span className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-accent-amber/10 text-accent-amber text-xs">
<AlertTriangle className="w-3 h-3" />
Unsaved changes
</span>
)}
{error && (
<span className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-accent-rose/10 text-accent-rose text-xs">
<AlertTriangle className="w-3 h-3" />
{error}
</span>
)}
</div>
</div>
);
}
+44
View File
@@ -0,0 +1,44 @@
/* ── Settings Page Helpers ── */
import type { PortalSettings } from "@/lib/storage";
export function formatStorageMB(mb: number): string {
if (mb >= 1024) return (mb / 1024).toFixed(0) + " GB";
return mb + " MB";
}
export function inputCls(field: string, validation: Record<string, string | null>): string {
return `w-full rounded-lg bg-surface-900 border ${
validation[field] ? "border-accent-rose/50" : "border-surface-700"
} px-3 py-2 text-sm text-surface-200 placeholder:text-surface-500 focus:outline-none focus:border-brand-500 focus:ring-1 focus:ring-brand-500 transition-colors`;
}
export function validateSettings(
form: PortalSettings,
): { valid: boolean; errors: Record<string, string | null> } {
const errors: Record<string, string | null> = {};
let valid = true;
if (!form.gatewayUrl.startsWith("http://") && !form.gatewayUrl.startsWith("https://")) {
errors.gatewayUrl = "Must start with http:// or https://";
valid = false;
}
if (form.gatewayUrl.length > 500) {
errors.gatewayUrl = "Too long (max 500 chars)";
valid = false;
}
if (form.apiEndpoint && !form.apiEndpoint.startsWith("/") && !form.apiEndpoint.startsWith("http")) {
errors.apiEndpoint = "Must be empty, a path (/…), or a URL";
valid = false;
}
if (form.storageMax < 1 || form.storageMax > 102400) {
errors.storageMax = "Must be 1–102400 MB";
valid = false;
}
if (form.refreshInterval < 5 || form.refreshInterval > 300) {
errors.refreshInterval = "Must be 5–300 seconds";
valid = false;
}
return { valid, errors };
}
+31 -119
View File
@@ -3,25 +3,16 @@
import { useState } from 'react';
import { getSettings, saveSettings, resetSettings, type PortalSettings } from '@/lib/storage';
import { useTheme } from '@/lib/theme';
import { formatBytes } from '@/lib/helpers';
import PortalLayout from '@/app/layout-portal';
import {
Settings, Globe, Server, HardDrive, RefreshCw, Shield,
Sun, Moon, Save, RotateCcw, Check, AlertTriangle,
Globe, Server, HardDrive, RefreshCw, Shield,
Sun, Moon,
} from 'lucide-react';
/* ── Helpers ── */
function formatStorageMB(mb: number): string {
if (mb >= 1024) return (mb / 1024).toFixed(0) + ' GB';
return mb + ' MB';
}
function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0) + ' ' + units[i];
}
import ErrorBoundary from '@/components/ErrorBoundary';
import { formatStorageMB, inputCls, validateSettings } from './helpers';
import { errorMsg } from './components';
import { SettingsActions } from './components';
/* ════════════════════════════ Page ════════════════════════════ */
@@ -40,47 +31,19 @@ export default function SettingsPage() {
setForm(prev => ({ ...prev, [key]: value }));
setSaved(false);
setError(null);
// Clear validation error on change
if (validation[key]) {
setValidation(prev => ({ ...prev, [key]: null }));
}
}
/* ── Validate ── */
function validate(): boolean {
const errs: Record<string, string | null> = {};
let valid = true;
if (!form.gatewayUrl.startsWith('http://') && !form.gatewayUrl.startsWith('https://')) {
errs.gatewayUrl = 'Must start with http:// or https://';
valid = false;
}
if (form.gatewayUrl.length > 500) {
errs.gatewayUrl = 'Too long (max 500 chars)';
valid = false;
}
if (form.apiEndpoint && !form.apiEndpoint.startsWith('/') && !form.apiEndpoint.startsWith('http')) {
errs.apiEndpoint = 'Must be empty, a path (/…), or a URL';
valid = false;
}
if (form.storageMax < 1 || form.storageMax > 102400) {
errs.storageMax = 'Must be 1–102400 MB';
valid = false;
}
if (form.refreshInterval < 5 || form.refreshInterval > 300) {
errs.refreshInterval = 'Must be 5–300 seconds';
valid = false;
}
setValidation(errs);
if (!valid) setError('Fix validation errors before saving');
return valid;
}
/* ── Save ── */
function handleSave() {
if (!validate()) return;
const result = validateSettings(form);
setValidation(result.errors);
if (!result.valid) {
setError('Fix validation errors before saving');
return;
}
const merged = saveSettings(form);
setForm(merged);
setOriginal({ ...merged });
@@ -100,18 +63,6 @@ export default function SettingsPage() {
setValidation({});
}
/* ── Render helpers ── */
function inputCls(field: string): string {
return `w-full rounded-lg bg-surface-900 border ${validation[field] ? 'border-accent-rose/50' : 'border-surface-700'} px-3 py-2 text-sm text-surface-200 placeholder:text-surface-500 focus:outline-none focus:border-brand-500 focus:ring-1 focus:ring-brand-500 transition-colors`;
}
function errorMsg(field: string) {
return validation[field] ? (
<p className="mt-1 text-xs text-accent-rose">{validation[field]}</p>
) : null;
}
/* ════════════ Sections ════════════ */
const sections = [
@@ -126,9 +77,9 @@ export default function SettingsPage() {
value={form.gatewayUrl}
onChange={e => update('gatewayUrl', e.target.value)}
placeholder="https://ipfs.io/ipfs"
className={inputCls('gatewayUrl')}
className={inputCls('gatewayUrl', validation)}
/>
{errorMsg('gatewayUrl')}
{errorMsg('gatewayUrl', validation)}
<p className="mt-1.5 text-[11px] text-surface-500">
Base URL for IPFS gateway links. Used in dashboard, history, and share links.
</p>
@@ -146,9 +97,9 @@ export default function SettingsPage() {
value={form.apiEndpoint}
onChange={e => update('apiEndpoint', e.target.value)}
placeholder="(same-origin)"
className={inputCls('apiEndpoint')}
className={inputCls('apiEndpoint', validation)}
/>
{errorMsg('apiEndpoint')}
{errorMsg('apiEndpoint', validation)}
<p className="mt-1.5 text-[11px] text-surface-500">
Leave empty to use the same origin. Set to a custom URL to proxy through another server.
</p>
@@ -185,7 +136,7 @@ export default function SettingsPage() {
/>
<span className="text-xs text-surface-500">MB</span>
</div>
{errorMsg('storageMax')}
{errorMsg('storageMax', validation)}
<p className="mt-1.5 text-[11px] text-surface-500">
Maximum storage the IPFS node should use. Applied server-side.
</p>
@@ -198,23 +149,20 @@ export default function SettingsPage() {
iconColor: 'text-accent-rose',
fields: (
<div className="space-y-4">
{/* Allowed file types */}
<div>
<label className="text-xs text-surface-400 mb-1.5 block">Allowed File Extensions</label>
<input
value={form.allowedFileTypes}
onChange={e => update('allowedFileTypes', e.target.value)}
placeholder=".jpg,.png,.pdf,.mp4 (leave empty for all)"
className={inputCls('allowedFileTypes')}
className={inputCls('allowedFileTypes', validation)}
/>
{errorMsg('allowedFileTypes')}
{errorMsg('allowedFileTypes', validation)}
<p className="mt-1.5 text-[11px] text-surface-500">
Comma-separated extensions. Empty = all file types accepted.
Extension check is case-insensitive.
Comma-separated extensions. Empty = all file types accepted. Extension check is case-insensitive.
</p>
</div>
{/* Max file size */}
<div>
<label className="text-xs text-surface-400 mb-1.5 block">
Max File Size: <span className="text-surface-200 font-medium">{formatBytes(form.maxFileSize)}</span>
@@ -244,7 +192,6 @@ export default function SettingsPage() {
</p>
</div>
{/* Max files per batch */}
<div>
<label className="text-xs text-surface-400 mb-1.5 block">
Max Files Per Upload: <span className="text-surface-200 font-medium">{form.maxFiles}</span>
@@ -306,7 +253,7 @@ export default function SettingsPage() {
/>
<span className="text-xs text-surface-500">sec</span>
</div>
{errorMsg('refreshInterval')}
{errorMsg('refreshInterval', validation)}
<p className="mt-1.5 text-[11px] text-surface-500">
How often the dashboard auto-refreshes peer/bandwidth data.
</p>
@@ -346,13 +293,12 @@ export default function SettingsPage() {
return (
<PortalLayout>
{/* ── Header ── */}
<ErrorBoundary label="Instellingen">
<div className="mb-6">
<h1 className="text-2xl font-bold text-white">Settings</h1>
<p className="text-sm text-surface-400 mt-1">IPFS Portal configuration</p>
</div>
{/* ── Settings grid ── */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
{sections.map((s) => (
<div key={s.title} className="glass rounded-xl p-5 animate-fade-in">
@@ -365,48 +311,14 @@ export default function SettingsPage() {
))}
</div>
{/* ── Actions + status ── */}
<div className="glass rounded-xl p-5 animate-fade-in">
<div className="flex flex-wrap items-center gap-3">
{/* Save */}
<button
onClick={handleSave}
disabled={!dirty && !error}
className="flex items-center gap-2 px-5 py-2.5 rounded-xl bg-brand-600 hover:bg-brand-500 disabled:opacity-40 disabled:cursor-not-allowed text-white text-sm font-medium transition-colors"
>
{saved ? (
<><Check className="w-4 h-4 text-accent-green" /> Saved!</>
) : (
<><Save className="w-4 h-4" /> Save Settings</>
)}
</button>
{/* Reset */}
<button
onClick={handleReset}
className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-surface-800 hover:bg-surface-700 text-surface-300 text-sm font-medium transition-colors"
>
<RotateCcw className="w-4 h-4" />
Reset Defaults
</button>
{/* Dirty indicator */}
{dirty && !saved && (
<span className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-accent-amber/10 text-accent-amber text-xs">
<AlertTriangle className="w-3 h-3" />
Unsaved changes
</span>
)}
{/* Error */}
{error && (
<span className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-accent-rose/10 text-accent-rose text-xs">
<AlertTriangle className="w-3 h-3" />
{error}
</span>
)}
</div>
</div>
<SettingsActions
dirty={dirty}
saved={saved}
error={error}
onSave={handleSave}
onReset={handleReset}
/>
</ErrorBoundary>
</PortalLayout>
);
}