From 48cf86a449451099cfe37fe22afa54c837371ca2 Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Fri, 28 Nov 2025 12:10:15 +0100 Subject: [PATCH] Refactor nullish checks and add type safety Replaces many uses of logical OR (||) with nullish coalescing (??) for more accurate handling of undefined/null values. Adds explicit type annotations and interfaces to improve type safety, especially in API routes and server-side code. Updates SSH connection test handling and config parsing in installedScripts router for better reliability. Minor fixes to deduplication logic, cookie handling, and error reporting. --- src/app/_components/AuthProvider.tsx | 61 +++++--- src/app/_components/BackupsTab.tsx | 19 +-- src/app/_components/GeneralSettingsModal.tsx | 2 +- src/app/_components/TextViewer.tsx | 11 +- src/app/api/servers/[id]/public-key/route.ts | 20 ++- src/app/api/servers/generate-keypair/route.ts | 2 +- src/app/api/settings/auto-sync/route.ts | 64 +++++--- src/app/page.tsx | 31 ++-- src/lib/auth.ts | 2 +- src/server/api/routers/backups.ts | 2 +- src/server/api/routers/installedScripts.ts | 143 +++++++++++------- src/server/db.ts | 2 +- 12 files changed, 227 insertions(+), 132 deletions(-) diff --git a/src/app/_components/AuthProvider.tsx b/src/app/_components/AuthProvider.tsx index 05e51cb..7a28f8f 100644 --- a/src/app/_components/AuthProvider.tsx +++ b/src/app/_components/AuthProvider.tsx @@ -1,6 +1,13 @@ -'use client'; +"use client"; -import { createContext, useContext, useEffect, useState, useCallback, type ReactNode } from 'react'; +import { + createContext, + useContext, + useEffect, + useState, + useCallback, + type ReactNode, +} from "react"; interface AuthContextType { isAuthenticated: boolean; @@ -27,10 +34,13 @@ export function AuthProvider({ children }: AuthProviderProps) { const checkAuthInternal = async (retryCount = 0) => { try { // First check if setup is completed - const setupResponse = await fetch('/api/settings/auth-credentials'); + const setupResponse = await fetch("/api/settings/auth-credentials"); if (setupResponse.ok) { - const setupData = await setupResponse.json() as { setupCompleted: boolean; enabled: boolean }; - + const setupData = (await setupResponse.json()) as { + setupCompleted: boolean; + enabled: boolean; + }; + // If setup is not completed or auth is disabled, don't verify if (!setupData.setupCompleted || !setupData.enabled) { setIsAuthenticated(false); @@ -42,12 +52,12 @@ export function AuthProvider({ children }: AuthProviderProps) { } // Only verify authentication if setup is completed and auth is enabled - const response = await fetch('/api/auth/verify', { - credentials: 'include', // Ensure cookies are sent + const response = await fetch("/api/auth/verify", { + credentials: "include", // Ensure cookies are sent }); if (response.ok) { - const data = await response.json() as { - username: string; + const data = (await response.json()) as { + username: string; expirationTime?: number | null; timeUntilExpiration?: number | null; }; @@ -58,7 +68,7 @@ export function AuthProvider({ children }: AuthProviderProps) { setIsAuthenticated(false); setUsername(null); setExpirationTime(null); - + // Retry logic for failed auth checks (max 2 retries) if (retryCount < 2) { setTimeout(() => { @@ -68,11 +78,11 @@ export function AuthProvider({ children }: AuthProviderProps) { } } } catch (error) { - console.error('Error checking auth:', error); + console.error("Error checking auth:", error); setIsAuthenticated(false); setUsername(null); setExpirationTime(null); - + // Retry logic for network errors (max 2 retries) if (retryCount < 2) { setTimeout(() => { @@ -87,24 +97,28 @@ export function AuthProvider({ children }: AuthProviderProps) { const checkAuth = useCallback(() => { return checkAuthInternal(0); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const login = async (username: string, password: string): Promise => { + const login = async ( + username: string, + password: string, + ): Promise => { try { - const response = await fetch('/api/auth/login', { - method: 'POST', + const response = await fetch("/api/auth/login", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ username, password }), - credentials: 'include', // Ensure cookies are received + credentials: "include", // Ensure cookies are received }); if (response.ok) { - const data = await response.json() as { username: string }; + const data = (await response.json()) as { username: string }; setIsAuthenticated(true); setUsername(data.username); - + // Check auth again to get expiration time // Add a small delay to ensure the httpOnly cookie is available await new Promise((resolve) => { @@ -115,18 +129,19 @@ export function AuthProvider({ children }: AuthProviderProps) { return true; } else { const errorData = await response.json(); - console.error('Login failed:', errorData.error); + console.error("Login failed:", errorData.error); return false; } } catch (error) { - console.error('Login error:', error); + console.error("Login error:", error); return false; } }; const logout = () => { // Clear the auth cookie by setting it to expire - document.cookie = 'auth-token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; + document.cookie = + "auth-token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; setIsAuthenticated(false); setUsername(null); setExpirationTime(null); @@ -156,7 +171,7 @@ export function AuthProvider({ children }: AuthProviderProps) { export function useAuth() { const context = useContext(AuthContext); if (context === undefined) { - throw new Error('useAuth must be used within an AuthProvider'); + throw new Error("useAuth must be used within an AuthProvider"); } return context; } diff --git a/src/app/_components/BackupsTab.tsx b/src/app/_components/BackupsTab.tsx index 4f579b1..05c0fc2 100644 --- a/src/app/_components/BackupsTab.tsx +++ b/src/app/_components/BackupsTab.tsx @@ -89,7 +89,7 @@ export function BackupsTab() { setShouldPollRestore(false); // Check if restore was successful or failed const lastLog = - restoreLogsData.logs[restoreLogsData.logs.length - 1] || ""; + restoreLogsData.logs[restoreLogsData.logs.length - 1] ?? ""; if (lastLog.includes("Restore completed successfully")) { setRestoreSuccess(true); setRestoreError(null); @@ -118,9 +118,9 @@ export function BackupsTab() { const progressMessages = restoreProgress.length > 0 ? restoreProgress - : result.progress?.map((p) => p.message) || [ + : (result.progress?.map((p) => p.message) ?? [ "Restore completed successfully", - ]; + ]); setRestoreProgress(progressMessages); setRestoreSuccess(true); setRestoreError(null); @@ -128,9 +128,9 @@ export function BackupsTab() { setSelectedBackup(null); // Keep success message visible - user can dismiss manually } else { - setRestoreError(result.error || "Restore failed"); + setRestoreError(result.error ?? "Restore failed"); setRestoreProgress( - result.progress?.map((p) => p.message) || restoreProgress, + result.progress?.map((p) => p.message) ?? restoreProgress, ); setRestoreSuccess(false); setRestoreConfirmOpen(false); @@ -141,7 +141,7 @@ export function BackupsTab() { onError: (error) => { // Stop polling on error setShouldPollRestore(false); - setRestoreError(error.message || "Restore failed"); + setRestoreError(error.message ?? "Restore failed"); setRestoreConfirmOpen(false); setSelectedBackup(null); setRestoreProgress([]); @@ -158,11 +158,12 @@ export function BackupsTab() { useEffect(() => { if (!hasAutoDiscovered && !isLoading && backupsData) { // Only auto-discover if there are no backups yet - if (!backupsData.backups || backupsData.backups.length === 0) { - handleDiscoverBackups(); + if (!backupsData.backups?.length) { + void handleDiscoverBackups(); } setHasAutoDiscovered(true); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [hasAutoDiscovered, isLoading, backupsData]); const handleDiscoverBackups = () => { @@ -436,7 +437,7 @@ export function BackupsTab() { {backupsData && !backupsData.success && (

- Error loading backups: {backupsData.error || "Unknown error"} + Error loading backups: {backupsData.error ?? "Unknown error"}

)} diff --git a/src/app/_components/GeneralSettingsModal.tsx b/src/app/_components/GeneralSettingsModal.tsx index f119737..86a641e 100644 --- a/src/app/_components/GeneralSettingsModal.tsx +++ b/src/app/_components/GeneralSettingsModal.tsx @@ -1785,7 +1785,7 @@ export function GeneralSettingsModal({ ) { return; } - setDeletingRepoId(repo.id); + setDeletingRepoId(Number(repo.id)); setMessage(null); try { const result = diff --git a/src/app/_components/TextViewer.tsx b/src/app/_components/TextViewer.tsx index f55330c..4440f77 100644 --- a/src/app/_components/TextViewer.tsx +++ b/src/app/_components/TextViewer.tsx @@ -216,11 +216,16 @@ export function TextViewer({ )} + {/* Boolean logic intentionally uses || for truthiness checks - eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */} {((selectedVersion === "default" && - (scriptContent.mainScript || scriptContent.installScript)) || + Boolean( + scriptContent.mainScript ?? scriptContent.installScript, + )) || (selectedVersion === "alpine" && - (scriptContent.alpineMainScript || - scriptContent.alpineInstallScript))) && ( + Boolean( + scriptContent.alpineMainScript ?? + scriptContent.alpineInstallScript, + ))) && (