Enhance GitHub Workflows and Refactor BFF Code
- Added support for push and workflow_dispatch events in pr-checks.yml to improve CI flexibility. - Implemented concurrency control in workflows to manage job execution more effectively. - Updated pnpm setup to include caching and specified version for consistency. - Removed redundant logging code from BFF application to streamline signal handling and improve readability. - Introduced CSRF_SECRET_KEY validation in environment configuration for production hardening. - Refactored logo component to use a fallback mechanism for image loading, enhancing user experience. - Added linting scripts to package.json for improved code quality checks.
This commit is contained in:
parent
9b2ce83229
commit
84a11f7efc
45
.github/workflows/pr-checks.yml
vendored
45
.github/workflows/pr-checks.yml
vendored
@ -2,9 +2,18 @@ name: Pull Request Checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: pr-checks-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
quality-checks:
|
||||
@ -15,42 +24,34 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.25.0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Check formatting
|
||||
run: pnpm format:check
|
||||
|
||||
- name: Run linter
|
||||
run: pnpm lint
|
||||
|
||||
- name: Run type check
|
||||
run: pnpm type-check
|
||||
|
||||
- name: Run security audit
|
||||
run: pnpm security:check
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm test
|
||||
|
||||
- name: Check formatting
|
||||
run: pnpm format:check
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Verify domain dist is up-to-date
|
||||
run: pnpm domain:check-dist
|
||||
|
||||
12
.github/workflows/security.yml
vendored
12
.github/workflows/security.yml
vendored
@ -97,11 +97,6 @@ jobs:
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ["javascript", "typescript"]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@ -109,16 +104,13 @@ jobs:
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
languages: javascript-typescript
|
||||
queries: security-and-quality
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
category: "/language:javascript-typescript"
|
||||
|
||||
outdated-dependencies:
|
||||
name: Check Outdated Dependencies
|
||||
|
||||
@ -120,21 +120,6 @@ 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.
|
||||
|
||||
|
||||
@ -243,5 +243,12 @@ export function validate(config: Record<string, unknown>): Record<string, unknow
|
||||
];
|
||||
throw new Error(`Invalid environment configuration: ${messages.join("; ")}`);
|
||||
}
|
||||
// Production hardening: CSRF must be backed by a stable secret key.
|
||||
// (If not set, CsrfService generates an ephemeral key which is not suitable for production.)
|
||||
if (result.data.NODE_ENV === "production" && !result.data.CSRF_SECRET_KEY) {
|
||||
throw new Error(
|
||||
"Invalid environment configuration: CSRF_SECRET_KEY must be set when NODE_ENV=production"
|
||||
);
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
import { Injectable, Inject } from "@nestjs/common";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import type { Redis } from "ioredis";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
const LOCK_PREFIX = "lock:";
|
||||
const DEFAULT_TTL_MS = 30_000; // 30 seconds
|
||||
@ -176,7 +177,7 @@ export class DistributedLockService {
|
||||
* Generate a unique token for lock ownership
|
||||
*/
|
||||
private generateToken(): string {
|
||||
return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
||||
return randomUUID();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -44,69 +44,16 @@ export class PrismaService extends PrismaClient implements OnModuleInit, OnModul
|
||||
|
||||
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");
|
||||
@ -117,25 +64,5 @@ export class PrismaService extends PrismaClient implements OnModuleInit, OnModul
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,21 +11,6 @@ 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.");
|
||||
@ -34,21 +19,6 @@ 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) {
|
||||
@ -58,21 +28,6 @@ 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);
|
||||
}
|
||||
})();
|
||||
|
||||
@ -15,6 +15,7 @@ import { ZodValidationPipe } from "nestjs-zod";
|
||||
import { Public } from "@bff/modules/auth/decorators/public.decorator.js";
|
||||
import { RateLimit, RateLimitGuard } from "@bff/core/rate-limiting/index.js";
|
||||
import { z } from "zod";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
supportCaseFilterSchema,
|
||||
createCaseRequestSchema,
|
||||
@ -37,6 +38,11 @@ const publicContactSchema = z.object({
|
||||
|
||||
type PublicContactRequest = z.infer<typeof publicContactSchema>;
|
||||
|
||||
const hashEmailForLogs = (email: string): string => {
|
||||
const normalized = email.trim().toLowerCase();
|
||||
return createHash("sha256").update(normalized).digest("hex").slice(0, 12);
|
||||
};
|
||||
|
||||
@Controller("support")
|
||||
export class SupportController {
|
||||
constructor(
|
||||
@ -84,7 +90,7 @@ export class SupportController {
|
||||
@Body(new ZodValidationPipe(publicContactSchema))
|
||||
body: PublicContactRequest
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
this.logger.log("Public contact form submission", { email: body.email });
|
||||
this.logger.log("Public contact form submission", { emailHash: hashEmailForLogs(body.email) });
|
||||
|
||||
try {
|
||||
await this.supportService.createPublicContactRequest(body);
|
||||
@ -96,7 +102,7 @@ export class SupportController {
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to process public contact form", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
email: body.email,
|
||||
emailHash: hashEmailForLogs(body.email),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
2
apps/portal/next-env.d.ts
vendored
2
apps/portal/next-env.d.ts
vendored
@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@ -9,6 +9,33 @@ interface LogoProps {
|
||||
}
|
||||
|
||||
export function Logo({ className = "", size = 32 }: LogoProps) {
|
||||
const [fallback, setFallback] = React.useState(false);
|
||||
|
||||
if (fallback) {
|
||||
return (
|
||||
<div className={className} style={{ width: size, height: size }}>
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-full h-full"
|
||||
role="img"
|
||||
aria-label="Assist Solutions Logo"
|
||||
>
|
||||
{/* Top section - Light blue curved arrows */}
|
||||
<path d="M8 8 C12 4, 20 4, 24 8 L20 12 C18 10, 14 10, 12 12 Z" fill="#60A5FA" />
|
||||
<path d="M24 8 C28 12, 28 20, 24 24 L20 20 C22 18, 22 14, 20 12 Z" fill="#60A5FA" />
|
||||
|
||||
{/* Bottom section - Dark blue curved arrows */}
|
||||
<path d="M8 24 C12 28, 20 28, 24 24 L20 20 C18 22, 14 22, 12 20 Z" fill="#1E40AF" />
|
||||
<path d="M8 24 C4 20, 4 12, 8 8 L12 12 C10 14, 10 18, 12 20 Z" fill="#1E40AF" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className} style={{ width: size, height: size }}>
|
||||
<Image
|
||||
@ -17,43 +44,7 @@ export function Logo({ className = "", size = 32 }: LogoProps) {
|
||||
width={size}
|
||||
height={size}
|
||||
className="w-full h-full object-contain"
|
||||
onError={e => {
|
||||
// Fallback to SVG if image fails to load
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.style.display = "none";
|
||||
const parent = target.parentElement;
|
||||
if (parent) {
|
||||
parent.innerHTML = `
|
||||
<svg
|
||||
width="${size}"
|
||||
height="${size}"
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<!-- Top section - Light blue curved arrows -->
|
||||
<path
|
||||
d="M8 8 C12 4, 20 4, 24 8 L20 12 C18 10, 14 10, 12 12 Z"
|
||||
fill="#60A5FA"
|
||||
/>
|
||||
<path
|
||||
d="M24 8 C28 12, 28 20, 24 24 L20 20 C22 18, 22 14, 20 12 Z"
|
||||
fill="#60A5FA"
|
||||
/>
|
||||
|
||||
<!-- Bottom section - Dark blue curved arrows -->
|
||||
<path
|
||||
d="M8 24 C12 28, 20 28, 24 24 L20 20 C18 22, 14 22, 12 20 Z"
|
||||
fill="#1E40AF"
|
||||
/>
|
||||
<path
|
||||
d="M8 24 C4 20, 4 12, 8 8 L12 12 C10 14, 10 18, 12 20 Z"
|
||||
fill="#1E40AF"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
}}
|
||||
onError={() => setFallback(true)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -169,6 +169,9 @@
|
||||
"build": "tsc",
|
||||
"dev": "tsc -w --preserveWatchOutput",
|
||||
"clean": "rm -rf dist",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"test": "echo 'No tests yet'",
|
||||
"type-check": "NODE_OPTIONS=\"--max-old-space-size=2048 --max-semi-space-size=128\" tsc --project tsconfig.json --noEmit",
|
||||
"typecheck": "pnpm run type-check"
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user