admin services

This commit is contained in:
2026-03-15 14:57:41 -05:00
parent 3197e68749
commit 28b23bc355
15 changed files with 401 additions and 30 deletions

View File

@@ -0,0 +1,19 @@
import type { StartupValidationCheckDto } from "@mrp/shared";
interface StartupValidationReport {
status: "PASS" | "WARN" | "FAIL";
checks: StartupValidationCheckDto[];
}
let latestStartupReport: StartupValidationReport = {
status: "WARN",
checks: [],
};
export function setLatestStartupReport(report: StartupValidationReport) {
latestStartupReport = report;
}
export function getLatestStartupReport() {
return latestStartupReport;
}

View File

@@ -0,0 +1,138 @@
import type { StartupValidationCheckDto } from "@mrp/shared";
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";
interface StartupValidationReport {
status: "PASS" | "WARN" | "FAIL";
checks: StartupValidationCheckDto[];
}
async function pathExists(targetPath: string) {
try {
await fs.access(targetPath);
return true;
} catch {
return false;
}
}
export async function collectStartupValidationReport(): Promise<StartupValidationReport> {
const checks: StartupValidationCheckDto[] = [];
checks.push({
id: "data-dir",
label: "Data directory",
status: (await pathExists(paths.dataDir)) ? "PASS" : "FAIL",
message: (await pathExists(paths.dataDir)) ? `Data directory available at ${paths.dataDir}.` : `Data directory is missing: ${paths.dataDir}.`,
});
checks.push({
id: "uploads-dir",
label: "Uploads directory",
status: (await pathExists(paths.uploadsDir)) ? "PASS" : "FAIL",
message: (await pathExists(paths.uploadsDir))
? `Uploads directory available at ${paths.uploadsDir}.`
: `Uploads directory is missing: ${paths.uploadsDir}.`,
});
checks.push({
id: "prisma-dir",
label: "Prisma directory",
status: (await pathExists(paths.prismaDir)) ? "PASS" : "FAIL",
message: (await pathExists(paths.prismaDir))
? `Prisma data directory available at ${paths.prismaDir}.`
: `Prisma data directory is missing: ${paths.prismaDir}.`,
});
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: (await pathExists(path.join(paths.clientDistDir, "index.html"))) ? "PASS" : "FAIL",
message: (await pathExists(path.join(paths.clientDistDir, "index.html")))
? `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.",
});
}
const puppeteerPath = env.PUPPETEER_EXECUTABLE_PATH || "/usr/bin/chromium";
const puppeteerExists = await pathExists(puppeteerPath);
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: "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,
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;
}

View File

@@ -14,6 +14,7 @@ import { paths } from "../../config/paths.js";
import { logAuditEvent } from "../../lib/audit.js";
import { hashPassword } from "../../lib/password.js";
import { prisma } from "../../lib/prisma.js";
import { getLatestStartupReport } from "../../lib/startup-state.js";
function mapAuditEvent(record: {
id: string;
@@ -464,6 +465,7 @@ export async function updateAdminUser(userId: string, payload: AdminUserInput, a
}
export async function getAdminDiagnostics(): Promise<AdminDiagnosticsDto> {
const startupReport = getLatestStartupReport();
const [
companyProfile,
userCount,
@@ -540,6 +542,8 @@ export async function getAdminDiagnostics(): Promise<AdminDiagnosticsDto> {
shipmentCount,
attachmentCount,
auditEventCount,
startupStatus: startupReport.status,
startupChecks: startupReport.checks,
recentAuditEvents: recentAuditEvents.map(mapAuditEvent),
};
}

View File

@@ -119,7 +119,7 @@ crmRouter.post("/customers", requirePermissions([permissions.crmWrite]), async (
return fail(response, 400, "INVALID_INPUT", "Customer payload is invalid.");
}
const customer = await createCustomer(parsed.data);
const customer = await createCustomer(parsed.data, request.authUser?.id);
if (!customer) {
return fail(response, 400, "INVALID_INPUT", "Customer reseller relationship is invalid.");
}
@@ -143,7 +143,7 @@ crmRouter.put("/customers/:customerId", requirePermissions([permissions.crmWrite
return fail(response, 404, "CRM_CUSTOMER_NOT_FOUND", "Customer record was not found.");
}
const customer = await updateCustomer(customerId, parsed.data);
const customer = await updateCustomer(customerId, parsed.data, request.authUser?.id);
if (!customer) {
return fail(response, 400, "INVALID_INPUT", "Customer reseller relationship is invalid.");
}
@@ -181,7 +181,7 @@ crmRouter.post("/customers/:customerId/contacts", requirePermissions([permission
return fail(response, 400, "INVALID_INPUT", "CRM contact is invalid.");
}
const contact = await createCustomerContact(customerId, parsed.data);
const contact = await createCustomerContact(customerId, parsed.data, request.authUser?.id);
if (!contact) {
return fail(response, 404, "CRM_CUSTOMER_NOT_FOUND", "Customer record was not found.");
}
@@ -227,7 +227,7 @@ crmRouter.post("/vendors", requirePermissions([permissions.crmWrite]), async (re
return fail(response, 400, "INVALID_INPUT", "Vendor payload is invalid.");
}
return ok(response, await createVendor(parsed.data), 201);
return ok(response, await createVendor(parsed.data, request.authUser?.id), 201);
});
crmRouter.put("/vendors/:vendorId", requirePermissions([permissions.crmWrite]), async (request, response) => {
@@ -241,7 +241,7 @@ crmRouter.put("/vendors/:vendorId", requirePermissions([permissions.crmWrite]),
return fail(response, 400, "INVALID_INPUT", "Vendor payload is invalid.");
}
const vendor = await updateVendor(vendorId, parsed.data);
const vendor = await updateVendor(vendorId, parsed.data, request.authUser?.id);
if (!vendor) {
return fail(response, 404, "CRM_VENDOR_NOT_FOUND", "Vendor record was not found.");
}
@@ -279,7 +279,7 @@ crmRouter.post("/vendors/:vendorId/contacts", requirePermissions([permissions.cr
return fail(response, 400, "INVALID_INPUT", "CRM contact is invalid.");
}
const contact = await createVendorContact(vendorId, parsed.data);
const contact = await createVendorContact(vendorId, parsed.data, request.authUser?.id);
if (!contact) {
return fail(response, 404, "CRM_VENDOR_NOT_FOUND", "Vendor record was not found.");
}

View File

@@ -15,6 +15,7 @@ import type {
} from "@mrp/shared/dist/crm/types.js";
import type { Customer, Vendor } from "@prisma/client";
import { logAuditEvent } from "../../lib/audit.js";
import { prisma } from "../../lib/prisma.js";
function mapSummary(record: Customer | Vendor): CrmRecordSummaryDto {
@@ -397,7 +398,7 @@ export async function getCustomerById(customerId: string) {
return mapCustomerDetail(customer, attachmentCount);
}
export async function createCustomer(payload: CrmRecordInput) {
export async function createCustomer(payload: CrmRecordInput, actorId?: string | null) {
if (payload.parentCustomerId) {
const parentCustomer = await prisma.customer.findUnique({
where: { id: payload.parentCustomerId },
@@ -436,6 +437,20 @@ export async function createCustomer(payload: CrmRecordInput) {
},
});
await logAuditEvent({
actorId,
entityType: "crm-customer",
entityId: customer.id,
action: "created",
summary: `Created customer ${customer.name}.`,
metadata: {
name: customer.name,
status: customer.status,
lifecycleStage: customer.lifecycleStage,
isReseller: customer.isReseller,
},
});
return {
...mapDetail(customer),
isReseller: customer.isReseller,
@@ -463,7 +478,7 @@ export async function createCustomer(payload: CrmRecordInput) {
};
}
export async function updateCustomer(customerId: string, payload: CrmRecordInput) {
export async function updateCustomer(customerId: string, payload: CrmRecordInput, actorId?: string | null) {
const existingCustomer = await prisma.customer.findUnique({
where: { id: customerId },
});
@@ -515,6 +530,20 @@ export async function updateCustomer(customerId: string, payload: CrmRecordInput
},
});
await logAuditEvent({
actorId,
entityType: "crm-customer",
entityId: customer.id,
action: "updated",
summary: `Updated customer ${customer.name}.`,
metadata: {
name: customer.name,
status: customer.status,
lifecycleStage: customer.lifecycleStage,
isReseller: customer.isReseller,
},
});
return {
...mapDetail(customer),
isReseller: customer.isReseller,
@@ -630,7 +659,7 @@ export async function getVendorById(vendorId: string) {
return mapVendorDetail(vendor, attachmentCount);
}
export async function createVendor(payload: CrmRecordInput) {
export async function createVendor(payload: CrmRecordInput, actorId?: string | null) {
const vendor = await prisma.vendor.create({
data: {
name: payload.name,
@@ -656,6 +685,19 @@ export async function createVendor(payload: CrmRecordInput) {
},
});
await logAuditEvent({
actorId,
entityType: "crm-vendor",
entityId: vendor.id,
action: "created",
summary: `Created vendor ${vendor.name}.`,
metadata: {
name: vendor.name,
status: vendor.status,
lifecycleStage: vendor.lifecycleStage,
},
});
return {
...mapDetail(vendor),
paymentTerms: vendor.paymentTerms,
@@ -677,7 +719,7 @@ export async function createVendor(payload: CrmRecordInput) {
};
}
export async function updateVendor(vendorId: string, payload: CrmRecordInput) {
export async function updateVendor(vendorId: string, payload: CrmRecordInput, actorId?: string | null) {
const existingVendor = await prisma.vendor.findUnique({
where: { id: vendorId },
});
@@ -712,6 +754,19 @@ export async function updateVendor(vendorId: string, payload: CrmRecordInput) {
},
});
await logAuditEvent({
actorId,
entityType: "crm-vendor",
entityId: vendor.id,
action: "updated",
summary: `Updated vendor ${vendor.name}.`,
metadata: {
name: vendor.name,
status: vendor.status,
lifecycleStage: vendor.lifecycleStage,
},
});
return {
...mapDetail(vendor),
paymentTerms: vendor.paymentTerms,
@@ -756,6 +811,19 @@ export async function createCustomerContactEntry(customerId: string, payload: Cr
},
});
await logAuditEvent({
actorId: createdById,
entityType: "crm-customer",
entityId: customerId,
action: "contact-entry.created",
summary: `Added ${payload.type.toLowerCase()} contact history for customer ${existingCustomer.name}.`,
metadata: {
type: payload.type,
summary: payload.summary,
contactAt: payload.contactAt,
},
});
return mapContactEntry(entry);
}
@@ -782,10 +850,23 @@ export async function createVendorContactEntry(vendorId: string, payload: CrmCon
},
});
await logAuditEvent({
actorId: createdById,
entityType: "crm-vendor",
entityId: vendorId,
action: "contact-entry.created",
summary: `Added ${payload.type.toLowerCase()} contact history for vendor ${existingVendor.name}.`,
metadata: {
type: payload.type,
summary: payload.summary,
contactAt: payload.contactAt,
},
});
return mapContactEntry(entry);
}
export async function createCustomerContact(customerId: string, payload: CrmContactInput) {
export async function createCustomerContact(customerId: string, payload: CrmContactInput, actorId?: string | null) {
const existingCustomer = await prisma.customer.findUnique({
where: { id: customerId },
});
@@ -812,10 +893,24 @@ export async function createCustomerContact(customerId: string, payload: CrmCont
},
});
await logAuditEvent({
actorId,
entityType: "crm-customer",
entityId: customerId,
action: "contact.created",
summary: `Added contact ${contact.fullName} to customer ${existingCustomer.name}.`,
metadata: {
fullName: contact.fullName,
role: contact.role,
email: contact.email,
isPrimary: contact.isPrimary,
},
});
return mapCrmContact(contact);
}
export async function createVendorContact(vendorId: string, payload: CrmContactInput) {
export async function createVendorContact(vendorId: string, payload: CrmContactInput, actorId?: string | null) {
const existingVendor = await prisma.vendor.findUnique({
where: { id: vendorId },
});
@@ -842,5 +937,19 @@ export async function createVendorContact(vendorId: string, payload: CrmContactI
},
});
await logAuditEvent({
actorId,
entityType: "crm-vendor",
entityId: vendorId,
action: "contact.created",
summary: `Added contact ${contact.fullName} to vendor ${existingVendor.name}.`,
metadata: {
fullName: contact.fullName,
role: contact.role,
email: contact.email,
isPrimary: contact.isPrimary,
},
});
return mapCrmContact(contact);
}

View File

@@ -67,7 +67,7 @@ shippingRouter.post("/shipments", requirePermissions([permissions.shippingWrite]
return fail(response, 400, "INVALID_INPUT", "Shipment payload is invalid.");
}
const result = await createShipment(parsed.data);
const result = await createShipment(parsed.data, request.authUser?.id);
if (!result.ok) {
return fail(response, 400, "INVALID_INPUT", result.reason);
}
@@ -86,7 +86,7 @@ shippingRouter.put("/shipments/:shipmentId", requirePermissions([permissions.shi
return fail(response, 400, "INVALID_INPUT", "Shipment payload is invalid.");
}
const result = await updateShipment(shipmentId, parsed.data);
const result = await updateShipment(shipmentId, parsed.data, request.authUser?.id);
if (!result.ok) {
return fail(response, 400, "INVALID_INPUT", result.reason);
}
@@ -105,7 +105,7 @@ shippingRouter.patch("/shipments/:shipmentId/status", requirePermissions([permis
return fail(response, 400, "INVALID_INPUT", "Shipment status payload is invalid.");
}
const result = await updateShipmentStatus(shipmentId, parsed.data.status);
const result = await updateShipmentStatus(shipmentId, parsed.data.status, request.authUser?.id);
if (!result.ok) {
return fail(response, 400, "INVALID_INPUT", result.reason);
}

View File

@@ -6,6 +6,7 @@ import type {
ShipmentSummaryDto,
} from "@mrp/shared/dist/shipping/types.js";
import { logAuditEvent } from "../../lib/audit.js";
import { prisma } from "../../lib/prisma.js";
export interface ShipmentPackingSlipData {
@@ -168,7 +169,7 @@ export async function getShipmentById(shipmentId: string) {
return shipment ? mapShipment(shipment) : null;
}
export async function createShipment(payload: ShipmentInput) {
export async function createShipment(payload: ShipmentInput, actorId?: string | null) {
const order = await prisma.salesOrder.findUnique({
where: { id: payload.salesOrderId },
select: { id: true },
@@ -195,10 +196,25 @@ export async function createShipment(payload: ShipmentInput) {
});
const detail = await getShipmentById(shipment.id);
if (detail) {
await logAuditEvent({
actorId,
entityType: "shipment",
entityId: shipment.id,
action: "created",
summary: `Created shipment ${detail.shipmentNumber}.`,
metadata: {
shipmentNumber: detail.shipmentNumber,
salesOrderId: detail.salesOrderId,
status: detail.status,
carrier: detail.carrier,
},
});
}
return detail ? { ok: true as const, shipment: detail } : { ok: false as const, reason: "Unable to load saved shipment." };
}
export async function updateShipment(shipmentId: string, payload: ShipmentInput) {
export async function updateShipment(shipmentId: string, payload: ShipmentInput, actorId?: string | null) {
const existing = await prisma.shipment.findUnique({
where: { id: shipmentId },
select: { id: true },
@@ -233,10 +249,25 @@ export async function updateShipment(shipmentId: string, payload: ShipmentInput)
});
const detail = await getShipmentById(shipmentId);
if (detail) {
await logAuditEvent({
actorId,
entityType: "shipment",
entityId: shipmentId,
action: "updated",
summary: `Updated shipment ${detail.shipmentNumber}.`,
metadata: {
shipmentNumber: detail.shipmentNumber,
salesOrderId: detail.salesOrderId,
status: detail.status,
carrier: detail.carrier,
},
});
}
return detail ? { ok: true as const, shipment: detail } : { ok: false as const, reason: "Unable to load saved shipment." };
}
export async function updateShipmentStatus(shipmentId: string, status: ShipmentStatus) {
export async function updateShipmentStatus(shipmentId: string, status: ShipmentStatus, actorId?: string | null) {
const existing = await prisma.shipment.findUnique({
where: { id: shipmentId },
select: { id: true },
@@ -253,6 +284,19 @@ export async function updateShipmentStatus(shipmentId: string, status: ShipmentS
});
const detail = await getShipmentById(shipmentId);
if (detail) {
await logAuditEvent({
actorId,
entityType: "shipment",
entityId: shipmentId,
action: "status.updated",
summary: `Updated shipment ${detail.shipmentNumber} to ${status}.`,
metadata: {
shipmentNumber: detail.shipmentNumber,
status,
},
});
}
return detail ? { ok: true as const, shipment: detail } : { ok: false as const, reason: "Unable to load updated shipment." };
}

View File

@@ -2,9 +2,17 @@ import { createApp } from "./app.js";
import { env } from "./config/env.js";
import { bootstrapAppData } from "./lib/bootstrap.js";
import { prisma } from "./lib/prisma.js";
import { setLatestStartupReport } from "./lib/startup-state.js";
import { assertStartupReadiness } from "./lib/startup-validation.js";
async function start() {
await bootstrapAppData();
const startupReport = await assertStartupReadiness();
setLatestStartupReport(startupReport);
for (const check of startupReport.checks.filter((entry) => entry.status !== "PASS")) {
console.warn(`[startup:${check.status.toLowerCase()}] ${check.label}: ${check.message}`);
}
const app = createApp();
const server = app.listen(env.PORT, () => {
@@ -25,4 +33,3 @@ start().catch(async (error) => {
await prisma.$disconnect();
process.exit(1);
});