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.
This commit is contained in:
@@ -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,9 +34,12 @@ 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) {
|
||||
@@ -42,11 +52,11 @@ 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 {
|
||||
const data = (await response.json()) as {
|
||||
username: string;
|
||||
expirationTime?: number | null;
|
||||
timeUntilExpiration?: number | null;
|
||||
@@ -68,7 +78,7 @@ 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);
|
||||
@@ -87,21 +97,25 @@ 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<boolean> => {
|
||||
const login = async (
|
||||
username: string,
|
||||
password: string,
|
||||
): Promise<boolean> => {
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
<div className="bg-destructive/10 border-destructive rounded-lg border p-4">
|
||||
<p className="text-destructive">
|
||||
Error loading backups: {backupsData.error || "Unknown error"}
|
||||
Error loading backups: {backupsData.error ?? "Unknown error"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1785,7 +1785,7 @@ export function GeneralSettingsModal({
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setDeletingRepoId(repo.id);
|
||||
setDeletingRepoId(Number(repo.id));
|
||||
setMessage(null);
|
||||
try {
|
||||
const result =
|
||||
|
||||
@@ -216,11 +216,16 @@ export function TextViewer({
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{/* 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,
|
||||
))) && (
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
variant={activeTab === "main" ? "outline" : "ghost"}
|
||||
|
||||
@@ -3,6 +3,14 @@ import { NextResponse } from 'next/server';
|
||||
import { getDatabase } from '../../../../../server/database-prisma';
|
||||
import { getSSHService } from '../../../../../server/ssh-service';
|
||||
|
||||
interface ServerData {
|
||||
id: number;
|
||||
name: string;
|
||||
ip: string;
|
||||
ssh_key_path?: string | null;
|
||||
key_generated?: boolean;
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
@@ -18,7 +26,7 @@ export async function GET(
|
||||
}
|
||||
|
||||
const db = getDatabase();
|
||||
const server = await db.getServerById(id);
|
||||
const server = await db.getServerById(id) as ServerData | null;
|
||||
|
||||
if (!server) {
|
||||
return NextResponse.json(
|
||||
@@ -28,14 +36,14 @@ export async function GET(
|
||||
}
|
||||
|
||||
// Only allow viewing public key if it was generated by the system
|
||||
if (!(server as any).key_generated) {
|
||||
if (!server.key_generated) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Public key not available for user-provided keys' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!(server as any).ssh_key_path) {
|
||||
if (!server.ssh_key_path) {
|
||||
return NextResponse.json(
|
||||
{ error: 'SSH key path not found' },
|
||||
{ status: 404 }
|
||||
@@ -43,13 +51,13 @@ export async function GET(
|
||||
}
|
||||
|
||||
const sshService = getSSHService();
|
||||
const publicKey = sshService.getPublicKey((server as any).ssh_key_path as string);
|
||||
const publicKey = sshService.getPublicKey(server.ssh_key_path);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
publicKey,
|
||||
serverName: (server as any).name,
|
||||
serverIp: (server as any).ip
|
||||
serverName: server.name,
|
||||
serverIp: server.ip
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error retrieving public key:', error);
|
||||
|
||||
@@ -12,7 +12,7 @@ export const POST = withApiLogging(async function POST(_request: NextRequest) {
|
||||
// Get the next available server ID for key file naming
|
||||
const serverId = await db.getNextServerId();
|
||||
|
||||
const keyPair = await sshService.generateKeyPair(serverId);
|
||||
const keyPair = await sshService.generateKeyPair(Number(serverId));
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
|
||||
@@ -4,9 +4,25 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { isValidCron } from 'cron-validator';
|
||||
|
||||
interface AutoSyncSettings {
|
||||
autoSyncEnabled: boolean;
|
||||
syncIntervalType: string;
|
||||
syncIntervalPredefined?: string;
|
||||
syncIntervalCron?: string;
|
||||
autoDownloadNew: boolean;
|
||||
autoUpdateExisting: boolean;
|
||||
notificationEnabled: boolean;
|
||||
appriseUrls?: string[] | string;
|
||||
lastAutoSync?: string;
|
||||
lastAutoSyncError?: string;
|
||||
lastAutoSyncErrorTime?: string;
|
||||
testNotification?: boolean;
|
||||
triggerManualSync?: boolean;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const settings = await request.json();
|
||||
const settings = await request.json() as AutoSyncSettings;
|
||||
|
||||
if (!settings || typeof settings !== 'object') {
|
||||
return NextResponse.json(
|
||||
@@ -54,7 +70,7 @@ export async function POST(request: NextRequest) {
|
||||
// Validate predefined interval
|
||||
if (settings.syncIntervalType === 'predefined') {
|
||||
const validIntervals = ['15min', '30min', '1hour', '6hours', '12hours', '24hours'];
|
||||
if (!validIntervals.includes(settings.syncIntervalPredefined)) {
|
||||
if (!settings.syncIntervalPredefined || !validIntervals.includes(settings.syncIntervalPredefined)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid predefined interval' },
|
||||
{ status: 400 }
|
||||
@@ -67,7 +83,7 @@ export async function POST(request: NextRequest) {
|
||||
if (!settings.syncIntervalCron || typeof settings.syncIntervalCron !== 'string' || settings.syncIntervalCron.trim() === '') {
|
||||
// Fallback to predefined if custom is selected but no cron expression
|
||||
settings.syncIntervalType = 'predefined';
|
||||
settings.syncIntervalPredefined = settings.syncIntervalPredefined || '1hour';
|
||||
settings.syncIntervalPredefined = settings.syncIntervalPredefined ?? '1hour';
|
||||
settings.syncIntervalCron = '';
|
||||
} else if (!isValidCron(settings.syncIntervalCron, { seconds: false })) {
|
||||
return NextResponse.json(
|
||||
@@ -109,7 +125,7 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (parseError) {
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid JSON format for Apprise URLs' },
|
||||
{ status: 400 }
|
||||
@@ -130,15 +146,15 @@ export async function POST(request: NextRequest) {
|
||||
const autoSyncSettings = {
|
||||
'AUTO_SYNC_ENABLED': settings.autoSyncEnabled ? 'true' : 'false',
|
||||
'SYNC_INTERVAL_TYPE': settings.syncIntervalType,
|
||||
'SYNC_INTERVAL_PREDEFINED': settings.syncIntervalPredefined || '',
|
||||
'SYNC_INTERVAL_CRON': settings.syncIntervalCron || '',
|
||||
'SYNC_INTERVAL_PREDEFINED': settings.syncIntervalPredefined ?? '',
|
||||
'SYNC_INTERVAL_CRON': settings.syncIntervalCron ?? '',
|
||||
'AUTO_DOWNLOAD_NEW': settings.autoDownloadNew ? 'true' : 'false',
|
||||
'AUTO_UPDATE_EXISTING': settings.autoUpdateExisting ? 'true' : 'false',
|
||||
'NOTIFICATION_ENABLED': settings.notificationEnabled ? 'true' : 'false',
|
||||
'APPRISE_URLS': Array.isArray(settings.appriseUrls) ? JSON.stringify(settings.appriseUrls) : (settings.appriseUrls || '[]'),
|
||||
'LAST_AUTO_SYNC': settings.lastAutoSync || '',
|
||||
'LAST_AUTO_SYNC_ERROR': settings.lastAutoSyncError || '',
|
||||
'LAST_AUTO_SYNC_ERROR_TIME': settings.lastAutoSyncErrorTime || ''
|
||||
'APPRISE_URLS': Array.isArray(settings.appriseUrls) ? JSON.stringify(settings.appriseUrls) : (settings.appriseUrls ?? '[]'),
|
||||
'LAST_AUTO_SYNC': settings.lastAutoSync ?? '',
|
||||
'LAST_AUTO_SYNC_ERROR': settings.lastAutoSyncError ?? '',
|
||||
'LAST_AUTO_SYNC_ERROR_TIME': settings.lastAutoSyncErrorTime ?? ''
|
||||
};
|
||||
|
||||
// Update or add each setting
|
||||
@@ -231,21 +247,21 @@ export async function GET() {
|
||||
autoSyncEnabled: getEnvValue(envContent, 'AUTO_SYNC_ENABLED') === 'true',
|
||||
syncIntervalType: getEnvValue(envContent, 'SYNC_INTERVAL_TYPE') || 'predefined',
|
||||
syncIntervalPredefined: getEnvValue(envContent, 'SYNC_INTERVAL_PREDEFINED') || '1hour',
|
||||
syncIntervalCron: getEnvValue(envContent, 'SYNC_INTERVAL_CRON') || '',
|
||||
syncIntervalCron: getEnvValue(envContent, 'SYNC_INTERVAL_CRON') ?? '',
|
||||
autoDownloadNew: getEnvValue(envContent, 'AUTO_DOWNLOAD_NEW') === 'true',
|
||||
autoUpdateExisting: getEnvValue(envContent, 'AUTO_UPDATE_EXISTING') === 'true',
|
||||
notificationEnabled: getEnvValue(envContent, 'NOTIFICATION_ENABLED') === 'true',
|
||||
appriseUrls: (() => {
|
||||
try {
|
||||
const urlsValue = getEnvValue(envContent, 'APPRISE_URLS') || '[]';
|
||||
return JSON.parse(urlsValue);
|
||||
const urlsValue = getEnvValue(envContent, 'APPRISE_URLS') ?? '[]';
|
||||
return JSON.parse(urlsValue) as string[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
})(),
|
||||
lastAutoSync: getEnvValue(envContent, 'LAST_AUTO_SYNC') || '',
|
||||
lastAutoSyncError: getEnvValue(envContent, 'LAST_AUTO_SYNC_ERROR') || null,
|
||||
lastAutoSyncErrorTime: getEnvValue(envContent, 'LAST_AUTO_SYNC_ERROR_TIME') || null
|
||||
lastAutoSync: getEnvValue(envContent, 'LAST_AUTO_SYNC') ?? '',
|
||||
lastAutoSyncError: getEnvValue(envContent, 'LAST_AUTO_SYNC_ERROR') ?? null,
|
||||
lastAutoSyncErrorTime: getEnvValue(envContent, 'LAST_AUTO_SYNC_ERROR_TIME') ?? null
|
||||
};
|
||||
|
||||
return NextResponse.json({ settings });
|
||||
@@ -275,8 +291,8 @@ async function handleTestNotification() {
|
||||
const notificationEnabled = getEnvValue(envContent, 'NOTIFICATION_ENABLED') === 'true';
|
||||
const appriseUrls = (() => {
|
||||
try {
|
||||
const urlsValue = getEnvValue(envContent, 'APPRISE_URLS') || '[]';
|
||||
return JSON.parse(urlsValue);
|
||||
const urlsValue = getEnvValue(envContent, 'APPRISE_URLS') ?? '[]';
|
||||
return JSON.parse(urlsValue) as string[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
@@ -289,7 +305,7 @@ async function handleTestNotification() {
|
||||
);
|
||||
}
|
||||
|
||||
if (!appriseUrls || appriseUrls.length === 0) {
|
||||
if (!appriseUrls?.length) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No Apprise URLs configured' },
|
||||
{ status: 400 }
|
||||
@@ -347,9 +363,9 @@ async function handleManualSync() {
|
||||
// Trigger manual sync using the auto-sync service
|
||||
const { AutoSyncService } = await import('../../../../server/services/autoSyncService.js');
|
||||
const autoSyncService = new AutoSyncService();
|
||||
const result = await autoSyncService.executeAutoSync() as any;
|
||||
const result = await autoSyncService.executeAutoSync() as { success: boolean; message?: string } | null;
|
||||
|
||||
if (result && result.success) {
|
||||
if (result?.success) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Manual sync completed successfully',
|
||||
@@ -357,7 +373,7 @@ async function handleManualSync() {
|
||||
});
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{ error: result.message },
|
||||
{ error: result?.message ?? 'Unknown error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
@@ -376,7 +392,7 @@ function getEnvValue(envContent: string, key: string): string {
|
||||
const regex = new RegExp(`^${key}="(.+)"$`, 'm');
|
||||
let match = regex.exec(envContent);
|
||||
|
||||
if (match && match[1]) {
|
||||
if (match?.[1]) {
|
||||
let value = match[1];
|
||||
// Remove extra quotes that might be around JSON values
|
||||
if (value.startsWith('"') && value.endsWith('"')) {
|
||||
@@ -388,7 +404,7 @@ function getEnvValue(envContent: string, key: string): string {
|
||||
// Try to match without quotes (fallback)
|
||||
const regexNoQuotes = new RegExp(`^${key}=([^\\s]*)$`, 'm');
|
||||
match = regexNoQuotes.exec(envContent);
|
||||
if (match && match[1]) {
|
||||
if (match?.[1]) {
|
||||
return match[1];
|
||||
}
|
||||
|
||||
|
||||
@@ -126,18 +126,26 @@ export default function Home() {
|
||||
.replace(/^-+|-+$/g, "");
|
||||
|
||||
// First deduplicate GitHub scripts using Map by slug
|
||||
const scriptMap = new Map<string, any>();
|
||||
interface ScriptCard {
|
||||
name?: string;
|
||||
slug?: string;
|
||||
install_basenames?: string[];
|
||||
}
|
||||
const scriptMap = new Map<string, ScriptCard>();
|
||||
|
||||
scriptCardsData.cards?.forEach((script) => {
|
||||
if (script?.name && script?.slug) {
|
||||
if (!scriptMap.has(script.slug)) {
|
||||
scriptMap.set(script.slug, script);
|
||||
scriptMap.set(script.slug, script as ScriptCard);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const deduplicatedGithubScripts = Array.from(scriptMap.values());
|
||||
const localScripts = localScriptsData.scripts ?? [];
|
||||
const localScripts = (localScriptsData.scripts ?? []) as Array<{
|
||||
name?: string;
|
||||
slug?: string;
|
||||
}>;
|
||||
|
||||
// Count scripts that are both in deduplicated GitHub data and have local versions
|
||||
// Use the same matching logic as DownloadedScriptsTab and ScriptsGrid
|
||||
@@ -154,26 +162,27 @@ export default function Home() {
|
||||
return true;
|
||||
}
|
||||
// Also try normalized slug matching (handles filename-based slugs vs JSON slugs)
|
||||
if (normalizeId(local.slug) === normalizeId(script.slug)) {
|
||||
if (
|
||||
normalizeId(local.slug ?? undefined) ===
|
||||
normalizeId(script.slug ?? undefined)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Secondary: Check install basenames (for edge cases where install script names differ from slugs)
|
||||
const normalizedLocal = normalizeId(local.name);
|
||||
const scriptWithBasenames = script as {
|
||||
install_basenames?: string[];
|
||||
};
|
||||
const normalizedLocal = normalizeId(local.name ?? undefined);
|
||||
const matchesInstallBasename =
|
||||
scriptWithBasenames.install_basenames?.some(
|
||||
(base: string) => normalizeId(base) === normalizedLocal,
|
||||
script.install_basenames?.some(
|
||||
(base) => normalizeId(String(base)) === normalizedLocal,
|
||||
) ?? false;
|
||||
if (matchesInstallBasename) return true;
|
||||
|
||||
// Tertiary: Normalized filename to normalized slug matching
|
||||
if (
|
||||
script.slug &&
|
||||
normalizeId(local.name) === normalizeId(script.slug)
|
||||
normalizeId(local.name ?? undefined) ===
|
||||
normalizeId(script.slug ?? undefined)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ export function getAuthConfig(): {
|
||||
const sessionDurationRegex = /^AUTH_SESSION_DURATION_DAYS=(.*)$/m;
|
||||
const sessionDurationMatch = sessionDurationRegex.exec(envContent);
|
||||
const sessionDurationDays = sessionDurationMatch
|
||||
? parseInt(sessionDurationMatch[1]?.trim() || String(DEFAULT_JWT_EXPIRY_DAYS), 10) || DEFAULT_JWT_EXPIRY_DAYS
|
||||
? parseInt(sessionDurationMatch[1]?.trim() ?? String(DEFAULT_JWT_EXPIRY_DAYS), 10) || DEFAULT_JWT_EXPIRY_DAYS
|
||||
: DEFAULT_JWT_EXPIRY_DAYS;
|
||||
|
||||
const hasCredentials = !!(username && passwordHash);
|
||||
|
||||
@@ -38,7 +38,7 @@ export const backupsRouter = createTRPCRouter({
|
||||
if (backups.length === 0) continue;
|
||||
|
||||
// Get hostname from first backup (all backups for same container should have same hostname)
|
||||
const hostname = backups[0]?.hostname || '';
|
||||
const hostname = backups[0]?.hostname ?? '';
|
||||
|
||||
result.push({
|
||||
container_id: containerId,
|
||||
|
||||
@@ -5,10 +5,47 @@ import { createHash } from "crypto";
|
||||
import type { Server } from "~/types/server";
|
||||
import { getStorageService } from "~/server/services/storageService";
|
||||
|
||||
// Type for SSH connection test result
|
||||
interface SSHConnectionResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// Type for parsed LXC config
|
||||
interface ParsedLXCConfig {
|
||||
arch?: string;
|
||||
cores?: number;
|
||||
memory?: number;
|
||||
hostname?: string;
|
||||
swap?: number;
|
||||
onboot?: number;
|
||||
ostype?: string;
|
||||
unprivileged?: number;
|
||||
tags?: string;
|
||||
net_name?: string;
|
||||
net_bridge?: string;
|
||||
net_hwaddr?: string;
|
||||
net_ip?: string;
|
||||
net_ip_type?: string;
|
||||
net_gateway?: string;
|
||||
net_type?: string;
|
||||
net_vlan?: number;
|
||||
feature_keyctl?: number;
|
||||
feature_nesting?: number;
|
||||
feature_fuse?: number;
|
||||
feature_mount?: string;
|
||||
rootfs_storage?: string;
|
||||
rootfs_size?: string;
|
||||
advanced_config?: string;
|
||||
config_hash?: string;
|
||||
synced_at?: Date;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// Helper function to parse raw LXC config into structured data
|
||||
function parseRawConfig(rawConfig: string): any {
|
||||
function parseRawConfig(rawConfig: string): ParsedLXCConfig {
|
||||
const lines = rawConfig.split('\n');
|
||||
const config: any = { advanced: [] };
|
||||
const config: ParsedLXCConfig & { advanced: string[] } = { advanced: [] };
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
@@ -80,17 +117,18 @@ function parseRawConfig(rawConfig: string): any {
|
||||
}
|
||||
|
||||
config.advanced_config = config.advanced.join('\n');
|
||||
delete config.advanced; // Remove the advanced array since we only need advanced_config
|
||||
return config;
|
||||
// Remove the advanced array after copying to advanced_config
|
||||
const { advanced: _, ...configWithoutAdvanced } = config;
|
||||
return configWithoutAdvanced;
|
||||
}
|
||||
|
||||
// Helper function to reconstruct config from structured data
|
||||
function reconstructConfig(parsed: any): string {
|
||||
function reconstructConfig(parsed: ParsedLXCConfig): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
// Add standard fields in order
|
||||
if (parsed.arch) lines.push(`arch: ${parsed.arch}`);
|
||||
if (parsed.cores) lines.push(`cores: ${parsed.cores}`);
|
||||
if (parsed.arch) lines.push(`arch: ${String(parsed.arch)}`);
|
||||
if (parsed.cores) lines.push(`cores: ${String(parsed.cores)}`);
|
||||
|
||||
// Build features line
|
||||
if (parsed.feature_keyctl !== undefined || parsed.feature_nesting !== undefined || parsed.feature_fuse !== undefined) {
|
||||
@@ -666,12 +704,12 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
|
||||
// Test SSH connection first
|
||||
|
||||
const connectionTest = await sshService.testSSHConnection(server as Server);
|
||||
const connectionTest = await sshService.testSSHConnection(server as Server) as SSHConnectionResult;
|
||||
|
||||
if (!(connectionTest as any).success) {
|
||||
if (!connectionTest.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: `SSH connection failed: ${(connectionTest as any).error ?? 'Unknown error'}`,
|
||||
error: `SSH connection failed: ${connectionTest.error ?? 'Unknown error'}`,
|
||||
detectedContainers: []
|
||||
};
|
||||
}
|
||||
@@ -744,8 +782,8 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
containerId,
|
||||
hostname,
|
||||
configPath,
|
||||
serverId: Number((server as any).id),
|
||||
serverName: (server as any).name,
|
||||
serverId: Number(server.id),
|
||||
serverName: String(server.name),
|
||||
parsedConfig: {
|
||||
...parsedConfig,
|
||||
config_hash: configHash,
|
||||
@@ -914,9 +952,9 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
// Test SSH connection
|
||||
const connectionTest = await sshService.testSSHConnection(server as Server);
|
||||
if (!(connectionTest as any).success) {
|
||||
console.warn(`cleanupOrphanedScripts: SSH connection failed for server ${String((server as any).name)}, skipping ${serverScripts.length} scripts`);
|
||||
const connectionTest = await sshService.testSSHConnection(server as Server) as SSHConnectionResult;
|
||||
if (!connectionTest.success) {
|
||||
console.warn(`cleanupOrphanedScripts: SSH connection failed for server ${String(String(server.name))}, skipping ${serverScripts.length} scripts`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -926,7 +964,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
|
||||
const existingContainerIds = await new Promise<Set<string>>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
console.warn(`cleanupOrphanedScripts: timeout while getting container list from server ${String((server as any).name)}`);
|
||||
console.warn(`cleanupOrphanedScripts: timeout while getting container list from server ${String(String(server.name))}`);
|
||||
resolve(new Set()); // Treat timeout as no containers found
|
||||
}, 20000);
|
||||
|
||||
@@ -937,7 +975,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
listOutput += data;
|
||||
},
|
||||
(error: string) => {
|
||||
console.error(`cleanupOrphanedScripts: error getting container list from server ${String((server as any).name)}:`, error);
|
||||
console.error(`cleanupOrphanedScripts: error getting container list from server ${String(String(server.name))}:`, error);
|
||||
clearTimeout(timeout);
|
||||
resolve(new Set()); // Treat error as no containers found
|
||||
},
|
||||
@@ -1010,7 +1048,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
|
||||
// If container is not in pct list AND config file doesn't exist, it's orphaned
|
||||
if (!configExists) {
|
||||
console.log(`cleanupOrphanedScripts: Removing orphaned script ${String(scriptData.script_name)} (container ${containerId}) from server ${String((server as any).name)}`);
|
||||
console.log(`cleanupOrphanedScripts: Removing orphaned script ${String(scriptData.script_name)} (container ${containerId}) from server ${String(String(server.name))}`);
|
||||
await db.deleteInstalledScript(Number(scriptData.id));
|
||||
deletedScripts.push(String(scriptData.script_name));
|
||||
} else {
|
||||
@@ -1019,7 +1057,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`cleanupOrphanedScripts: Error checking script ${String((scriptData as any).script_name)}:`, error);
|
||||
console.error(`cleanupOrphanedScripts: Error checking script ${String(scriptData.script_name)}:`, error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -1075,8 +1113,8 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
|
||||
// Test SSH connection
|
||||
|
||||
const connectionTest = await sshService.testSSHConnection(server as Server);
|
||||
if (!(connectionTest as any).success) {
|
||||
const connectionTest = await sshService.testSSHConnection(server as Server) as SSHConnectionResult;
|
||||
if (!connectionTest.success) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1099,7 +1137,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
listOutput += data;
|
||||
},
|
||||
(error: string) => {
|
||||
console.error(`pct list error on server ${(server as any).name}:`, error);
|
||||
console.error(`pct list error on server ${String(server.name)}:`, error);
|
||||
reject(new Error(error));
|
||||
},
|
||||
(_exitCode: number) => {
|
||||
@@ -1134,7 +1172,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing server ${(server as any).name}:`, error);
|
||||
console.error(`Error processing server ${String(server.name)}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1198,11 +1236,11 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
|
||||
// Test SSH connection first
|
||||
|
||||
const connectionTest = await sshService.testSSHConnection(server as Server);
|
||||
if (!(connectionTest as any).success) {
|
||||
const connectionTest = await sshService.testSSHConnection(server as Server) as SSHConnectionResult;
|
||||
if (!connectionTest.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: `SSH connection failed: ${(connectionTest as any).error ?? 'Unknown error'}`,
|
||||
error: `SSH connection failed: ${connectionTest.error ?? 'Unknown error'}`,
|
||||
status: 'unknown' as const
|
||||
};
|
||||
}
|
||||
@@ -1297,11 +1335,11 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
|
||||
// Test SSH connection first
|
||||
|
||||
const connectionTest = await sshService.testSSHConnection(server as Server);
|
||||
if (!(connectionTest as any).success) {
|
||||
const connectionTest = await sshService.testSSHConnection(server as Server) as SSHConnectionResult;
|
||||
if (!connectionTest.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: `SSH connection failed: ${(connectionTest as any).error ?? 'Unknown error'}`
|
||||
error: `SSH connection failed: ${connectionTest.error ?? 'Unknown error'}`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1388,11 +1426,11 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
|
||||
// Test SSH connection first
|
||||
|
||||
const connectionTest = await sshService.testSSHConnection(server as Server);
|
||||
if (!(connectionTest as any).success) {
|
||||
const connectionTest = await sshService.testSSHConnection(server as Server) as SSHConnectionResult;
|
||||
if (!connectionTest.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: `SSH connection failed: ${(connectionTest as any).error ?? 'Unknown error'}`
|
||||
error: `SSH connection failed: ${connectionTest.error ?? 'Unknown error'}`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1551,7 +1589,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
};
|
||||
}
|
||||
|
||||
console.log('🖥️ Server found:', { id: (server as any).id, name: (server as any).name, ip: (server as any).ip });
|
||||
console.log('🖥️ Server found:', { id: Number(server.id), name: String(server.name), ip: String(server.ip) });
|
||||
|
||||
// Import SSH services
|
||||
const { default: SSHService } = await import('~/server/ssh-service');
|
||||
@@ -1562,12 +1600,12 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
// Test SSH connection first
|
||||
console.log('🔌 Testing SSH connection...');
|
||||
|
||||
const connectionTest = await sshService.testSSHConnection(server as Server);
|
||||
if (!(connectionTest as any).success) {
|
||||
console.log('❌ SSH connection failed:', (connectionTest as any).error);
|
||||
const connectionTest = await sshService.testSSHConnection(server as Server) as SSHConnectionResult;
|
||||
if (!connectionTest.success) {
|
||||
console.log('❌ SSH connection failed:', connectionTest.error);
|
||||
return {
|
||||
success: false,
|
||||
error: `SSH connection failed: ${(connectionTest as any).error ?? 'Unknown error'}`
|
||||
error: `SSH connection failed: ${connectionTest.error ?? 'Unknown error'}`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1666,11 +1704,11 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
console.log('✅ Successfully updated database');
|
||||
return {
|
||||
success: true,
|
||||
message: `Successfully detected IP: ${detectedIp}:${detectedPort} for LXC ${scriptData.container_id} on ${(server as any).name}`,
|
||||
message: `Successfully detected IP: ${detectedIp}:${detectedPort} for LXC ${scriptData.container_id} on ${String(server.name)}`,
|
||||
detectedIp,
|
||||
detectedPort: detectedPort,
|
||||
containerId: scriptData.container_id,
|
||||
serverName: (server as any).name
|
||||
serverName: String(server.name)
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error in autoDetectWebUI:', error);
|
||||
@@ -1739,11 +1777,11 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
const sshExecutionService = new SSHExecutionService();
|
||||
|
||||
// Test SSH connection
|
||||
const connectionTest = await sshService.testSSHConnection(server as Server);
|
||||
if (!(connectionTest as any).success) {
|
||||
const connectionTest = await sshService.testSSHConnection(server as Server) as SSHConnectionResult;
|
||||
if (!connectionTest.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: `SSH connection failed: ${(connectionTest as any).error ?? 'Unknown error'}`
|
||||
error: `SSH connection failed: ${connectionTest.error ?? 'Unknown error'}`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1857,11 +1895,11 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
const sshExecutionService = new SSHExecutionService();
|
||||
|
||||
// Test SSH connection
|
||||
const connectionTest = await sshService.testSSHConnection(server as Server);
|
||||
if (!(connectionTest as any).success) {
|
||||
const connectionTest = await sshService.testSSHConnection(server as Server) as SSHConnectionResult;
|
||||
if (!connectionTest.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: `SSH connection failed: ${(connectionTest as any).error ?? 'Unknown error'}`
|
||||
error: `SSH connection failed: ${connectionTest.error ?? 'Unknown error'}`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2068,11 +2106,11 @@ EOFCONFIG`;
|
||||
const sshExecutionService = getSSHExecutionService();
|
||||
|
||||
// Test SSH connection first
|
||||
const connectionTest = await sshService.testSSHConnection(server as Server);
|
||||
if (!(connectionTest as any).success) {
|
||||
const connectionTest = await sshService.testSSHConnection(server as Server) as SSHConnectionResult;
|
||||
if (!connectionTest.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: `SSH connection failed: ${(connectionTest as any).error ?? 'Unknown error'}`,
|
||||
error: `SSH connection failed: ${connectionTest.error ?? 'Unknown error'}`,
|
||||
storages: [],
|
||||
cached: false
|
||||
};
|
||||
@@ -2170,11 +2208,11 @@ EOFCONFIG`;
|
||||
const sshService = new SSHService();
|
||||
|
||||
// Test SSH connection first
|
||||
const connectionTest = await sshService.testSSHConnection(server as Server);
|
||||
if (!(connectionTest as any).success) {
|
||||
const connectionTest = await sshService.testSSHConnection(server as Server) as SSHConnectionResult;
|
||||
if (!connectionTest.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: `SSH connection failed: ${(connectionTest as any).error ?? 'Unknown error'}`,
|
||||
error: `SSH connection failed: ${connectionTest.error ?? 'Unknown error'}`,
|
||||
executionId: null
|
||||
};
|
||||
}
|
||||
@@ -2199,3 +2237,6 @@ EOFCONFIG`;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined;
|
||||
};
|
||||
|
||||
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
|
||||
export const prisma: PrismaClient = globalForPrisma.prisma ?? new PrismaClient({
|
||||
log: ['warn', 'error']
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user