Enhance environment configuration and update package scripts
- Added .env.development to .gitignore for better environment management. - Introduced new dev script in package.json for streamlined application development. - Updated Prisma migration commands in docker-entrypoint.sh for improved schema handling. - Enhanced logging configuration in logging.module.ts to support pretty logs based on environment. - Refactored app.config.ts to prioritize environment file loading for better configuration management. - Removed outdated test files and configurations to clean up the project structure.
This commit is contained in:
parent
0e32d4004a
commit
6e3626eff0
1
.gitignore
vendored
1
.gitignore
vendored
@ -6,6 +6,7 @@ node_modules/
|
|||||||
# Environment files
|
# Environment files
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
|
.env.development
|
||||||
.env.production
|
.env.production
|
||||||
.env.test
|
.env.test
|
||||||
|
|
||||||
|
|||||||
@ -88,8 +88,14 @@ fi
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
if [ "$RUN_MIGRATIONS" = "true" ] && [ -n "$DATABASE_URL" ]; then
|
if [ "$RUN_MIGRATIONS" = "true" ] && [ -n "$DATABASE_URL" ]; then
|
||||||
echo "🗄️ Running database migrations..."
|
echo "🗄️ Running database migrations..."
|
||||||
|
|
||||||
if pnpm dlx prisma@"${PRISMA_VERSION}" migrate deploy --schema=/app/prisma/schema.prisma; then
|
# Set Prisma schema path for migration
|
||||||
|
export PRISMA_SCHEMA_PATH="/app/prisma/schema.prisma"
|
||||||
|
|
||||||
|
# Change to app directory where schema is located
|
||||||
|
cd /app
|
||||||
|
|
||||||
|
if pnpm dlx prisma@"${PRISMA_VERSION}" migrate deploy; then
|
||||||
echo "✅ Migrations complete"
|
echo "✅ Migrations complete"
|
||||||
else
|
else
|
||||||
echo "⚠️ Migration failed - check database connectivity"
|
echo "⚠️ Migration failed - check database connectivity"
|
||||||
|
|||||||
@ -2,12 +2,23 @@ import { resolve } from "node:path";
|
|||||||
import type { ConfigModuleOptions } from "@nestjs/config";
|
import type { ConfigModuleOptions } from "@nestjs/config";
|
||||||
import { validate } from "./env.validation.js";
|
import { validate } from "./env.validation.js";
|
||||||
|
|
||||||
|
const nodeEnv = process.env.NODE_ENV || "development";
|
||||||
|
|
||||||
|
// Load env files in order of priority (first found wins)
|
||||||
|
// 1. .env.{NODE_ENV}.local - local overrides for specific environment
|
||||||
|
// 2. .env.local - local overrides (not committed)
|
||||||
|
// 3. .env.{NODE_ENV} - environment-specific
|
||||||
|
// 4. .env - default
|
||||||
|
// Also check monorepo root as fallback
|
||||||
export const appConfig: ConfigModuleOptions = {
|
export const appConfig: ConfigModuleOptions = {
|
||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
expandVariables: true,
|
expandVariables: true,
|
||||||
validate,
|
validate,
|
||||||
envFilePath: [
|
envFilePath: [
|
||||||
|
resolve(process.cwd(), `.env.${nodeEnv}.local`),
|
||||||
|
resolve(process.cwd(), ".env.local"),
|
||||||
|
resolve(process.cwd(), `.env.${nodeEnv}`),
|
||||||
resolve(process.cwd(), ".env"),
|
resolve(process.cwd(), ".env"),
|
||||||
resolve(process.cwd(), "../../.env"), // Monorepo root
|
resolve(process.cwd(), "../../.env"), // Monorepo root fallback
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { Global, Module } from "@nestjs/common";
|
import { Global, Module } from "@nestjs/common";
|
||||||
import { LoggerModule } from "nestjs-pino";
|
import { LoggerModule } from "nestjs-pino";
|
||||||
|
|
||||||
|
const prettyLogsEnabled = process.env.PRETTY_LOGS === "true" || process.env.NODE_ENV !== "production";
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -31,18 +33,18 @@ import { LoggerModule } from "nestjs-pino";
|
|||||||
statusCode: res.statusCode,
|
statusCode: res.statusCode,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
transport:
|
transport: prettyLogsEnabled
|
||||||
process.env.NODE_ENV === "development"
|
? {
|
||||||
? {
|
target: "pino-pretty",
|
||||||
target: "pino-pretty",
|
options: {
|
||||||
options: {
|
colorize: true,
|
||||||
colorize: true,
|
translateTime: false,
|
||||||
translateTime: "yyyy-mm-dd HH:MM:ss",
|
singleLine: true,
|
||||||
ignore: "pid,hostname,req,res",
|
// keep level for coloring but drop other noisy metadata
|
||||||
messageFormat: "{msg}",
|
ignore: "pid,req,res,context,name,time",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
redact: {
|
redact: {
|
||||||
paths: [
|
paths: [
|
||||||
"req.headers.authorization",
|
"req.headers.authorization",
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { PassportStrategy } from "@nestjs/passport";
|
|||||||
import { Strategy } from "passport-local";
|
import { Strategy } from "passport-local";
|
||||||
import type { Request } from "express";
|
import type { Request } from "express";
|
||||||
import { AuthFacade } from "@bff/modules/auth/application/auth.facade.js";
|
import { AuthFacade } from "@bff/modules/auth/application/auth.facade.js";
|
||||||
|
import { ErrorCode, ErrorMessages } from "@customer-portal/domain/common";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LocalStrategy extends PassportStrategy(Strategy) {
|
export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||||
@ -17,7 +18,7 @@ export class LocalStrategy extends PassportStrategy(Strategy) {
|
|||||||
): Promise<{ id: string; email: string; role: string }> {
|
): Promise<{ id: string; email: string; role: string }> {
|
||||||
const user = await this.authFacade.validateUser(email, password, req);
|
const user = await this.authFacade.validateUser(email, password, req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new UnauthorizedException("Invalid credentials");
|
throw new UnauthorizedException(ErrorMessages[ErrorCode.INVALID_CREDENTIALS]);
|
||||||
}
|
}
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,50 +0,0 @@
|
|||||||
/// <reference types="jest" />
|
|
||||||
import request from "supertest";
|
|
||||||
import type { INestApplication } from "@nestjs/common";
|
|
||||||
import { Test } from "@nestjs/testing";
|
|
||||||
import type { Server } from "node:http";
|
|
||||||
|
|
||||||
import { AppModule } from "../src/app.module.js";
|
|
||||||
import {
|
|
||||||
parseInternetCatalog,
|
|
||||||
internetCatalogResponseSchema,
|
|
||||||
} from "@customer-portal/domain/catalog";
|
|
||||||
import { apiSuccessResponseSchema } from "@customer-portal/domain/common/schema";
|
|
||||||
|
|
||||||
const internetCatalogApiResponseSchema = apiSuccessResponseSchema(internetCatalogResponseSchema);
|
|
||||||
|
|
||||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
||||||
typeof value === "object" && value !== null;
|
|
||||||
|
|
||||||
const isHttpServer = (value: unknown): value is Server =>
|
|
||||||
isRecord(value) && typeof value.listen === "function" && typeof value.close === "function";
|
|
||||||
|
|
||||||
describe("Catalog contract", () => {
|
|
||||||
let app: INestApplication;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const moduleRef = await Test.createTestingModule({
|
|
||||||
imports: [AppModule],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
app = moduleRef.createNestApplication();
|
|
||||||
await app.init();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return internet catalog matching domain schema", async () => {
|
|
||||||
const serverCandidate: unknown = app.getHttpServer();
|
|
||||||
if (!isHttpServer(serverCandidate)) {
|
|
||||||
throw new Error("Expected Nest application to expose an HTTP server");
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await request(serverCandidate).get("/api/catalog/internet/plans");
|
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
|
||||||
const payload = internetCatalogApiResponseSchema.parse(response.body);
|
|
||||||
expect(() => parseInternetCatalog(payload.data)).not.toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"moduleFileExtensions": ["js", "json", "ts"],
|
|
||||||
"rootDir": ".",
|
|
||||||
"testEnvironment": "node",
|
|
||||||
"testRegex": ".spec.ts$",
|
|
||||||
"transform": {
|
|
||||||
"^.+\\.(t|j)s$": [
|
|
||||||
"ts-jest",
|
|
||||||
{
|
|
||||||
"tsconfig": {
|
|
||||||
"types": ["jest", "node"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"moduleNameMapper": {
|
|
||||||
"^@bff/(.*)$": "<rootDir>/../src/$1"
|
|
||||||
},
|
|
||||||
"globals": {
|
|
||||||
"ts-jest": {
|
|
||||||
"tsconfig": {
|
|
||||||
"types": ["jest", "node"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"types": ["jest", "node", "supertest"]
|
|
||||||
},
|
|
||||||
"include": ["./**/*.ts", "../src/**/*"]
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,15 +1,10 @@
|
|||||||
/* eslint-env node */
|
/* eslint-env node */
|
||||||
import bundleAnalyzer from "@next/bundle-analyzer";
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const workspaceRoot = path.resolve(__dirname, "..", "..");
|
const workspaceRoot = path.resolve(__dirname, "..", "..");
|
||||||
|
|
||||||
const withBundleAnalyzer = bundleAnalyzer({
|
|
||||||
enabled: process.env.ANALYZE === "true",
|
|
||||||
});
|
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
output: process.env.NODE_ENV === "production" ? "standalone" : undefined,
|
output: process.env.NODE_ENV === "production" ? "standalone" : undefined,
|
||||||
@ -38,71 +33,6 @@ const nextConfig = {
|
|||||||
remotePatterns: [{ protocol: "https", hostname: "**" }],
|
remotePatterns: [{ protocol: "https", hostname: "**" }],
|
||||||
},
|
},
|
||||||
|
|
||||||
async rewrites() {
|
|
||||||
if (process.env.NODE_ENV !== "production") {
|
|
||||||
return [{ source: "/api/:path*", destination: "http://localhost:4000/api/:path*" }];
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
|
|
||||||
async headers() {
|
|
||||||
const isDev = process.env.NODE_ENV === "development";
|
|
||||||
if (isDev) {
|
|
||||||
const devConnectSources = ["'self'", "https:", "http://localhost:*", "ws://localhost:*"];
|
|
||||||
const devScriptSrc = ["'self'", "'unsafe-inline'", "'unsafe-eval'", "blob:"].join(" ");
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
source: "/(.*)",
|
|
||||||
headers: [
|
|
||||||
{ key: "X-Frame-Options", value: "DENY" },
|
|
||||||
{ key: "X-Content-Type-Options", value: "nosniff" },
|
|
||||||
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
|
|
||||||
{ key: "X-XSS-Protection", value: "1; mode=block" },
|
|
||||||
{
|
|
||||||
key: "Content-Security-Policy",
|
|
||||||
value: [
|
|
||||||
"default-src 'self'",
|
|
||||||
`script-src ${devScriptSrc}`,
|
|
||||||
"style-src 'self' 'unsafe-inline'",
|
|
||||||
"img-src 'self' data: https:",
|
|
||||||
"font-src 'self' data:",
|
|
||||||
`connect-src ${devConnectSources.join(" ")}`,
|
|
||||||
"frame-ancestors 'none'",
|
|
||||||
].join("; "),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const connectSources = ["'self'", "https:"];
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
source: "/(.*)",
|
|
||||||
headers: [
|
|
||||||
{ key: "X-Frame-Options", value: "DENY" },
|
|
||||||
{ key: "X-Content-Type-Options", value: "nosniff" },
|
|
||||||
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
|
|
||||||
{ key: "X-XSS-Protection", value: "1; mode=block" },
|
|
||||||
{
|
|
||||||
key: "Content-Security-Policy",
|
|
||||||
value: [
|
|
||||||
"default-src 'self'",
|
|
||||||
"script-src 'self'",
|
|
||||||
"style-src 'self' 'unsafe-inline'",
|
|
||||||
"img-src 'self' data: https:",
|
|
||||||
"font-src 'self' data:",
|
|
||||||
`connect-src ${connectSources.join(" ")}`,
|
|
||||||
"frame-ancestors 'none'",
|
|
||||||
].join("; "),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
compiler: {
|
compiler: {
|
||||||
removeConsole: process.env.NODE_ENV === "production",
|
removeConsole: process.env.NODE_ENV === "production",
|
||||||
},
|
},
|
||||||
@ -114,4 +44,12 @@ const nextConfig = {
|
|||||||
typescript: { ignoreBuildErrors: false },
|
typescript: { ignoreBuildErrors: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withBundleAnalyzer(nextConfig);
|
// Only load bundle analyzer when explicitly requested
|
||||||
|
let exportedConfig = nextConfig;
|
||||||
|
|
||||||
|
if (process.env.ANALYZE === "true") {
|
||||||
|
const bundleAnalyzer = (await import("@next/bundle-analyzer")).default;
|
||||||
|
exportedConfig = bundleAnalyzer({ enabled: true })(nextConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default exportedConfig;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
import { headers } from "next/headers";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { QueryProvider } from "@/lib/providers";
|
import { QueryProvider } from "@/lib/providers";
|
||||||
import { SessionTimeoutWarning } from "@/features/auth/components/SessionTimeoutWarning";
|
import { SessionTimeoutWarning } from "@/features/auth/components/SessionTimeoutWarning";
|
||||||
@ -23,15 +24,19 @@ export const metadata: Metadata = {
|
|||||||
// This is the recommended approach for apps with heavy useSearchParams usage
|
// This is the recommended approach for apps with heavy useSearchParams usage
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
export default function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
|
// Get nonce from proxy.ts for CSP-compliant inline scripts
|
||||||
|
const headersList = await headers();
|
||||||
|
const nonce = headersList.get("x-nonce") || undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
|
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
|
||||||
<QueryProvider>
|
<QueryProvider nonce={nonce}>
|
||||||
{children}
|
{children}
|
||||||
<SessionTimeoutWarning />
|
<SessionTimeoutWarning />
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
|
|||||||
@ -56,8 +56,9 @@ export interface ApiClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve API base URL:
|
* Resolve API base URL:
|
||||||
* - Browser: Use same origin (Next.js rewrites in dev, nginx proxy in prod)
|
* - If NEXT_PUBLIC_API_BASE is set, use it (enables direct BFF calls in dev via CORS)
|
||||||
* - SSR: Use NEXT_PUBLIC_API_BASE env var or fallback to localhost:4000
|
* - Browser fallback: Use same origin (nginx proxy in prod)
|
||||||
|
* - SSR fallback: Use localhost:4000
|
||||||
*/
|
*/
|
||||||
export const resolveBaseUrl = (explicitBase?: string): string => {
|
export const resolveBaseUrl = (explicitBase?: string): string => {
|
||||||
// 1. Explicit base URL provided (for testing/overrides)
|
// 1. Explicit base URL provided (for testing/overrides)
|
||||||
@ -65,20 +66,20 @@ export const resolveBaseUrl = (explicitBase?: string): string => {
|
|||||||
return explicitBase.replace(/\/+$/, "");
|
return explicitBase.replace(/\/+$/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Browser: use same origin
|
// 2. Check NEXT_PUBLIC_API_BASE env var (works in both browser and SSR)
|
||||||
// - Development: Next.js rewrites proxy /api/* to localhost:4000
|
// In development: set to http://localhost:4000 for direct CORS calls
|
||||||
// - Production: nginx proxies /api/* to backend
|
// In production: typically not set, falls through to same-origin
|
||||||
if (typeof window !== "undefined" && window.location?.origin) {
|
|
||||||
return window.location.origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Server-side: check env var for SSR requests
|
|
||||||
const envBase = process.env.NEXT_PUBLIC_API_BASE;
|
const envBase = process.env.NEXT_PUBLIC_API_BASE;
|
||||||
if (envBase?.trim() && envBase.startsWith("http")) {
|
if (envBase?.trim() && envBase.startsWith("http")) {
|
||||||
return envBase.replace(/\/+$/, "");
|
return envBase.replace(/\/+$/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Fallback for server-side in development
|
// 3. Browser fallback: use same origin (production nginx proxy)
|
||||||
|
if (typeof window !== "undefined" && window.location?.origin) {
|
||||||
|
return window.location.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. SSR fallback for development
|
||||||
return "http://localhost:4000";
|
return "http://localhost:4000";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* React Query Provider
|
* React Query Provider
|
||||||
* Simple provider setup for TanStack Query
|
* Simple provider setup for TanStack Query with CSP nonce support
|
||||||
*/
|
*/
|
||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
@ -10,7 +10,12 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { isApiError } from "@/lib/api/runtime/client";
|
import { isApiError } from "@/lib/api/runtime/client";
|
||||||
|
|
||||||
export function QueryProvider({ children }: { children: React.ReactNode }) {
|
interface QueryProviderProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
nonce?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function QueryProvider({ children, nonce }: QueryProviderProps) {
|
||||||
const [queryClient] = useState(
|
const [queryClient] = useState(
|
||||||
() =>
|
() =>
|
||||||
new QueryClient({
|
new QueryClient({
|
||||||
@ -46,7 +51,7 @@ export function QueryProvider({ children }: { children: React.ReactNode }) {
|
|||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
{children}
|
{children}
|
||||||
{process.env.NODE_ENV === "development" && <ReactQueryDevtools />}
|
{process.env.NODE_ENV === "development" && <ReactQueryDevtools nonce={nonce} />}
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
98
apps/portal/src/proxy.ts
Normal file
98
apps/portal/src/proxy.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Next.js 16 Proxy (formerly Middleware)
|
||||||
|
*
|
||||||
|
* Generates a cryptographic nonce for each request to allow inline scripts
|
||||||
|
* while maintaining Content Security Policy protection.
|
||||||
|
*
|
||||||
|
* @see https://nextjs.org/docs/app/guides/content-security-policy
|
||||||
|
* @see https://nextjs.org/docs/messages/middleware-to-proxy
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function proxy(request: NextRequest) {
|
||||||
|
// Generate a random nonce for this request
|
||||||
|
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
|
||||||
|
|
||||||
|
// Determine environment
|
||||||
|
const isDev = process.env.NODE_ENV === "development";
|
||||||
|
|
||||||
|
// Build CSP header value
|
||||||
|
const cspHeader = buildCSP(nonce, isDev);
|
||||||
|
|
||||||
|
// Clone the request headers
|
||||||
|
const requestHeaders = new Headers(request.headers);
|
||||||
|
requestHeaders.set("x-nonce", nonce);
|
||||||
|
|
||||||
|
// Create response with updated headers
|
||||||
|
const response = NextResponse.next({
|
||||||
|
request: {
|
||||||
|
headers: requestHeaders,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set CSP header on response
|
||||||
|
response.headers.set("Content-Security-Policy", cspHeader);
|
||||||
|
|
||||||
|
// Add additional security headers
|
||||||
|
response.headers.set("X-Frame-Options", "DENY");
|
||||||
|
response.headers.set("X-Content-Type-Options", "nosniff");
|
||||||
|
response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
||||||
|
response.headers.set("X-XSS-Protection", "1; mode=block");
|
||||||
|
response.headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCSP(nonce: string, isDev: boolean): string {
|
||||||
|
if (isDev) {
|
||||||
|
// Development: More permissive for HMR and dev tools
|
||||||
|
return [
|
||||||
|
"default-src 'self'",
|
||||||
|
"script-src 'self' 'unsafe-eval' 'unsafe-inline'", // HMR needs eval
|
||||||
|
"style-src 'self' 'unsafe-inline'",
|
||||||
|
"img-src 'self' data: https:",
|
||||||
|
"font-src 'self' data:",
|
||||||
|
"connect-src 'self' https: http://localhost:* ws://localhost:*",
|
||||||
|
"frame-ancestors 'none'",
|
||||||
|
].join("; ");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Production: Strict CSP with nonce
|
||||||
|
// 'strict-dynamic' allows scripts loaded by nonced scripts to execute.
|
||||||
|
// Next 16 applies the nonce to its own inline scripts, so 'unsafe-inline'
|
||||||
|
// is not required in script-src when the nonce is present.
|
||||||
|
return [
|
||||||
|
"default-src 'self'",
|
||||||
|
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
|
||||||
|
"style-src 'self' 'unsafe-inline'", // Next.js requires this for styled-jsx
|
||||||
|
"img-src 'self' data: https:",
|
||||||
|
"font-src 'self' data:",
|
||||||
|
"connect-src 'self' https:",
|
||||||
|
"frame-ancestors 'none'",
|
||||||
|
"base-uri 'self'",
|
||||||
|
"form-action 'self'",
|
||||||
|
"object-src 'none'",
|
||||||
|
"upgrade-insecure-requests",
|
||||||
|
].join("; ");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply proxy to all routes except static assets
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
/*
|
||||||
|
* Match all request paths except:
|
||||||
|
* - _next/static (static files)
|
||||||
|
* - _next/image (image optimization files)
|
||||||
|
* - favicon.ico, sitemap.xml, robots.txt (metadata files)
|
||||||
|
* - api/health (health check endpoint)
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
source: "/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|api/health).*)",
|
||||||
|
missing: [
|
||||||
|
{ type: "header", key: "next-router-prefetch" },
|
||||||
|
{ type: "header", key: "purpose", value: "prefetch" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@ -151,7 +151,7 @@ services:
|
|||||||
cache:
|
cache:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
container_name: portal-cache
|
container_name: portal-cache
|
||||||
command: ["redis-server", "--save", "60", "1", "--loglevel", "warning", "--maxmemory", "128mb", "--maxmemory-policy", "allkeys-lru"]
|
command: ["redis-server", "--save", "60", "1", "--loglevel", "warning", "--maxmemory", "256mb", "--maxmemory-policy", "noeviction"]
|
||||||
volumes:
|
volumes:
|
||||||
- redis_data:/data
|
- redis_data:/data
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
112
docker/Prod - Portainer/stack.env
Normal file
112
docker/Prod - Portainer/stack.env
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Customer Portal - Portainer Environment Variables
|
||||||
|
# =============================================================================
|
||||||
|
# Copy these into Portainer UI when creating/updating the stack
|
||||||
|
# Replace all placeholder values with your actual secrets
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Images & Ports
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
FRONTEND_IMAGE=portal-frontend
|
||||||
|
BACKEND_IMAGE=portal-backend
|
||||||
|
IMAGE_TAG=latest
|
||||||
|
FRONTEND_PORT=3000
|
||||||
|
BACKEND_PORT=4000
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Application
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
APP_NAME=customer-portal-bff
|
||||||
|
APP_BASE_URL=https://asolutions.jp
|
||||||
|
CORS_ORIGIN=https://asolutions.jp
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Database (PostgreSQL)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
POSTGRES_DB=portal_prod
|
||||||
|
POSTGRES_USER=portal
|
||||||
|
POSTGRES_PASSWORD=wf8vVNxaGXqJbE4AMwBg8olJtUptLNcH
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Security & Auth
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Generate with: openssl rand -base64 32
|
||||||
|
JWT_SECRET=N+HXXwJBM93omVC8mCbrrWKNR/deCmSe5q4TTwMFur8=
|
||||||
|
JWT_EXPIRES_IN=7d
|
||||||
|
BCRYPT_ROUNDS=12
|
||||||
|
CSRF_SECRET_KEY=/W6PPJ0DeduasE4GqeLIxfdSNg9TDNwyuVNz0IWz0Bs=
|
||||||
|
|
||||||
|
# Auth Settings
|
||||||
|
AUTH_ALLOW_REDIS_TOKEN_FAILOPEN=false
|
||||||
|
AUTH_REQUIRE_REDIS_FOR_TOKENS=false
|
||||||
|
AUTH_MAINTENANCE_MODE=false
|
||||||
|
|
||||||
|
# Rate Limiting
|
||||||
|
RATE_LIMIT_TTL=60
|
||||||
|
RATE_LIMIT_LIMIT=100
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# WHMCS Integration
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
WHMCS_BASE_URL=https://dev-wh.asolutions.co.jp
|
||||||
|
WHMCS_API_IDENTIFIER=WZckHGfzAQEum3v5SAcSfzgvVkPJEF2M
|
||||||
|
WHMCS_API_SECRET=YlqKyynJ6I1088DV6jufFj6cJiW0N0y4
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Salesforce Integration
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
SF_LOGIN_URL=https://asolutions.my.salesforce.com
|
||||||
|
SF_CLIENT_ID=3MVG9n_HvETGhr3Af33utEHAR_KbKEQh_.KRzVBBA6u3tSIMraIlY9pqNqKJgUILstAPS4JASzExj3OpCRbLz
|
||||||
|
SF_USERNAME=portal.integration@asolutions.co.jp
|
||||||
|
SF_EVENTS_ENABLED=true
|
||||||
|
|
||||||
|
# Salesforce Private Key (Base64 encoded)
|
||||||
|
# To encode: base64 -w 0 < sf-private.key
|
||||||
|
SF_PRIVATE_KEY_BASE64=MIIDvzCCAqegAwIBAgIUWD/Nx/Tem+FbPzsowuIYP6eioWwwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwbzELMAkGA1UEBhMCSlAxDjAMBgNVBAgMBVRva3lvMQ4wDAYDVQQHDAVUb2t5
|
||||||
|
bzEZMBcGA1UECgwQQXNzaXN0IFNvbHV0aW9uczELMAkGA1UECwwCSVQxGDAWBgNV
|
||||||
|
BAMMD0N1c3RvbWVyIFBvcnRhbDAeFw0yNTA4MTUwNTAxMDNaFw0yNjA4MTUwNTAx
|
||||||
|
MDNaMG8xCzAJBgNVBAYTAkpQMQ4wDAYDVQQIDAVUb2t5bzEOMAwGA1UEBwwFVG9r
|
||||||
|
eW8xGTAXBgNVBAoMEEFzc2lzdCBTb2x1dGlvbnMxCzAJBgNVBAsMAklUMRgwFgYD
|
||||||
|
VQQDDA9DdXN0b21lciBQb3J0YWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
|
||||||
|
AoIBAQCtBK38ZXzz3LA8dKqHbwz+ucWLPqz8sjoXY4V0W+fMhifn5Oi4u3k2mlqg
|
||||||
|
J2bGFPn8DH5cRafM0+CCwO9TV6PbYxolsO7NKjFxSERJPqj5tZ0bpZljul4J0wiJ
|
||||||
|
ZyT8NWK+WV9aga+zrHOThgvUSPJAb3I1FbRSSha9k2UsaZ5Ubo43EFMRAoAU1DqV
|
||||||
|
tRvG9UW+Ditrlr/8hhDT8WREwzwdGc4GVtM2AsiNNbKM5kcjhu8sgKZ2j+ZCM+0l
|
||||||
|
yk0JUcciSYUWgY79XEvCVAAiUGiL3qtxurEe02f9/ISWawbJne1SQIhaXZycsehm
|
||||||
|
VHN4ySW5uj2waOu4IzDXOqW75e+1AgMBAAGjUzBRMB0GA1UdDgQWBBTOBfxQ/VS+
|
||||||
|
MtjqnY2ielB6n4qHVDAfBgNVHSMEGDAWgBTOBfxQ/VS+MtjqnY2ielB6n4qHVDAP
|
||||||
|
BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAcpPfUF4kPuiB91Igq
|
||||||
|
gwpRZmUJjq0+fIv2Llyn3Q+srk/IOcnkjJgaksGTXPwsMErtcD60z1tIzp39aDl2
|
||||||
|
wUmTxssXg41X7jBpy9Q7wXduvZwpSHbwrz8GShGKnwBCITTVHg0PRCIMn9DvJg3H
|
||||||
|
So/A2TQyazhWSh1yz4P6hAn7UKAG4rzMPjyzq+RYFOSKKCtdJ5ImqIkrNnHhQj/4
|
||||||
|
py/E6K6/ZkroKWr6z1gFU2E8xQQ+u1YNAEjL8U+vd0ftLmYTHCciaZdy4emo5BRg
|
||||||
|
V8oZp81Kw1Da+nVuBCMtZ4ICYLBI8LVtfkzdFDr3MShRWcPe+k6/lbDfT98qy01O
|
||||||
|
26sJ
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Freebit SIM API
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
FREEBIT_BASE_URL=https://i1-q.mvno.net/emptool/api
|
||||||
|
FREEBIT_OEM_ID=PASI
|
||||||
|
FREEBIT_OEM_KEY=6Au3o7wrQNR07JxFHPmf0YfFqN9a31t5
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Email (SendGrid)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
EMAIL_ENABLED=true
|
||||||
|
EMAIL_FROM=no-reply@asolutions.jp
|
||||||
|
EMAIL_FROM_NAME=Assist Solutions
|
||||||
|
SENDGRID_API_KEY=<YOUR-SENDGRID-API-KEY>
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Salesforce Portal Config
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
PORTAL_PRICEBOOK_ID=01sTL000008eLVlYAM
|
||||||
|
PORTAL_PRICEBOOK_NAME=Portal
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Logging
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
LOG_LEVEL=info
|
||||||
@ -1,188 +0,0 @@
|
|||||||
# =============================================================================
|
|
||||||
# Customer Portal - Production Docker Compose
|
|
||||||
# =============================================================================
|
|
||||||
# Full stack for standalone production deployments (non-Portainer)
|
|
||||||
# For Portainer/Plesk, use docker/portainer/docker-compose.yml instead
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
services:
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Frontend (Next.js)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
frontend:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: apps/portal/Dockerfile
|
|
||||||
args:
|
|
||||||
- NODE_VERSION=22
|
|
||||||
- PNPM_VERSION=${PNPM_VERSION:-10.25.0}
|
|
||||||
- NEXT_PUBLIC_API_BASE=${NEXT_PUBLIC_API_BASE:-/api}
|
|
||||||
- NEXT_PUBLIC_APP_NAME=${NEXT_PUBLIC_APP_NAME:-Customer Portal}
|
|
||||||
- NEXT_PUBLIC_APP_VERSION=${NEXT_PUBLIC_APP_VERSION:-1.0.0}
|
|
||||||
image: portal-frontend:${IMAGE_TAG:-latest}
|
|
||||||
container_name: portal-frontend
|
|
||||||
ports:
|
|
||||||
- "${FRONTEND_PORT:-3000}:3000"
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=production
|
|
||||||
- PORT=3000
|
|
||||||
- HOSTNAME=0.0.0.0
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
backend:
|
|
||||||
condition: service_healthy
|
|
||||||
networks:
|
|
||||||
- portal-network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "node", "-e", "fetch('http://localhost:3000/api/health').then(r=>r.ok||process.exit(1)).catch(()=>process.exit(1))"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
start_period: 40s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Backend (NestJS BFF)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
backend:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: apps/bff/Dockerfile
|
|
||||||
args:
|
|
||||||
- NODE_VERSION=22
|
|
||||||
- PNPM_VERSION=${PNPM_VERSION:-10.25.0}
|
|
||||||
- PRISMA_VERSION=7.1.0
|
|
||||||
image: portal-backend:${IMAGE_TAG:-latest}
|
|
||||||
container_name: portal-backend
|
|
||||||
ports:
|
|
||||||
- "${BACKEND_PORT:-4000}:4000"
|
|
||||||
environment:
|
|
||||||
# Core
|
|
||||||
- NODE_ENV=production
|
|
||||||
- APP_NAME=${APP_NAME:-customer-portal-bff}
|
|
||||||
- APP_BASE_URL=${APP_BASE_URL}
|
|
||||||
- BFF_PORT=4000
|
|
||||||
- PORT=4000
|
|
||||||
|
|
||||||
# Database
|
|
||||||
- DATABASE_URL=postgresql://${POSTGRES_USER:-portal}:${POSTGRES_PASSWORD}@database:5432/${POSTGRES_DB:-portal_prod}?schema=public
|
|
||||||
|
|
||||||
# Redis
|
|
||||||
- REDIS_URL=redis://cache:6379/0
|
|
||||||
|
|
||||||
# Security
|
|
||||||
- JWT_SECRET=${JWT_SECRET}
|
|
||||||
- JWT_EXPIRES_IN=${JWT_EXPIRES_IN:-7d}
|
|
||||||
- BCRYPT_ROUNDS=${BCRYPT_ROUNDS:-12}
|
|
||||||
- CORS_ORIGIN=${CORS_ORIGIN}
|
|
||||||
- TRUST_PROXY=true
|
|
||||||
- CSRF_SECRET_KEY=${CSRF_SECRET_KEY}
|
|
||||||
|
|
||||||
# Auth
|
|
||||||
- AUTH_ALLOW_REDIS_TOKEN_FAILOPEN=${AUTH_ALLOW_REDIS_TOKEN_FAILOPEN:-false}
|
|
||||||
- AUTH_REQUIRE_REDIS_FOR_TOKENS=${AUTH_REQUIRE_REDIS_FOR_TOKENS:-false}
|
|
||||||
- AUTH_MAINTENANCE_MODE=${AUTH_MAINTENANCE_MODE:-false}
|
|
||||||
|
|
||||||
# Rate Limiting
|
|
||||||
- RATE_LIMIT_TTL=${RATE_LIMIT_TTL:-60}
|
|
||||||
- RATE_LIMIT_LIMIT=${RATE_LIMIT_LIMIT:-100}
|
|
||||||
- EXPOSE_VALIDATION_ERRORS=false
|
|
||||||
|
|
||||||
# WHMCS
|
|
||||||
- WHMCS_BASE_URL=${WHMCS_BASE_URL}
|
|
||||||
- WHMCS_API_IDENTIFIER=${WHMCS_API_IDENTIFIER}
|
|
||||||
- WHMCS_API_SECRET=${WHMCS_API_SECRET}
|
|
||||||
|
|
||||||
# Salesforce
|
|
||||||
- SF_LOGIN_URL=${SF_LOGIN_URL}
|
|
||||||
- SF_CLIENT_ID=${SF_CLIENT_ID}
|
|
||||||
- SF_USERNAME=${SF_USERNAME}
|
|
||||||
- SF_EVENTS_ENABLED=${SF_EVENTS_ENABLED:-true}
|
|
||||||
- SF_PRIVATE_KEY_BASE64=${SF_PRIVATE_KEY_BASE64}
|
|
||||||
- SF_PRIVATE_KEY_PATH=/app/secrets/sf-private.key
|
|
||||||
|
|
||||||
# Freebit
|
|
||||||
- FREEBIT_BASE_URL=${FREEBIT_BASE_URL:-https://i1.mvno.net/emptool/api}
|
|
||||||
- FREEBIT_OEM_ID=${FREEBIT_OEM_ID:-PASI}
|
|
||||||
- FREEBIT_OEM_KEY=${FREEBIT_OEM_KEY}
|
|
||||||
|
|
||||||
# Email
|
|
||||||
- EMAIL_ENABLED=${EMAIL_ENABLED:-true}
|
|
||||||
- EMAIL_FROM=${EMAIL_FROM:-no-reply@asolutions.jp}
|
|
||||||
- EMAIL_FROM_NAME=${EMAIL_FROM_NAME:-Assist Solutions}
|
|
||||||
- SENDGRID_API_KEY=${SENDGRID_API_KEY}
|
|
||||||
|
|
||||||
# Portal
|
|
||||||
- PORTAL_PRICEBOOK_ID=${PORTAL_PRICEBOOK_ID}
|
|
||||||
- PORTAL_PRICEBOOK_NAME=${PORTAL_PRICEBOOK_NAME:-Portal}
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
- LOG_LEVEL=${LOG_LEVEL:-info}
|
|
||||||
|
|
||||||
# Migrations
|
|
||||||
- RUN_MIGRATIONS=${RUN_MIGRATIONS:-true}
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
database:
|
|
||||||
condition: service_healthy
|
|
||||||
cache:
|
|
||||||
condition: service_healthy
|
|
||||||
networks:
|
|
||||||
- portal-network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "node", "-e", "fetch('http://localhost:4000/health').then(r=>r.ok||process.exit(1)).catch(()=>process.exit(1))"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
start_period: 60s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# PostgreSQL Database
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
database:
|
|
||||||
image: postgres:17-alpine
|
|
||||||
container_name: portal-database
|
|
||||||
environment:
|
|
||||||
- POSTGRES_DB=${POSTGRES_DB:-portal_prod}
|
|
||||||
- POSTGRES_USER=${POSTGRES_USER:-portal}
|
|
||||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
|
||||||
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
|
|
||||||
volumes:
|
|
||||||
- postgres_data:/var/lib/postgresql/data
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- portal-network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-portal} -d ${POSTGRES_DB:-portal_prod}"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
start_period: 30s
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Redis Cache
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
cache:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: portal-cache
|
|
||||||
command: ["redis-server", "--save", "60", "1", "--loglevel", "warning", "--maxmemory", "128mb", "--maxmemory-policy", "allkeys-lru"]
|
|
||||||
volumes:
|
|
||||||
- redis_data:/data
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- portal-network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres_data:
|
|
||||||
driver: local
|
|
||||||
redis_data:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
networks:
|
|
||||||
portal-network:
|
|
||||||
driver: bridge
|
|
||||||
|
|
||||||
180
env/dev.env.sample
vendored
180
env/dev.env.sample
vendored
@ -1,21 +1,21 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Customer Portal - Development Environment
|
# Customer Portal - Development Environment
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Copy to .env in project root for local development
|
# Copy to .env in project root OR to apps/portal/.env.development and
|
||||||
# This file configures both frontend and backend for dev mode
|
# apps/bff/.env.development for per-app configuration.
|
||||||
|
#
|
||||||
|
# Most settings have sensible defaults - only required values and
|
||||||
|
# dev-specific overrides are listed here.
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Core
|
# REQUIRED - No defaults, must be set
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
NODE_ENV=development
|
DATABASE_URL=postgresql://dev:dev@localhost:5432/portal_dev?schema=public
|
||||||
APP_NAME=customer-portal-bff
|
|
||||||
APP_BASE_URL=http://localhost:3000
|
|
||||||
|
|
||||||
# Ports
|
# Generate with: openssl rand -base64 32
|
||||||
BFF_PORT=4000
|
JWT_SECRET=HjHsUyTE3WhPn5N07iSvurdV4hk2VEkIuN+lIflHhVQ=
|
||||||
NEXT_PORT=3000
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Frontend (Next.js) - Browser-exposed variables
|
# Frontend (Next.js) - Browser-exposed variables
|
||||||
@ -27,112 +27,82 @@ NEXT_PUBLIC_API_BASE=http://localhost:4000
|
|||||||
NEXT_PUBLIC_ENABLE_DEVTOOLS=true
|
NEXT_PUBLIC_ENABLE_DEVTOOLS=true
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Database & Cache
|
# CORS - Required for direct browser calls to BFF
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
DATABASE_URL=postgresql://dev:dev@localhost:5432/portal_dev?schema=public
|
|
||||||
REDIS_URL=redis://localhost:6379
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Security
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Generate with: openssl rand -base64 32
|
|
||||||
JWT_SECRET=HjHsUyTE3WhPn5N07iSvurdV4hk2VEkIuN+lIflHhVQ=
|
|
||||||
JWT_EXPIRES_IN=7d
|
|
||||||
BCRYPT_ROUNDS=12
|
|
||||||
|
|
||||||
CORS_ORIGIN=http://localhost:3000
|
CORS_ORIGIN=http://localhost:3000
|
||||||
TRUST_PROXY=false
|
|
||||||
|
|
||||||
# Redis token handling (relaxed for dev)
|
|
||||||
AUTH_ALLOW_REDIS_TOKEN_FAILOPEN=false
|
|
||||||
AUTH_REQUIRE_REDIS_FOR_TOKENS=false
|
|
||||||
AUTH_MAINTENANCE_MODE=false
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Rate Limiting (relaxed for dev)
|
# Dev Overrides (relaxed limits, verbose logging)
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
RATE_LIMIT_TTL=60
|
|
||||||
RATE_LIMIT_LIMIT=1000
|
|
||||||
AUTH_RATE_LIMIT_TTL=900
|
|
||||||
AUTH_RATE_LIMIT_LIMIT=10
|
|
||||||
|
|
||||||
# Show detailed validation errors in dev
|
|
||||||
EXPOSE_VALIDATION_ERRORS=true
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# WHMCS Integration (dev credentials)
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
WHMCS_DEV_BASE_URL=
|
|
||||||
WHMCS_DEV_API_IDENTIFIER=
|
|
||||||
WHMCS_DEV_API_SECRET=
|
|
||||||
WHMCS_QUEUE_CONCURRENCY=15
|
|
||||||
WHMCS_QUEUE_TIMEOUT_MS=30000
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Salesforce Integration
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
SF_LOGIN_URL=
|
|
||||||
SF_CLIENT_ID=
|
|
||||||
SF_PRIVATE_KEY_PATH=./secrets/sf-private.key
|
|
||||||
SF_USERNAME=
|
|
||||||
|
|
||||||
# Timeouts
|
|
||||||
SF_AUTH_TIMEOUT_MS=30000
|
|
||||||
SF_TOKEN_TTL_MS=720000
|
|
||||||
SF_TOKEN_REFRESH_BUFFER_MS=60000
|
|
||||||
|
|
||||||
# Queue throttling
|
|
||||||
SF_QUEUE_CONCURRENCY=15
|
|
||||||
SF_QUEUE_TIMEOUT_MS=30000
|
|
||||||
SF_QUEUE_LONG_RUNNING_TIMEOUT_MS=600000
|
|
||||||
|
|
||||||
# Platform Events
|
|
||||||
SF_EVENTS_ENABLED=true
|
|
||||||
SF_EVENTS_REPLAY=LATEST
|
|
||||||
SF_PUBSUB_ENDPOINT=api.pubsub.salesforce.com:7443
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Freebit SIM Management
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
FREEBIT_BASE_URL=
|
|
||||||
FREEBIT_OEM_ID=
|
|
||||||
FREEBIT_OEM_KEY=
|
|
||||||
FREEBIT_TIMEOUT=30000
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Email
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
EMAIL_ENABLED=true
|
|
||||||
EMAIL_USE_QUEUE=true
|
|
||||||
EMAIL_FROM=no-reply@asolutions.co.jp
|
|
||||||
EMAIL_FROM_NAME=Assist Solutions
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Portal Configuration
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
PORTAL_PRICEBOOK_ID=01sTL000008eLVlYAM
|
|
||||||
PORTAL_PRICEBOOK_NAME=Portal
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Logging
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
DISABLE_HTTP_LOGGING=false
|
RATE_LIMIT_LIMIT=1000
|
||||||
|
AUTH_RATE_LIMIT_LIMIT=10
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Local Dev Bypasses (NEVER enable in production!)
|
# External Services - Fill in when testing integrations
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
DISABLE_CSRF=false
|
# WHMCS (Development/Sandbox)
|
||||||
DISABLE_RATE_LIMIT=false
|
WHMCS_DEV_BASE_URL=
|
||||||
DISABLE_ACCOUNT_LOCKING=false
|
WHMCS_DEV_API_IDENTIFIER=
|
||||||
|
WHMCS_DEV_API_SECRET=
|
||||||
|
|
||||||
|
# Salesforce
|
||||||
|
SF_LOGIN_URL=
|
||||||
|
SF_CLIENT_ID=
|
||||||
|
SF_USERNAME=
|
||||||
|
SF_PRIVATE_KEY_PATH=./secrets/sf-private.key
|
||||||
|
|
||||||
|
# Freebit SIM Management
|
||||||
|
FREEBIT_BASE_URL=
|
||||||
|
FREEBIT_OEM_KEY=
|
||||||
|
|
||||||
|
# SendGrid Email
|
||||||
|
SENDGRID_API_KEY=
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# DEFAULTS - Uncomment only if you need to override
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Core (defaults shown)
|
||||||
|
# NODE_ENV=development
|
||||||
|
# APP_NAME=customer-portal-bff
|
||||||
|
# APP_BASE_URL=http://localhost:3000
|
||||||
|
# BFF_PORT=4000
|
||||||
|
# REDIS_URL=redis://localhost:6379
|
||||||
|
|
||||||
|
# Security (defaults shown)
|
||||||
|
# JWT_EXPIRES_IN=7d
|
||||||
|
# BCRYPT_ROUNDS=14
|
||||||
|
# TRUST_PROXY=false
|
||||||
|
|
||||||
|
# Rate Limiting (production defaults - dev overrides above)
|
||||||
|
# RATE_LIMIT_TTL=60
|
||||||
|
# AUTH_RATE_LIMIT_TTL=900
|
||||||
|
|
||||||
|
# Freebit (defaults shown)
|
||||||
|
# FREEBIT_OEM_ID=PASI
|
||||||
|
# FREEBIT_TIMEOUT=30000
|
||||||
|
# FREEBIT_RETRY_ATTEMPTS=3
|
||||||
|
|
||||||
|
# Email (defaults shown)
|
||||||
|
# EMAIL_ENABLED=true
|
||||||
|
# EMAIL_USE_QUEUE=true
|
||||||
|
# EMAIL_FROM=no-reply@example.com
|
||||||
|
# EMAIL_FROM_NAME=Assist Solutions
|
||||||
|
|
||||||
|
# Portal (defaults shown)
|
||||||
|
# PORTAL_PRICEBOOK_ID=01sTL000008eLVlYAM
|
||||||
|
# PORTAL_PRICEBOOK_NAME=Portal
|
||||||
|
|
||||||
|
# Salesforce Events (defaults shown)
|
||||||
|
# SF_EVENTS_ENABLED=false
|
||||||
|
# SF_EVENTS_REPLAY=LATEST
|
||||||
|
# SF_PUBSUB_ENDPOINT=api.pubsub.salesforce.com:7443
|
||||||
|
|
||||||
|
# Dev Bypasses - NEVER enable in production!
|
||||||
|
# DISABLE_CSRF=false
|
||||||
|
# DISABLE_RATE_LIMIT=false
|
||||||
|
# DISABLE_ACCOUNT_LOCKING=false
|
||||||
|
|||||||
152
env/portal-backend.env.sample
vendored
152
env/portal-backend.env.sample
vendored
@ -6,7 +6,7 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# REQUIRED - Must be configured
|
# REQUIRED - Must be configured (no defaults)
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
@ -17,117 +17,118 @@ JWT_SECRET=CHANGE_ME_GENERATE_WITH_openssl_rand_base64_32
|
|||||||
# Core Application
|
# Core Application
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
APP_NAME=customer-portal-bff
|
# APP_NAME=customer-portal-bff # default
|
||||||
APP_BASE_URL=https://your-domain.com
|
APP_BASE_URL=https://your-domain.com
|
||||||
BFF_PORT=4000
|
# BFF_PORT=4000 # default
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Cache & Session
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
REDIS_URL=redis://cache:6379/0
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Security
|
# Security
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
# Redis cache (required for production token management)
|
|
||||||
REDIS_URL=redis://cache:6379/0
|
|
||||||
|
|
||||||
# JWT configuration
|
|
||||||
JWT_EXPIRES_IN=7d
|
|
||||||
BCRYPT_ROUNDS=12
|
|
||||||
|
|
||||||
# CORS - set to your frontend domain
|
|
||||||
CORS_ORIGIN=https://your-domain.com
|
CORS_ORIGIN=https://your-domain.com
|
||||||
TRUST_PROXY=true
|
TRUST_PROXY=true
|
||||||
|
|
||||||
# CSRF Protection (generate secret: openssl rand -base64 32)
|
# CSRF Protection (generate: openssl rand -base64 32)
|
||||||
CSRF_SECRET_KEY=CHANGE_ME_GENERATE_WITH_openssl_rand_base64_32
|
CSRF_SECRET_KEY=CHANGE_ME_GENERATE_WITH_openssl_rand_base64_32
|
||||||
|
|
||||||
|
# JWT (defaults shown)
|
||||||
|
# JWT_EXPIRES_IN=7d
|
||||||
|
# BCRYPT_ROUNDS=14
|
||||||
|
|
||||||
# Redis token handling
|
# Redis token handling
|
||||||
AUTH_ALLOW_REDIS_TOKEN_FAILOPEN=false
|
# AUTH_ALLOW_REDIS_TOKEN_FAILOPEN=false # default
|
||||||
AUTH_REQUIRE_REDIS_FOR_TOKENS=false
|
# AUTH_REQUIRE_REDIS_FOR_TOKENS=false # default
|
||||||
|
# AUTH_MAINTENANCE_MODE=false # default
|
||||||
# Maintenance mode (enable during deployments)
|
|
||||||
AUTH_MAINTENANCE_MODE=false
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Rate Limiting
|
# Rate Limiting (defaults shown - uncomment to override)
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
RATE_LIMIT_TTL=60
|
# RATE_LIMIT_TTL=60
|
||||||
RATE_LIMIT_LIMIT=100
|
# RATE_LIMIT_LIMIT=100
|
||||||
AUTH_RATE_LIMIT_TTL=900
|
# AUTH_RATE_LIMIT_TTL=900
|
||||||
AUTH_RATE_LIMIT_LIMIT=3
|
# AUTH_RATE_LIMIT_LIMIT=3
|
||||||
LOGIN_RATE_LIMIT_TTL=900
|
# LOGIN_RATE_LIMIT_TTL=900
|
||||||
LOGIN_RATE_LIMIT_LIMIT=5
|
# LOGIN_RATE_LIMIT_LIMIT=5
|
||||||
|
|
||||||
# CAPTCHA (optional - set provider to 'turnstile' or 'hcaptcha' to enable)
|
# CAPTCHA (optional)
|
||||||
AUTH_CAPTCHA_PROVIDER=none
|
# AUTH_CAPTCHA_PROVIDER=none # 'none', 'turnstile', or 'hcaptcha'
|
||||||
AUTH_CAPTCHA_SECRET=
|
# AUTH_CAPTCHA_SECRET=
|
||||||
|
|
||||||
# Hide validation errors from clients in production
|
|
||||||
EXPOSE_VALIDATION_ERRORS=false
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# WHMCS Integration
|
# EXTERNAL SERVICES - Configure all that apply
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# --- WHMCS (Billing) ---
|
||||||
WHMCS_BASE_URL=https://accounts.asolutions.co.jp
|
WHMCS_BASE_URL=https://accounts.asolutions.co.jp
|
||||||
WHMCS_API_IDENTIFIER=
|
WHMCS_API_IDENTIFIER=CHANGE_ME
|
||||||
WHMCS_API_SECRET=
|
WHMCS_API_SECRET=CHANGE_ME
|
||||||
WHMCS_WEBHOOK_SECRET=
|
|
||||||
|
|
||||||
# Queue throttling
|
# Queue settings (defaults shown)
|
||||||
WHMCS_QUEUE_CONCURRENCY=15
|
# WHMCS_QUEUE_CONCURRENCY=15
|
||||||
WHMCS_QUEUE_TIMEOUT_MS=30000
|
# WHMCS_QUEUE_INTERVAL_CAP=300
|
||||||
|
# WHMCS_QUEUE_TIMEOUT_MS=30000
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Salesforce Integration
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
# --- Salesforce (CRM) ---
|
||||||
SF_LOGIN_URL=https://asolutions.my.salesforce.com
|
SF_LOGIN_URL=https://asolutions.my.salesforce.com
|
||||||
SF_CLIENT_ID=
|
SF_CLIENT_ID=CHANGE_ME
|
||||||
SF_USERNAME=
|
SF_USERNAME=CHANGE_ME
|
||||||
SF_PRIVATE_KEY_PATH=/app/secrets/sf-private.key
|
SF_PRIVATE_KEY_PATH=/app/secrets/sf-private.key
|
||||||
SF_WEBHOOK_SECRET=
|
|
||||||
|
|
||||||
# Queue throttling
|
# Queue settings (defaults shown)
|
||||||
SF_QUEUE_CONCURRENCY=15
|
# SF_QUEUE_CONCURRENCY=15
|
||||||
SF_QUEUE_TIMEOUT_MS=30000
|
# SF_QUEUE_LONG_RUNNING_CONCURRENCY=22
|
||||||
SF_QUEUE_LONG_RUNNING_TIMEOUT_MS=600000
|
# SF_QUEUE_INTERVAL_CAP=600
|
||||||
|
# SF_QUEUE_TIMEOUT_MS=30000
|
||||||
|
# SF_QUEUE_LONG_RUNNING_TIMEOUT_MS=600000
|
||||||
|
# SF_DAILY_API_LIMIT=100000
|
||||||
|
|
||||||
|
# Token management (defaults shown)
|
||||||
|
# SF_AUTH_TIMEOUT_MS=30000
|
||||||
|
# SF_TOKEN_TTL_MS=720000
|
||||||
|
# SF_TOKEN_REFRESH_BUFFER_MS=60000
|
||||||
|
|
||||||
# Platform Events
|
# Platform Events
|
||||||
SF_EVENTS_ENABLED=true
|
SF_EVENTS_ENABLED=true
|
||||||
SF_EVENTS_REPLAY=LATEST
|
# SF_EVENTS_REPLAY=LATEST # default
|
||||||
SF_PUBSUB_ENDPOINT=api.pubsub.salesforce.com:7443
|
# SF_PUBSUB_ENDPOINT=api.pubsub.salesforce.com:7443 # default
|
||||||
|
# SF_PUBSUB_NUM_REQUESTED=25 # default
|
||||||
# -----------------------------------------------------------------------------
|
# SF_PUBSUB_QUEUE_MAX=100 # default
|
||||||
# Freebit SIM Management
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
# --- Freebit (SIM Management) ---
|
||||||
FREEBIT_BASE_URL=https://i1.mvno.net/emptool/api
|
FREEBIT_BASE_URL=https://i1.mvno.net/emptool/api
|
||||||
FREEBIT_OEM_ID=PASI
|
FREEBIT_OEM_KEY=CHANGE_ME
|
||||||
FREEBIT_OEM_KEY=
|
# FREEBIT_OEM_ID=PASI # default
|
||||||
FREEBIT_TIMEOUT=30000
|
# FREEBIT_TIMEOUT=30000 # default
|
||||||
|
# FREEBIT_RETRY_ATTEMPTS=3 # default
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# --- SendGrid (Email) ---
|
||||||
# Email (SendGrid)
|
SENDGRID_API_KEY=CHANGE_ME
|
||||||
# -----------------------------------------------------------------------------
|
# EMAIL_ENABLED=true # default
|
||||||
|
# EMAIL_USE_QUEUE=true # default
|
||||||
EMAIL_ENABLED=true
|
|
||||||
EMAIL_FROM=no-reply@asolutions.jp
|
EMAIL_FROM=no-reply@asolutions.jp
|
||||||
EMAIL_FROM_NAME=Assist Solutions
|
# EMAIL_FROM_NAME=Assist Solutions # default
|
||||||
SENDGRID_API_KEY=
|
# SENDGRID_SANDBOX=false # default
|
||||||
SENDGRID_SANDBOX=false
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Portal Configuration
|
# Portal Configuration
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
PORTAL_PRICEBOOK_ID=
|
PORTAL_PRICEBOOK_ID=CHANGE_ME
|
||||||
PORTAL_PRICEBOOK_NAME=Portal
|
# PORTAL_PRICEBOOK_NAME=Portal # default
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
|
# PRETTY_LOGS=false # auto-disabled in production
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Node Runtime
|
# Node Runtime
|
||||||
@ -137,21 +138,20 @@ NODE_OPTIONS=--max-old-space-size=512
|
|||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# ADVANCED CONFIGURATION (rarely need to change)
|
# SALESFORCE FIELD MAPPINGS (Advanced)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# The following variables have sensible defaults and only need to be set
|
# All fields have sensible defaults matching standard SF field API names.
|
||||||
# if your Salesforce org uses non-standard field API names.
|
# Only uncomment if your Salesforce org uses non-standard field names.
|
||||||
# Uncomment and modify only if needed.
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
# --- Salesforce Field Mappings - Account ---
|
# --- Account Fields ---
|
||||||
# ACCOUNT_INTERNET_ELIGIBILITY_FIELD=Internet_Eligibility__c
|
# ACCOUNT_INTERNET_ELIGIBILITY_FIELD=Internet_Eligibility__c
|
||||||
# ACCOUNT_CUSTOMER_NUMBER_FIELD=SF_Account_No__c
|
# ACCOUNT_CUSTOMER_NUMBER_FIELD=SF_Account_No__c
|
||||||
# ACCOUNT_PORTAL_STATUS_FIELD=Portal_Status__c
|
# ACCOUNT_PORTAL_STATUS_FIELD=Portal_Status__c
|
||||||
# ACCOUNT_PORTAL_STATUS_SOURCE_FIELD=Portal_Registration_Source__c
|
# ACCOUNT_PORTAL_STATUS_SOURCE_FIELD=Portal_Registration_Source__c
|
||||||
# ACCOUNT_PORTAL_LAST_SIGNED_IN_FIELD=Portal_Last_SignIn__c
|
# ACCOUNT_PORTAL_LAST_SIGNED_IN_FIELD=Portal_Last_SignIn__c
|
||||||
|
|
||||||
# --- Salesforce Field Mappings - Product ---
|
# --- Product Fields ---
|
||||||
# PRODUCT_SKU_FIELD=StockKeepingUnit
|
# PRODUCT_SKU_FIELD=StockKeepingUnit
|
||||||
# PRODUCT_PORTAL_CATEGORY_FIELD=Product2Categories1__c
|
# PRODUCT_PORTAL_CATEGORY_FIELD=Product2Categories1__c
|
||||||
# PRODUCT_PORTAL_CATALOG_FIELD=Portal_Catalog__c
|
# PRODUCT_PORTAL_CATALOG_FIELD=Portal_Catalog__c
|
||||||
@ -163,14 +163,12 @@ NODE_OPTIONS=--max-old-space-size=512
|
|||||||
# PRODUCT_INTERNET_PLAN_TIER_FIELD=Internet_Plan_Tier__c
|
# PRODUCT_INTERNET_PLAN_TIER_FIELD=Internet_Plan_Tier__c
|
||||||
# PRODUCT_INTERNET_OFFERING_TYPE_FIELD=Internet_Offering_Type__c
|
# PRODUCT_INTERNET_OFFERING_TYPE_FIELD=Internet_Offering_Type__c
|
||||||
# PRODUCT_DISPLAY_ORDER_FIELD=Catalog_Order__c
|
# PRODUCT_DISPLAY_ORDER_FIELD=Catalog_Order__c
|
||||||
# PRODUCT_BUNDLED_ADDON_FIELD=Bundled_Addon__c
|
|
||||||
# PRODUCT_IS_BUNDLED_ADDON_FIELD=Is_Bundled_Addon__c
|
|
||||||
# PRODUCT_SIM_DATA_SIZE_FIELD=SIM_Data_Size__c
|
# PRODUCT_SIM_DATA_SIZE_FIELD=SIM_Data_Size__c
|
||||||
# PRODUCT_SIM_PLAN_TYPE_FIELD=SIM_Plan_Type__c
|
# PRODUCT_SIM_PLAN_TYPE_FIELD=SIM_Plan_Type__c
|
||||||
# PRODUCT_SIM_HAS_FAMILY_DISCOUNT_FIELD=SIM_Has_Family_Discount__c
|
# PRODUCT_SIM_HAS_FAMILY_DISCOUNT_FIELD=SIM_Has_Family_Discount__c
|
||||||
# PRODUCT_VPN_REGION_FIELD=VPN_Region__c
|
# PRODUCT_VPN_REGION_FIELD=VPN_Region__c
|
||||||
|
|
||||||
# --- Salesforce Field Mappings - Order ---
|
# --- Order Fields ---
|
||||||
# ORDER_TYPE_FIELD=Type
|
# ORDER_TYPE_FIELD=Type
|
||||||
# ORDER_ACTIVATION_TYPE_FIELD=Activation_Type__c
|
# ORDER_ACTIVATION_TYPE_FIELD=Activation_Type__c
|
||||||
# ORDER_ACTIVATION_SCHEDULED_AT_FIELD=Activation_Scheduled_At__c
|
# ORDER_ACTIVATION_SCHEDULED_AT_FIELD=Activation_Scheduled_At__c
|
||||||
@ -183,7 +181,7 @@ NODE_OPTIONS=--max-old-space-size=512
|
|||||||
# ORDER_MNP_PHONE_FIELD=MNP_Phone_Number__c
|
# ORDER_MNP_PHONE_FIELD=MNP_Phone_Number__c
|
||||||
# ORDER_WHMCS_ORDER_ID_FIELD=WHMCS_Order_ID__c
|
# ORDER_WHMCS_ORDER_ID_FIELD=WHMCS_Order_ID__c
|
||||||
|
|
||||||
# --- Salesforce CDC Channels (Change Data Capture) ---
|
# --- CDC Channels ---
|
||||||
# SF_CATALOG_PRODUCT_CDC_CHANNEL=/data/Product2ChangeEvent
|
# SF_CATALOG_PRODUCT_CDC_CHANNEL=/data/Product2ChangeEvent
|
||||||
# SF_CATALOG_PRICEBOOKENTRY_CDC_CHANNEL=/data/PricebookEntryChangeEvent
|
# SF_CATALOG_PRICEBOOKENTRY_CDC_CHANNEL=/data/PricebookEntryChangeEvent
|
||||||
# SF_ORDER_CDC_CHANNEL=/data/OrderChangeEvent
|
# SF_ORDER_CDC_CHANNEL=/data/OrderChangeEvent
|
||||||
|
|||||||
7
env/portal-frontend.env.sample
vendored
7
env/portal-frontend.env.sample
vendored
@ -2,16 +2,17 @@
|
|||||||
# Customer Portal Frontend (Next.js) - Production Environment
|
# Customer Portal Frontend (Next.js) - Production Environment
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Copy to portal-frontend.env
|
# Copy to portal-frontend.env
|
||||||
# Note: NEXT_PUBLIC_* variables are embedded at build time
|
# Note: NEXT_PUBLIC_* variables are embedded at BUILD time
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
|
|
||||||
# Application name shown in UI
|
# Application identity shown in UI
|
||||||
NEXT_PUBLIC_APP_NAME=Assist Solutions Portal
|
NEXT_PUBLIC_APP_NAME=Assist Solutions Portal
|
||||||
NEXT_PUBLIC_APP_VERSION=1.0.0
|
NEXT_PUBLIC_APP_VERSION=1.0.0
|
||||||
|
|
||||||
# API endpoint - use /api for same-origin requests behind reverse proxy
|
# API endpoint - use /api for same-origin requests behind nginx proxy
|
||||||
|
# In production, nginx proxies /api/* to the BFF container
|
||||||
NEXT_PUBLIC_API_BASE=/api
|
NEXT_PUBLIC_API_BASE=/api
|
||||||
|
|
||||||
# Disable React Query devtools in production
|
# Disable React Query devtools in production
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "./scripts/dev/manage.sh apps",
|
"dev": "./scripts/dev/manage.sh apps",
|
||||||
"dev:all": "pnpm --filter @customer-portal/domain build && pnpm --parallel --filter @customer-portal/portal --filter @customer-portal/bff run dev",
|
"dev:all": "pnpm --filter @customer-portal/domain build && pnpm --parallel --filter @customer-portal/portal --filter @customer-portal/bff run dev",
|
||||||
|
"dev:apps": "pnpm --parallel --filter @customer-portal/portal --filter @customer-portal/bff run dev",
|
||||||
"build": "pnpm --filter @customer-portal/domain build && pnpm --recursive --filter=!@customer-portal/domain run build",
|
"build": "pnpm --filter @customer-portal/domain build && pnpm --recursive --filter=!@customer-portal/domain run build",
|
||||||
"start": "pnpm --parallel --filter @customer-portal/portal --filter @customer-portal/bff run start",
|
"start": "pnpm --parallel --filter @customer-portal/portal --filter @customer-portal/bff run start",
|
||||||
"test": "pnpm --recursive run test",
|
"test": "pnpm --recursive run test",
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
b809f14715623b94d3a38d058ab09ef4cb3f9aa655031c4613c2feeedacce14b /home/barsa/projects/customer_portal/customer-portal/portal-backend.latest.tar.gz
|
2b5865668763cce0781a0e6448ce2106f1736c27ade328b2da4920d6946ecce7 /home/barsa/projects/customer_portal/customer-portal/portal-backend.latest.tar.gz
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
447e6f2bebb4670bab788977187704c10a5b8cf0e318f950d16bc15d1e459ce2 /home/barsa/projects/customer_portal/customer-portal/portal-frontend.latest.tar.gz
|
e37256f0aa756d9838bcc00d1fd2433db1d199e72b38595d0c986a7d85eb534e /home/barsa/projects/customer_portal/customer-portal/portal-frontend.latest.tar.gz
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user