Update SHA256 checksums, enhance BFF development scripts, and improve Salesforce connection handling

- Updated SHA256 checksums for the latest portal backend and frontend tar.gz files to reflect new builds.
- Introduced a new development script (`dev-watch.sh`) for the BFF application to streamline TypeScript building and aliasing during development.
- Refactored the `package.json` scripts in the BFF application to improve development workflow and added new watch commands.
- Enhanced the Salesforce connection service to support private key handling via environment variables, improving security and flexibility in configuration.
This commit is contained in:
barsa 2025-12-12 18:44:26 +09:00
parent a176f5d6ce
commit ff099525ca
10 changed files with 399 additions and 59 deletions

View File

@ -11,15 +11,20 @@
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"predev": "nest build && tsc-alias -p tsconfig.build.json --resolve-full-paths",
"dev": "nest start --watch --preserveWatchOutput",
"start:debug": "nest start --debug --watch",
"dev": "bash ./scripts/dev-watch.sh",
"dev:build:watch": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
"dev:alias": "tsc-alias -p tsconfig.build.json --resolve-full-paths",
"dev:alias:watch": "tsc-alias -p tsconfig.build.json --resolve-full-paths -w",
"dev:run:watch": "node --watch dist/main.js",
"dev:run:debug:watch": "node --inspect --watch dist/main.js",
"start:debug": "BFF_NODE_RUN_SCRIPT=dev:run:debug:watch bash ./scripts/dev-watch.sh",
"start:prod": "node dist/main.js",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test": "node --test",
"type-check": "tsc --project tsconfig.json --noEmit",
"type-check:watch": "tsc --project tsconfig.json --noEmit --watch",
"clean": "rm -rf dist tsconfig.build.tsbuildinfo",
"clean": "rm -rf dist .typecheck tsconfig.tsbuildinfo tsconfig.build.tsbuildinfo",
"db:migrate": "prisma migrate dev --config prisma/prisma.config.ts",
"db:generate": "prisma generate --config prisma/prisma.config.ts",
"db:studio": "prisma studio --port 5555 --config prisma/prisma.config.ts",

82
apps/bff/scripts/dev-watch.sh Executable file
View File

@ -0,0 +1,82 @@
#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
log() {
echo "[bff:dev] $*"
}
kill_tree() {
local pid="$1"
if [ -z "${pid:-}" ]; then return 0; fi
if kill -0 "$pid" >/dev/null 2>&1; then
kill "$pid" >/dev/null 2>&1 || true
# Give it a moment to exit gracefully
sleep 0.2 || true
kill -9 "$pid" >/dev/null 2>&1 || true
fi
}
PID_BUILD=""
PID_ALIAS=""
PID_NODE=""
cleanup() {
log "Stopping dev processes..."
kill_tree "$PID_NODE"
kill_tree "$PID_ALIAS"
kill_tree "$PID_BUILD"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
cd "$ROOT_DIR"
log "Starting TypeScript build watcher (tsc --watch)..."
pnpm run -s dev:build:watch &
PID_BUILD=$!
# Wait for initial output to exist
MAIN_FILE="$ROOT_DIR/dist/main.js"
WAIT_SECS="${BFF_BUILD_WAIT_SECS:-180}"
log "Waiting for initial build output: $MAIN_FILE (timeout: ${WAIT_SECS}s)"
loops=$((WAIT_SECS * 4))
for ((i=1; i<=loops; i++)); do
if [ -f "$MAIN_FILE" ]; then
break
fi
sleep 0.25
if ! kill -0 "$PID_BUILD" >/dev/null 2>&1; then
log "Build watcher exited unexpectedly."
exit 1
fi
done
if [ ! -f "$MAIN_FILE" ]; then
log "Timed out waiting for $MAIN_FILE"
log "Tip: if this keeps happening, run: pnpm -C \"$ROOT_DIR\" run build"
exit 1
fi
log "Rewriting path aliases in dist (tsc-alias)..."
pnpm run -s dev:alias
log "Starting alias rewriter watcher (tsc-alias -w)..."
pnpm run -s dev:alias:watch &
PID_ALIAS=$!
RUN_SCRIPT="${BFF_NODE_RUN_SCRIPT:-dev:run:watch}"
log "Starting Node runtime watcher ($RUN_SCRIPT)..."
pnpm run -s "$RUN_SCRIPT" &
PID_NODE=$!
# If any process exits, stop the rest
wait -n "$PID_BUILD" "$PID_ALIAS" "$PID_NODE"
exit_code=$?
log "A dev process exited (code=$exit_code). Shutting down."
exit "$exit_code"

View File

@ -120,6 +120,21 @@ export async function bootstrap(): Promise<INestApplication> {
// Rely on Nest's built-in shutdown hooks. External orchestrator will send signals.
app.enableShutdownHooks();
// #region agent log (hypothesis S1)
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
sessionId: "debug-session",
runId: "run1",
hypothesisId: "S1",
location: "apps/bff/src/app/bootstrap.ts:enableShutdownHooks",
message: "Nest shutdown hooks enabled",
data: { pid: process.pid },
timestamp: Date.now(),
}),
}).catch(() => {});
// #endregion
// API routing prefix is applied via RouterModule in AppModule for clarity and modern routing.

View File

@ -17,6 +17,9 @@ import { PrismaPg } from "@prisma/adapter-pg";
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(PrismaService.name);
private readonly pool: Pool;
private readonly instanceTag: string;
private destroyCalls = 0;
private poolEnded = false;
constructor() {
const connectionString = process.env.DATABASE_URL;
@ -40,16 +43,99 @@ export class PrismaService extends PrismaClient implements OnModuleInit, OnModul
super({ adapter });
this.pool = pool;
this.instanceTag = `${process.pid}:${Date.now()}`;
// #region agent log (hypothesis S3)
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
sessionId: "debug-session",
runId: "run1",
hypothesisId: "S3",
location: "apps/bff/src/infra/database/prisma.service.ts:constructor",
message: "PrismaService constructed",
data: { instanceTag: this.instanceTag, pid: process.pid },
timestamp: Date.now(),
}),
}).catch(() => {});
// #endregion
}
async onModuleInit() {
await this.$connect();
this.logger.log("Database connection established");
// #region agent log (hypothesis S3)
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
sessionId: "debug-session",
runId: "run1",
hypothesisId: "S3",
location: "apps/bff/src/infra/database/prisma.service.ts:onModuleInit",
message: "PrismaService connected",
data: { instanceTag: this.instanceTag },
timestamp: Date.now(),
}),
}).catch(() => {});
// #endregion
}
async onModuleDestroy() {
this.destroyCalls += 1;
// #region agent log (hypothesis S1)
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
sessionId: "debug-session",
runId: "run1",
hypothesisId: "S1",
location: "apps/bff/src/infra/database/prisma.service.ts:onModuleDestroy(entry)",
message: "PrismaService destroy called",
data: {
instanceTag: this.instanceTag,
destroyCalls: this.destroyCalls,
poolEnded: this.poolEnded,
pid: process.pid,
},
timestamp: Date.now(),
}),
}).catch(() => {});
// #endregion
// Make shutdown idempotent: Nest can call destroy hooks more than once during restarts.
if (this.poolEnded) {
this.logger.warn("Database pool already closed; skipping duplicate shutdown");
return;
}
this.poolEnded = true;
await this.$disconnect();
await this.pool.end();
this.logger.log("Database connection closed");
// #region agent log (hypothesis S1)
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
sessionId: "debug-session",
runId: "run1",
hypothesisId: "S1",
location: "apps/bff/src/infra/database/prisma.service.ts:onModuleDestroy(exit)",
message: "PrismaService destroy completed",
data: {
instanceTag: this.instanceTag,
destroyCalls: this.destroyCalls,
poolEnded: this.poolEnded,
},
timestamp: Date.now(),
}),
}).catch(() => {});
// #endregion
}
}

View File

@ -9,6 +9,56 @@ import { createPrivateKey } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
function normalizePrivateKeyInput(
raw: string
): { kind: "pem"; value: string } | { kind: "b64"; value: string } {
// Handle common "pasted" forms:
// - Windows newlines (\r\n)
// - literal "\n" sequences (when passed through env/template systems)
// - PEM with missing line breaks
// Avoid regex control characters (eslint no-control-regex); remove null bytes safely.
const trimmed = raw.trim().replaceAll("\u0000", "");
// Convert literal backslash-n into real newlines (only if present)
const withNewlines = trimmed.includes("\\n") ? trimmed.replaceAll("\\n", "\n") : trimmed;
const normalizedNewlines = withNewlines.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
// If it looks like PEM, ensure headers/footers are on their own lines.
if (normalizedNewlines.includes("-----BEGIN")) {
// IMPORTANT: Keep normalization line-based. Avoid patterns that can span across the
// base64 body (which often contains no '-' characters), otherwise we can corrupt
// otherwise-valid keys.
let pem = normalizedNewlines.trim();
// Ensure the BEGIN line ends with a newline (only if it's missing).
pem = pem.replace(/(-----BEGIN [^-\n]+-----)(?!\n)/g, "$1\n");
// Ensure the END line starts on its own line (only if it's missing).
pem = pem.replace(/([A-Za-z0-9+/=])\s*(-----END [^-\n]+-----)/g, "$1\n$2");
return { kind: "pem", value: pem.trim() + "\n" };
}
// Otherwise, treat it as base64 DER if it is base64-ish.
// (This supports secrets systems that store the key as base64 without PEM headers.)
const b64 = normalizedNewlines.replace(/\s+/g, "");
const looksBase64 = /^[A-Za-z0-9+/=]+$/.test(b64) && b64.length >= 256;
if (looksBase64) {
// Some systems store the PEM text itself as base64. If so, decode and retry as PEM.
try {
const decoded = Buffer.from(b64, "base64").toString("utf8").trim();
if (decoded.includes("-----BEGIN")) {
return normalizePrivateKeyInput(decoded);
}
} catch {
// ignore and fall back to DER handling
}
return { kind: "b64", value: b64 };
}
return { kind: "pem", value: normalizedNewlines.trim() + "\n" };
}
export interface SalesforceSObjectApi {
create: (data: Record<string, unknown>) => Promise<{ id?: string }>;
update?: (data: Record<string, unknown> & { Id: string }) => Promise<unknown>;
@ -78,64 +128,83 @@ export class SalesforceConnection {
const username = this.configService.get<string>("SF_USERNAME");
const clientId = this.configService.get<string>("SF_CLIENT_ID");
const privateKeyPath = this.configService.get<string>("SF_PRIVATE_KEY_PATH");
const privateKeyEnv = this.configService.get<string>("SF_PRIVATE_KEY_BASE64");
const audience =
this.configService.get<string>("SF_LOGIN_URL") || "https://login.salesforce.com";
// Gracefully skip connection if not configured for local/dev environments
if (!username || !clientId || !privateKeyPath) {
if (!username || !clientId || (!privateKeyEnv && !privateKeyPath)) {
const devMessage =
"Missing required Salesforce configuration. Please check SF_USERNAME, SF_CLIENT_ID, and SF_PRIVATE_KEY_PATH environment variables.";
"Missing required Salesforce configuration. Please check SF_USERNAME, SF_CLIENT_ID, and either SF_PRIVATE_KEY_BASE64 or SF_PRIVATE_KEY_PATH environment variables.";
throw new Error(isProd ? "Salesforce configuration is missing" : devMessage);
}
// Resolve private key and enforce allowed secrets directories
// Supports project-root ./secrets, apps/bff ./secrets, and container /app/secrets
const getProjectRoot = () => {
const cwd = process.cwd();
const norm = path.normalize(cwd);
const bffSuffix = path.normalize(path.join("apps", "bff"));
if (norm.endsWith(bffSuffix)) {
return path.resolve(cwd, "../..");
}
return cwd;
};
const isAbsolute = path.isAbsolute(privateKeyPath);
const resolvedKeyPath = isAbsolute
? privateKeyPath
: path.resolve(getProjectRoot(), privateKeyPath);
const allowedBases = [
path.resolve(getProjectRoot(), "secrets"),
path.resolve(process.cwd(), "secrets"),
"/app/secrets",
].map(p => path.normalize(p) + path.sep);
const normalizedKeyPath = path.normalize(resolvedKeyPath);
const isUnderAllowedBase = allowedBases.some(base =>
(normalizedKeyPath + path.sep).startsWith(base)
);
if (!isUnderAllowedBase) {
const devMsg = `Salesforce private key must be under one of the allowed secrets directories: ${allowedBases
.map(b => b.replace(/\\$/, ""))
.join(", ")}. Got: ${normalizedKeyPath}`;
throw new Error(isProd ? "Invalid Salesforce private key path" : devMsg);
}
try {
await fs.access(resolvedKeyPath);
} catch {
const devMsg = `Salesforce private key file not found at: ${resolvedKeyPath}. Ensure the file exists and has proper permissions (chmod 600).`;
throw new Error(isProd ? "Salesforce private key not found" : devMsg);
}
// Load private key
const privateKey = await fs.readFile(resolvedKeyPath, "utf8");
// - Prefer env-provided key for container/platforms where mounting a file is awkward
// - Otherwise read from path (restricted to allowed secrets dirs)
let rawPrivateKey: string;
if (privateKeyEnv) {
rawPrivateKey = privateKeyEnv;
} else {
const keyPath = privateKeyPath;
// This should already be guaranteed by the config check above, but keep TS happy and fail safely.
if (!keyPath) {
const devMessage =
"Missing required Salesforce configuration. Please check SF_PRIVATE_KEY_PATH environment variable.";
throw new Error(isProd ? "Salesforce configuration is missing" : devMessage);
}
// Resolve private key and enforce allowed secrets directories
// Supports project-root ./secrets, apps/bff ./secrets, and container /app/secrets
const getProjectRoot = () => {
const cwd = process.cwd();
const norm = path.normalize(cwd);
const bffSuffix = path.normalize(path.join("apps", "bff"));
if (norm.endsWith(bffSuffix)) {
return path.resolve(cwd, "../..");
}
return cwd;
};
const isAbsolute = path.isAbsolute(keyPath);
const resolvedKeyPath = isAbsolute ? keyPath : path.resolve(getProjectRoot(), keyPath);
const allowedBases = [
path.resolve(getProjectRoot(), "secrets"),
path.resolve(process.cwd(), "secrets"),
"/app/secrets",
].map(p => path.normalize(p) + path.sep);
const normalizedKeyPath = path.normalize(resolvedKeyPath);
const isUnderAllowedBase = allowedBases.some(base =>
(normalizedKeyPath + path.sep).startsWith(base)
);
if (!isUnderAllowedBase) {
const devMsg = `Salesforce private key must be under one of the allowed secrets directories: ${allowedBases
.map(b => b.replace(/\\$/, ""))
.join(", ")}. Got: ${normalizedKeyPath}`;
throw new Error(isProd ? "Invalid Salesforce private key path" : devMsg);
}
try {
await fs.access(resolvedKeyPath);
} catch {
const devMsg = `Salesforce private key file not found at: ${resolvedKeyPath}. Ensure the file exists and has proper permissions (chmod 600).`;
throw new Error(isProd ? "Salesforce private key not found" : devMsg);
}
rawPrivateKey = await fs.readFile(resolvedKeyPath, "utf8");
}
const normalizedKey = normalizePrivateKeyInput(rawPrivateKey);
// Validate private key format
const isPkcs8 = privateKey.includes("BEGIN PRIVATE KEY");
const isPkcs1Rsa = privateKey.includes("BEGIN RSA PRIVATE KEY");
if ((!isPkcs8 && !isPkcs1Rsa) || privateKey.includes("[PLACEHOLDER")) {
const pemForValidation = normalizedKey.kind === "pem" ? normalizedKey.value : "";
const isPkcs8 = pemForValidation.includes("BEGIN PRIVATE KEY");
const isPkcs1Rsa = pemForValidation.includes("BEGIN RSA PRIVATE KEY");
const containsPlaceholder =
rawPrivateKey.includes("[PLACEHOLDER") || pemForValidation.includes("[PLACEHOLDER");
if (containsPlaceholder || (normalizedKey.kind === "pem" && !isPkcs8 && !isPkcs1Rsa)) {
const devMsg =
"Salesforce private key appears to be invalid or still contains placeholder content. Expected a PEM key containing either 'BEGIN PRIVATE KEY' (PKCS8) or 'BEGIN RSA PRIVATE KEY' (PKCS1).";
throw new Error(isProd ? "Invalid Salesforce private key" : devMsg);
@ -144,10 +213,20 @@ export class SalesforceConnection {
// Create JWT assertion (jose). Use Node crypto to support both PKCS8 and PKCS1 PEMs.
let key: ReturnType<typeof createPrivateKey>;
try {
key = createPrivateKey({ key: privateKey, format: "pem" });
if (normalizedKey.kind === "pem") {
key = createPrivateKey({ key: normalizedKey.value, format: "pem" });
} else {
const der = Buffer.from(normalizedKey.value, "base64");
// Try PKCS8 first, then PKCS1 (RSA)
try {
key = createPrivateKey({ key: der, format: "der", type: "pkcs8" });
} catch {
key = createPrivateKey({ key: der, format: "der", type: "pkcs1" });
}
}
} catch {
const devMsg =
"Salesforce private key could not be parsed. Ensure it is a valid RSA PEM (PKCS8 'BEGIN PRIVATE KEY' or PKCS1 'BEGIN RSA PRIVATE KEY').";
"Salesforce private key could not be parsed. Ensure it is a valid RSA private key (PEM with BEGIN PRIVATE KEY / BEGIN RSA PRIVATE KEY, or base64-encoded DER).";
throw new Error(isProd ? "Invalid Salesforce private key" : devMsg);
}

View File

@ -11,6 +11,21 @@ for (const signal of signals) {
process.once(signal, () => {
void (async () => {
logger.log(`Received ${signal}. Closing Nest application...`);
// #region agent log (hypothesis S1)
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
sessionId: "debug-session",
runId: "run1",
hypothesisId: "S1",
location: "apps/bff/src/main.ts:signalHandler(entry)",
message: "Process signal handler invoked",
data: { signal, pid: process.pid, appInitialized: !!app },
timestamp: Date.now(),
}),
}).catch(() => {});
// #endregion
if (!app) {
logger.warn("Nest application not initialized. Exiting immediately.");
@ -19,6 +34,21 @@ for (const signal of signals) {
}
try {
// #region agent log (hypothesis S1)
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
sessionId: "debug-session",
runId: "run1",
hypothesisId: "S1",
location: "apps/bff/src/main.ts:signalHandler(beforeClose)",
message: "Calling app.close()",
data: { signal, pid: process.pid },
timestamp: Date.now(),
}),
}).catch(() => {});
// #endregion
await app.close();
logger.log("Nest application closed gracefully.");
} catch (error) {
@ -28,6 +58,21 @@ for (const signal of signals) {
resolvedError.stack
);
} finally {
// #region agent log (hypothesis S1)
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
sessionId: "debug-session",
runId: "run1",
hypothesisId: "S1",
location: "apps/bff/src/main.ts:signalHandler(exit)",
message: "Exiting process after shutdown handler",
data: { signal, pid: process.pid },
timestamp: Date.now(),
}),
}).catch(() => {});
// #endregion
process.exit(0);
}
})();

View File

@ -35,8 +35,12 @@ POSTGRES_PASSWORD=<GENERATE_WITH_openssl_rand_base64_24>
JWT_SECRET=<GENERATE_WITH_openssl_rand_base64_32>
JWT_SECRET_PREVIOUS=
JWT_EXPIRES_IN=7d
JWT_ISSUER=customer-portal
JWT_AUDIENCE=portal
# JWT claim validation (required; must be non-empty strings)
# - JWT_ISSUER: who issues tokens (this backend). Use your production origin.
# - JWT_AUDIENCE: who the token is intended for (your portal/app). Often same as issuer.
# Keep these stable per environment to prevent prod/dev token mix-ups.
JWT_ISSUER=https://asolutions.jp
JWT_AUDIENCE=https://asolutions.jp
BCRYPT_ROUNDS=12
CSRF_SECRET_KEY=<GENERATE_WITH_openssl_rand_base64_32>
@ -64,9 +68,29 @@ SF_CLIENT_ID=<YOUR_SF_CLIENT_ID>
SF_USERNAME=<YOUR_SF_USERNAME>
SF_EVENTS_ENABLED=true
# Salesforce Private Key (Base64 encoded)
# To encode: base64 -w 0 < sf-private.key
SF_PRIVATE_KEY_BASE64=<BASE64_ENCODED_SF_PRIVATE_KEY>
# Salesforce Private Key (recommended handling)
# -----------------------------------------------------------------------------
# IMPORTANT:
# - Do NOT paste raw PEM in Portainer env.
# - Prefer mounting the key file into the container and setting SF_PRIVATE_KEY_PATH.
# - If you must use env, use SF_PRIVATE_KEY_BASE64 (single-line base64) and the container
# entrypoint will write it to SF_PRIVATE_KEY_PATH.
#
# Option A (preferred): mount a file (no env secret)
# - Mount host file -> /app/secrets/sf-private.key (read-only)
# - Set:
# SF_PRIVATE_KEY_PATH=/app/secrets/sf-private.key
# - Leave SF_PRIVATE_KEY_BASE64 empty/unset
#
# Option B: env var (least preferred)
# 1) Ensure you have the *private key* PEM (NOT a certificate):
# -----BEGIN PRIVATE KEY----- (PKCS8) OR -----BEGIN RSA PRIVATE KEY----- (PKCS1)
# 2) Base64 encode into ONE line (Linux):
# base64 -w0 sf-private.key
# 3) Paste that output into SF_PRIVATE_KEY_BASE64 (no quotes, no newlines)
#
# NOTE: Never commit real key material into git. Keep only placeholders here.
SF_PRIVATE_KEY_BASE64=<BASE64_ENCODED_SALESFORCE_PRIVATE_KEY>
# -----------------------------------------------------------------------------

View File

@ -1 +1 @@
1d758ef9ad60c480fcf34d5c1e6bd8b14215049ea524b61e5de828e74ba3170b /home/barsa/projects/customer_portal/customer-portal/portal-backend.latest.tar.gz
dee6d178f1599a05911c9b6fb1246105112b7e81bfbac4e7df87ed91ca8a46c9 /home/barsa/projects/customer_portal/customer-portal/portal-backend.latest.tar.gz

View File

@ -1 +1 @@
129107760c197bce5493d6a33837cbc812dd8ba1516ee6c37239431ce973a58c /home/barsa/projects/customer_portal/customer-portal/portal-frontend.latest.tar.gz
2595c445c8ed1dd73006d5f4e35adea931de3c101260e1e0e1126790a761812c /home/barsa/projects/customer_portal/customer-portal/portal-frontend.latest.tar.gz

View File

@ -307,6 +307,10 @@ cleanup_dev() {
pkill -f "next-server" 2>/dev/null && log " Stopped Next.js server process" || true
pkill -f "pnpm.*--parallel.*dev" 2>/dev/null && log " Stopped parallel dev processes" || true
pkill -f "prisma studio" 2>/dev/null && log " Stopped Prisma Studio" || true
pkill -f "apps/bff/scripts/dev-watch.sh" 2>/dev/null && log " Stopped BFF dev-watch script" || true
pkill -f "tsc -p tsconfig.build.json --watch" 2>/dev/null && log " Stopped BFF TypeScript watcher" || true
pkill -f "tsc-alias.*tsconfig.build.json.*-w" 2>/dev/null && log " Stopped BFF tsc-alias watcher" || true
pkill -f "node --watch dist/main.js" 2>/dev/null && log " Stopped BFF Node watcher" || true
sleep 1
log "✅ Development cleanup completed"