From a8496a9c6400af16c410e149a096b8ec7ab9283f Mon Sep 17 00:00:00 2001 From: null Date: Sat, 4 Jul 2026 21:26:22 -0500 Subject: [PATCH] refactor(ts): convert admin/ cards to TypeScript (AuthMethods, BankSync, Backup, ...) Completes the entire client/components/admin/ directory conversion to .tsx: OidcForm/AuthConfigData/OidcTest types for the OIDC config form; Backup/ BackupSettings types for the backup manager. errMessage() on all strict catch(unknown) sites. typecheck 0, lint 0 errors, build green. Co-Authored-By: Claude Opus 4.8 --- ...uthMethodsCard.jsx => AuthMethodsCard.tsx} | 124 +++++++++++------- ...ementCard.jsx => BackupManagementCard.tsx} | 72 ++++++---- ...yncAdminCard.jsx => BankSyncAdminCard.tsx} | 61 ++++++--- 3 files changed, 164 insertions(+), 93 deletions(-) rename client/components/admin/{AuthMethodsCard.jsx => AuthMethodsCard.tsx} (81%) rename client/components/admin/{BackupManagementCard.jsx => BackupManagementCard.tsx} (88%) rename client/components/admin/{BankSyncAdminCard.jsx => BankSyncAdminCard.tsx} (87%) diff --git a/client/components/admin/AuthMethodsCard.jsx b/client/components/admin/AuthMethodsCard.tsx similarity index 81% rename from client/components/admin/AuthMethodsCard.jsx rename to client/components/admin/AuthMethodsCard.tsx index aac86fc..a1edfa3 100644 --- a/client/components/admin/AuthMethodsCard.jsx +++ b/client/components/admin/AuthMethodsCard.tsx @@ -1,6 +1,7 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { toast } from 'sonner'; import { api } from '@/api'; +import { errMessage } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; @@ -8,45 +9,89 @@ import { FieldRow, Toggle } from './adminShared'; const AUTHENTIK_ICON_URL = '/img/auth.png'; -function defaultOidcRedirectUri() { +interface OidcForm { + local_login_enabled: boolean; + oidc_login_enabled: boolean; + oidc_provider_name: string; + oidc_issuer_url: string; + oidc_client_id: string; + oidc_client_secret: string; + oidc_client_secret_clear: boolean; + oidc_token_auth_method: string; + oidc_redirect_uri: string; + oidc_scopes: string; + oidc_auto_provision: boolean; + oidc_admin_group: string; + oidc_default_role: string; +} + +interface AuthConfigData { + local_login_enabled?: boolean; + oidc_login_enabled?: boolean; + oidc_provider_name?: string; + oidc_issuer_url?: string; + oidc_client_id?: string; + oidc_token_auth_method?: string; + oidc_redirect_uri?: string; + oidc_scopes?: string; + oidc_auto_provision?: boolean; + oidc_admin_group?: string; + oidc_default_role?: string; + oidc_client_secret_set?: boolean; + oidc_configured?: boolean; + oidc_env_fallback_used?: boolean; + warnings?: string[]; +} + +interface OidcTestResult { + ok?: boolean; + error?: string; + issuer?: string; +} + +function defaultOidcRedirectUri(): string { if (typeof window === 'undefined') return ''; return `${window.location.origin}/api/auth/oidc/callback`; } -function looksLikeOidcEndpoint(url) { +function looksLikeOidcEndpoint(url: string | null | undefined): boolean { const value = String(url || '').toLowerCase(); return /\/(?:authorize|token|userinfo|jwks|certs)\/?$/.test(value); } +function formFromData(d: AuthConfigData): OidcForm { + return { + local_login_enabled: d.local_login_enabled !== false, + oidc_login_enabled: !!d.oidc_login_enabled, + oidc_provider_name: d.oidc_provider_name || 'authentik', + oidc_issuer_url: d.oidc_issuer_url || '', + oidc_client_id: d.oidc_client_id || '', + oidc_client_secret: '', + oidc_client_secret_clear: false, + oidc_token_auth_method: d.oidc_token_auth_method || 'client_secret_basic', + oidc_redirect_uri: d.oidc_redirect_uri || defaultOidcRedirectUri(), + oidc_scopes: d.oidc_scopes || 'openid email profile groups', + oidc_auto_provision: d.oidc_auto_provision !== false, + oidc_admin_group: d.oidc_admin_group || '', + oidc_default_role: d.oidc_default_role || 'user', + }; +} + export default function AuthMethodsCard() { - const [data, setData] = useState(null); - const [form, setForm] = useState(null); + const [data, setData] = useState(null); + const [form, setForm] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [testingOidc, setTestingOidc] = useState(false); - const [oidcTest, setOidcTest] = useState(null); + const [oidcTest, setOidcTest] = useState(null); const load = useCallback(async () => { try { - const d = await api.authModeConfig(); + const d = await api.authModeConfig() as AuthConfigData; setData(d); - setForm({ - local_login_enabled: d.local_login_enabled !== false, - oidc_login_enabled: !!d.oidc_login_enabled, - oidc_provider_name: d.oidc_provider_name || 'authentik', - oidc_issuer_url: d.oidc_issuer_url || '', - oidc_client_id: d.oidc_client_id || '', - oidc_client_secret: '', - oidc_client_secret_clear: false, - oidc_token_auth_method: d.oidc_token_auth_method || 'client_secret_basic', - oidc_redirect_uri: d.oidc_redirect_uri || defaultOidcRedirectUri(), - oidc_scopes: d.oidc_scopes || 'openid email profile groups', - oidc_auto_provision: d.oidc_auto_provision !== false, - oidc_admin_group: d.oidc_admin_group || '', - oidc_default_role: d.oidc_default_role || 'user', - }); + setForm(formFromData(d)); } catch (err) { - toast.error(err.message || 'Failed to load auth settings.'); + toast.error(errMessage(err, 'Failed to load auth settings.')); } finally { setLoading(false); } @@ -54,31 +99,17 @@ export default function AuthMethodsCard() { useEffect(() => { load(); }, [load]); - const set = (k, v) => setForm(prev => ({ ...prev, [k]: v })); + const set = (k: keyof OidcForm, v: string | boolean) => setForm(prev => prev ? ({ ...prev, [k]: v }) as OidcForm : prev); async function handleSave() { setSaving(true); try { - const d = await api.setAuthMode(form); + const d = await api.setAuthMode(form) as AuthConfigData; setData(d); - setForm({ - local_login_enabled: d.local_login_enabled !== false, - oidc_login_enabled: !!d.oidc_login_enabled, - oidc_provider_name: d.oidc_provider_name || 'authentik', - oidc_issuer_url: d.oidc_issuer_url || '', - oidc_client_id: d.oidc_client_id || '', - oidc_client_secret: '', - oidc_client_secret_clear: false, - oidc_token_auth_method: d.oidc_token_auth_method || 'client_secret_basic', - oidc_redirect_uri: d.oidc_redirect_uri || defaultOidcRedirectUri(), - oidc_scopes: d.oidc_scopes || 'openid email profile groups', - oidc_auto_provision: d.oidc_auto_provision !== false, - oidc_admin_group: d.oidc_admin_group || '', - oidc_default_role: d.oidc_default_role || 'user', - }); + setForm(formFromData(d)); toast.success('Auth method settings saved.'); } catch (err) { - toast.error(err.message || 'Failed to save auth method settings.'); + toast.error(errMessage(err, 'Failed to save auth method settings.')); } finally { setSaving(false); } @@ -88,11 +119,12 @@ export default function AuthMethodsCard() { setTestingOidc(true); setOidcTest(null); try { - const result = await api.testOidcConfig(form); + const result = await api.testOidcConfig(form) as OidcTestResult; setOidcTest(result); toast.success('authentik configuration test passed.'); } catch (err) { - const result = err.data || { ok: false, error: err.message || 'OIDC configuration test failed.' }; + const e = err as { data?: OidcTestResult; message?: string }; + const result = e.data || { ok: false, error: e.message || 'OIDC configuration test failed.' }; setOidcTest(result); toast.error(result.error || 'OIDC configuration test failed.'); } finally { @@ -151,7 +183,7 @@ export default function AuthMethodsCard() { - {(data?.warnings?.length > 0 || wouldLockOut || cantDisableLocal) && ( + {((data?.warnings?.length ?? 0) > 0 || wouldLockOut || cantDisableLocal) && (
{wouldLockOut && (

@@ -229,11 +261,11 @@ export default function AuthMethodsCard() { setForm(prev => ({ + onChange={e => setForm(prev => prev ? ({ ...prev, oidc_client_secret: e.target.value, oidc_client_secret_clear: e.target.value ? false : prev.oidc_client_secret_clear, - }))} + }) : prev)} placeholder="Leave blank to keep existing secret" className="h-8 text-sm" /> diff --git a/client/components/admin/BackupManagementCard.jsx b/client/components/admin/BackupManagementCard.tsx similarity index 88% rename from client/components/admin/BackupManagementCard.jsx rename to client/components/admin/BackupManagementCard.tsx index 282c10b..bedbac0 100644 --- a/client/components/admin/BackupManagementCard.jsx +++ b/client/components/admin/BackupManagementCard.tsx @@ -1,8 +1,8 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { Database, Download, Play, RefreshCw, RotateCcw, Trash2, Upload } from 'lucide-react'; import { toast } from 'sonner'; import { api } from '@/api'; -import { cn, fmtBytes } from '@/lib/utils'; +import { cn, fmtBytes, errMessage } from '@/lib/utils'; import { Button, buttonVariants } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Input } from '@/components/ui/input'; @@ -18,7 +18,25 @@ import { import { SectionHeading, Toggle, formatDateTime, BackupTypeBadge } from './adminShared'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; -const DEFAULT_SETTINGS = { +interface Backup { + id: string; + type?: string; + modified_at?: string | null; + size_bytes?: number; + checksum?: string | null; +} + +interface BackupSettings { + enabled: boolean; + frequency: string; + time: string; + retention_count: number | string; + last_run_at: string | null; + next_run_at: string | null; + last_error: string | null; +} + +const DEFAULT_SETTINGS: BackupSettings = { enabled: false, frequency: 'daily', time: '02:00', @@ -29,24 +47,24 @@ const DEFAULT_SETTINGS = { }; export default function BackupManagementCard() { - const [backups, setBackups] = useState([]); - const [settings, setSettings] = useState(DEFAULT_SETTINGS); + const [backups, setBackups] = useState([]); + const [settings, setSettings] = useState(DEFAULT_SETTINGS); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(''); - const [restoreTarget, setRestoreTarget] = useState(null); - const [deleteTarget, setDeleteTarget] = useState(null); + const [restoreTarget, setRestoreTarget] = useState(null); + const [deleteTarget, setDeleteTarget] = useState(null); const load = useCallback(async () => { setLoading(true); try { const [backupData, settingsData] = await Promise.all([ - api.adminBackups(), - api.adminBackupSettings(), + api.adminBackups() as Promise<{ backups?: Backup[] }>, + api.adminBackupSettings() as Promise>, ]); setBackups(backupData.backups || []); setSettings({ ...DEFAULT_SETTINGS, ...settingsData }); } catch (err) { - toast.error(err.message || 'Failed to load backups.'); + toast.error(errMessage(err, 'Failed to load backups.')); } finally { setLoading(false); } @@ -55,7 +73,7 @@ export default function BackupManagementCard() { useEffect(() => { load(); }, [load]); const latest = backups[0]; - const setSchedule = (key, value) => setSettings(prev => ({ ...prev, [key]: value })); + const setSchedule = (key: keyof BackupSettings, value: string | boolean) => setSettings(prev => ({ ...prev, [key]: value })); async function handleCreate() { setBusy('create'); @@ -64,16 +82,16 @@ export default function BackupManagementCard() { toast.success('Backup created.'); await load(); } catch (err) { - toast.error(err.message || 'Failed to create backup.'); + toast.error(errMessage(err, 'Failed to create backup.')); } finally { setBusy(''); } } - async function handleDownload(backup) { + async function handleDownload(backup: Backup) { setBusy(`download:${backup.id}`); try { - const { blob, filename } = await api.downloadAdminBackup(backup.id); + const { blob, filename } = await api.downloadAdminBackup(backup.id) as { blob: Blob; filename?: string }; const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; @@ -83,13 +101,13 @@ export default function BackupManagementCard() { a.remove(); URL.revokeObjectURL(url); } catch (err) { - toast.error(err.message || 'Failed to download backup.'); + toast.error(errMessage(err, 'Failed to download backup.')); } finally { setBusy(''); } } - async function handleImport(e) { + async function handleImport(e: React.ChangeEvent) { const file = e.target.files?.[0]; e.target.value = ''; if (!file) return; @@ -100,7 +118,7 @@ export default function BackupManagementCard() { toast.success('Backup imported.'); await load(); } catch (err) { - toast.error(err.message || 'Failed to import backup.'); + toast.error(errMessage(err, 'Failed to import backup.')); } finally { setBusy(''); } @@ -110,12 +128,12 @@ export default function BackupManagementCard() { if (!restoreTarget) return; setBusy(`restore:${restoreTarget.id}`); try { - const result = await api.restoreAdminBackup(restoreTarget.id); + const result = await api.restoreAdminBackup(restoreTarget.id) as { pre_restore_backup?: string }; toast.success(`Database restored. Pre-restore backup: ${result.pre_restore_backup}`); setRestoreTarget(null); await load(); } catch (err) { - toast.error(err.message || 'Failed to restore backup.'); + toast.error(errMessage(err, 'Failed to restore backup.')); } finally { setBusy(''); } @@ -130,7 +148,7 @@ export default function BackupManagementCard() { setDeleteTarget(null); await load(); } catch (err) { - toast.error(err.message || 'Failed to delete backup.'); + toast.error(errMessage(err, 'Failed to delete backup.')); } finally { setBusy(''); } @@ -143,12 +161,12 @@ export default function BackupManagementCard() { enabled: !!settings.enabled, frequency: settings.frequency, time: settings.time, - retention_count: parseInt(settings.retention_count, 10) || 2, - }); + retention_count: parseInt(String(settings.retention_count), 10) || 2, + }) as Partial; setSettings({ ...DEFAULT_SETTINGS, ...saved }); toast.success('Backup schedule saved.'); } catch (err) { - toast.error(err.message || 'Failed to save backup schedule.'); + toast.error(errMessage(err, 'Failed to save backup schedule.')); } finally { setBusy(''); } @@ -161,7 +179,7 @@ export default function BackupManagementCard() { toast.success('Scheduled backup created.'); await load(); } catch (err) { - toast.error(err.message || 'Failed to run scheduled backup.'); + toast.error(errMessage(err, 'Failed to run scheduled backup.')); } finally { setBusy(''); } @@ -196,12 +214,12 @@ export default function BackupManagementCard() {

- {[ + {([ ['Managed backups', backups.length], ['Latest backup', latest ? formatDateTime(latest.modified_at) : '—'], ['Latest size', latest ? fmtBytes(latest.size_bytes) : '—'], ['Scheduled', settings.enabled ? `${settings.frequency} ${settings.time}` : 'Disabled'], - ].map(([label, value]) => ( + ] as [string, React.ReactNode][]).map(([label, value]) => (

{label}

{value}

@@ -258,7 +276,7 @@ export default function BackupManagementCard() { {formatDateTime(backup.modified_at)} {fmtBytes(backup.size_bytes)} - + {backup.checksum || '—'} diff --git a/client/components/admin/BankSyncAdminCard.jsx b/client/components/admin/BankSyncAdminCard.tsx similarity index 87% rename from client/components/admin/BankSyncAdminCard.jsx rename to client/components/admin/BankSyncAdminCard.tsx index 0106c43..839813f 100644 --- a/client/components/admin/BankSyncAdminCard.jsx +++ b/client/components/admin/BankSyncAdminCard.tsx @@ -1,30 +1,50 @@ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { AlertTriangle, Info } from 'lucide-react'; import { toast } from 'sonner'; import { api } from '@/api'; +import { errMessage } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; import { Toggle } from './adminShared'; -function parseUtc(str) { +interface BankSyncWorker { + running?: boolean; + last_run_at?: string | null; + next_run_at?: string | null; +} + +interface BankSyncConfig { + enabled?: boolean; + sync_interval_hours?: number; + sync_days?: number; + debug_logging?: boolean; + sync_days_max?: number; + seed_days?: number; + worker?: BankSyncWorker; + encryption_key_source?: string; +} + +function parseUtc(str: string | null | undefined): Date | null { if (!str) return null; const normalized = str.includes('T') ? str : str.replace(' ', 'T') + 'Z'; return new Date(normalized); } -function timeAgo(iso) { - if (!iso) return null; - const secs = Math.floor((Date.now() - parseUtc(iso).getTime()) / 1000); +function timeAgo(iso: string | null | undefined): string | null { + const d = parseUtc(iso); + if (!d) return null; + const secs = Math.floor((Date.now() - d.getTime()) / 1000); if (secs < 60) return 'just now'; if (secs < 3600) return `${Math.floor(secs / 60)}m ago`; if (secs < 86400) return `${Math.floor(secs / 3600)}h ago`; return `${Math.floor(secs / 86400)}d ago`; } -function timeUntil(iso) { - if (!iso) return null; - const secs = Math.floor((parseUtc(iso).getTime() - Date.now()) / 1000); +function timeUntil(iso: string | null | undefined): string | null { + const d = parseUtc(iso); + if (!d) return null; + const secs = Math.floor((d.getTime() - Date.now()) / 1000); if (secs <= 0) return 'soon'; if (secs < 60) return `${secs}s`; if (secs < 3600) return `${Math.floor(secs / 60)}m`; @@ -32,31 +52,32 @@ function timeUntil(iso) { } export default function BankSyncAdminCard() { - const [config, setConfig] = useState(null); + const [config, setConfig] = useState(null); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(''); const [saving, setSaving] = useState(false); const [enabled, setEnabled] = useState(false); - const [syncInterval, setSyncInterval] = useState(4); - const [syncDays, setSyncDays] = useState(30); + const [syncInterval, setSyncInterval] = useState(4); + const [syncDays, setSyncDays] = useState(30); const [debugLogging, setDebugLogging] = useState(false); useEffect(() => { api.bankSyncConfig() - .then(d => { + .then(raw => { + const d = raw as BankSyncConfig; setConfig(d); setEnabled(!!d.enabled); setSyncInterval(d.sync_interval_hours ?? 4); setSyncDays(d.sync_days ?? 30); setDebugLogging(!!d.debug_logging); }) - .catch(err => setLoadError(err.message || 'Failed to load bank sync config')) + .catch(err => setLoadError(errMessage(err, 'Failed to load bank sync config'))) .finally(() => setLoading(false)); }, []); const handleSave = async () => { - const hours = parseFloat(syncInterval); - const days = parseInt(syncDays, 10); + const hours = parseFloat(String(syncInterval)); + const days = parseInt(String(syncDays), 10); const maxDays = config?.sync_days_max ?? 45; if (!Number.isFinite(hours) || hours < 0.5 || hours > 168) { toast.error('Sync interval must be between 0.5 and 168 hours.'); @@ -68,7 +89,7 @@ export default function BankSyncAdminCard() { } setSaving(true); try { - const result = await api.setBankSyncConfig({ enabled, sync_interval_hours: hours, sync_days: days, debug_logging: debugLogging }); + const result = await api.setBankSyncConfig({ enabled, sync_interval_hours: hours, sync_days: days, debug_logging: debugLogging }) as BankSyncConfig; setConfig(result); setEnabled(!!result.enabled); setSyncInterval(result.sync_interval_hours ?? 4); @@ -76,7 +97,7 @@ export default function BankSyncAdminCard() { setDebugLogging(!!result.debug_logging); toast.success('Bank sync settings saved.'); } catch (err) { - toast.error(err.message || 'Failed to update bank sync setting.'); + toast.error(errMessage(err, 'Failed to update bank sync setting.')); } finally { setSaving(false); } @@ -103,8 +124,8 @@ export default function BankSyncAdminCard() { } const changed = enabled !== !!config?.enabled - || parseFloat(syncInterval) !== config?.sync_interval_hours - || parseInt(syncDays, 10) !== config?.sync_days + || parseFloat(String(syncInterval)) !== config?.sync_interval_hours + || parseInt(String(syncDays), 10) !== config?.sync_days || debugLogging !== !!config?.debug_logging; const worker = config?.worker; const seedDays = config?.seed_days ?? 44; @@ -209,7 +230,7 @@ export default function BankSyncAdminCard() {
{/* Amber warning at the SimpleFIN limit */} - {parseInt(syncDays, 10) >= maxDays && ( + {parseInt(String(syncDays), 10) >= maxDays && (