import type { StartupValidationCheckDto, StartupValidationReportDto } from "@mrp/shared"; import { constants as fsConstants } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; import { env } from "../config/env.js"; import { paths } from "../config/paths.js"; import { prisma } from "./prisma.js"; async function pathExists(targetPath: string) { try { await fs.access(targetPath); return true; } catch { return false; } } async function canWritePath(targetPath: string) { try { await fs.access(targetPath, fsConstants.W_OK); return true; } catch { return false; } } export async function collectStartupValidationReport(): Promise { const startedAt = Date.now(); const checks: StartupValidationCheckDto[] = []; const dataDirExists = await pathExists(paths.dataDir); const uploadsDirExists = await pathExists(paths.uploadsDir); const prismaDirExists = await pathExists(paths.prismaDir); const databaseFilePath = path.join(paths.prismaDir, "app.db"); const databaseFileExists = await pathExists(databaseFilePath); const clientBundlePath = path.join(paths.clientDistDir, "index.html"); const clientBundleExists = await pathExists(clientBundlePath); const puppeteerPath = env.PUPPETEER_EXECUTABLE_PATH || "/usr/bin/chromium"; const puppeteerExists = await pathExists(puppeteerPath); const dataDirWritable = dataDirExists && (await canWritePath(paths.dataDir)); const uploadsDirWritable = uploadsDirExists && (await canWritePath(paths.uploadsDir)); checks.push({ id: "data-dir", label: "Data directory", status: dataDirExists ? "PASS" : "FAIL", message: dataDirExists ? `Data directory available at ${paths.dataDir}.` : `Data directory is missing: ${paths.dataDir}.`, }); checks.push({ id: "uploads-dir", label: "Uploads directory", status: uploadsDirExists ? "PASS" : "FAIL", message: uploadsDirExists ? `Uploads directory available at ${paths.uploadsDir}.` : `Uploads directory is missing: ${paths.uploadsDir}.`, }); checks.push({ id: "prisma-dir", label: "Prisma directory", status: prismaDirExists ? "PASS" : "FAIL", message: prismaDirExists ? `Prisma data directory available at ${paths.prismaDir}.` : `Prisma data directory is missing: ${paths.prismaDir}.`, }); checks.push({ id: "database-file", label: "Database file", status: databaseFileExists ? "PASS" : env.NODE_ENV === "production" ? "FAIL" : "WARN", message: databaseFileExists ? `SQLite database file found at ${databaseFilePath}.` : `SQLite database file is missing: ${databaseFilePath}.`, }); checks.push({ id: "data-dir-write", label: "Data directory writable", status: dataDirWritable ? "PASS" : "FAIL", message: dataDirWritable ? `Application can write to ${paths.dataDir}.` : `Application cannot write to ${paths.dataDir}.`, }); checks.push({ id: "uploads-dir-write", label: "Uploads directory writable", status: uploadsDirWritable ? "PASS" : "FAIL", message: uploadsDirWritable ? `Application can write to ${paths.uploadsDir}.` : `Application cannot write to ${paths.uploadsDir}.`, }); try { await prisma.$queryRawUnsafe("SELECT 1"); checks.push({ id: "database-connection", label: "Database connection", status: "PASS", message: "SQLite connection check succeeded.", }); } catch (error) { checks.push({ id: "database-connection", label: "Database connection", status: "FAIL", message: error instanceof Error ? error.message : "SQLite connection check failed.", }); } if (env.NODE_ENV === "production") { checks.push({ id: "client-dist", label: "Client bundle", status: clientBundleExists ? "PASS" : "FAIL", message: clientBundleExists ? `Client bundle found at ${paths.clientDistDir}.` : `Production client bundle is missing from ${paths.clientDistDir}.`, }); } else { checks.push({ id: "client-dist", label: "Client bundle", status: "PASS", message: "Client bundle check skipped outside production mode.", }); } checks.push({ id: "puppeteer-runtime", label: "PDF runtime", status: puppeteerExists ? "PASS" : env.NODE_ENV === "production" ? "FAIL" : "WARN", message: puppeteerExists ? `Chromium runtime available at ${puppeteerPath}.` : `Chromium runtime was not found at ${puppeteerPath}.`, }); checks.push({ id: "client-origin", label: "Client origin", status: env.NODE_ENV === "production" && env.CLIENT_ORIGIN.includes("localhost") ? "WARN" : "PASS", message: env.NODE_ENV === "production" && env.CLIENT_ORIGIN.includes("localhost") ? `Production CLIENT_ORIGIN still points to localhost: ${env.CLIENT_ORIGIN}.` : `Client origin is configured as ${env.CLIENT_ORIGIN}.`, }); checks.push({ id: "jwt-secret", label: "JWT secret", status: env.NODE_ENV === "production" && env.JWT_SECRET === "change-me" ? "WARN" : "PASS", message: env.NODE_ENV === "production" && env.JWT_SECRET === "change-me" ? "Production is still using the default JWT secret." : "JWT secret is not using the default production value.", }); checks.push({ id: "admin-password", label: "Bootstrap admin password", status: env.NODE_ENV === "production" && env.ADMIN_PASSWORD === "ChangeMe123!" ? "WARN" : "PASS", message: env.NODE_ENV === "production" && env.ADMIN_PASSWORD === "ChangeMe123!" ? "Production is still using the default bootstrap admin password." : "Bootstrap admin credentials are not using the default production password.", }); const status = checks.some((check) => check.status === "FAIL") ? "FAIL" : checks.some((check) => check.status === "WARN") ? "WARN" : "PASS"; return { status, generatedAt: new Date().toISOString(), durationMs: Date.now() - startedAt, passCount: checks.filter((check) => check.status === "PASS").length, warnCount: checks.filter((check) => check.status === "WARN").length, failCount: checks.filter((check) => check.status === "FAIL").length, checks, }; } export async function assertStartupReadiness() { const report = await collectStartupValidationReport(); if (report.status === "FAIL") { const failedChecks = report.checks.filter((check) => check.status === "FAIL").map((check) => `${check.label}: ${check.message}`); throw new Error(`Startup validation failed. ${failedChecks.join(" | ")}`); } return report; }