diff --git a/.gitignore b/.gitignore index 68b097df..d9bf8f57 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ node_modules/ # Environment files .env .env.local +.env.development .env.production .env.test diff --git a/apps/bff/scripts/docker-entrypoint.sh b/apps/bff/scripts/docker-entrypoint.sh index 17900f41..68032a5a 100755 --- a/apps/bff/scripts/docker-entrypoint.sh +++ b/apps/bff/scripts/docker-entrypoint.sh @@ -88,8 +88,14 @@ fi # ============================================================================= if [ "$RUN_MIGRATIONS" = "true" ] && [ -n "$DATABASE_URL" ]; then 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" else echo "⚠️ Migration failed - check database connectivity" diff --git a/apps/bff/src/core/config/app.config.ts b/apps/bff/src/core/config/app.config.ts index 4d27af57..bbd571a3 100644 --- a/apps/bff/src/core/config/app.config.ts +++ b/apps/bff/src/core/config/app.config.ts @@ -2,12 +2,23 @@ import { resolve } from "node:path"; import type { ConfigModuleOptions } from "@nestjs/config"; 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 = { isGlobal: true, expandVariables: true, validate, 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"), // Monorepo root + resolve(process.cwd(), "../../.env"), // Monorepo root fallback ], }; diff --git a/apps/bff/src/core/logging/logging.module.ts b/apps/bff/src/core/logging/logging.module.ts index 754b999e..a46dbe47 100644 --- a/apps/bff/src/core/logging/logging.module.ts +++ b/apps/bff/src/core/logging/logging.module.ts @@ -1,6 +1,8 @@ import { Global, Module } from "@nestjs/common"; import { LoggerModule } from "nestjs-pino"; +const prettyLogsEnabled = process.env.PRETTY_LOGS === "true" || process.env.NODE_ENV !== "production"; + @Global() @Module({ imports: [ @@ -31,18 +33,18 @@ import { LoggerModule } from "nestjs-pino"; statusCode: res.statusCode, }), }, - transport: - process.env.NODE_ENV === "development" - ? { - target: "pino-pretty", - options: { - colorize: true, - translateTime: "yyyy-mm-dd HH:MM:ss", - ignore: "pid,hostname,req,res", - messageFormat: "{msg}", - }, - } - : undefined, + transport: prettyLogsEnabled + ? { + target: "pino-pretty", + options: { + colorize: true, + translateTime: false, + singleLine: true, + // keep level for coloring but drop other noisy metadata + ignore: "pid,req,res,context,name,time", + }, + } + : undefined, redact: { paths: [ "req.headers.authorization", diff --git a/apps/bff/src/modules/auth/presentation/strategies/local.strategy.ts b/apps/bff/src/modules/auth/presentation/strategies/local.strategy.ts index cd40ea59..330f8a99 100644 --- a/apps/bff/src/modules/auth/presentation/strategies/local.strategy.ts +++ b/apps/bff/src/modules/auth/presentation/strategies/local.strategy.ts @@ -3,6 +3,7 @@ import { PassportStrategy } from "@nestjs/passport"; import { Strategy } from "passport-local"; import type { Request } from "express"; import { AuthFacade } from "@bff/modules/auth/application/auth.facade.js"; +import { ErrorCode, ErrorMessages } from "@customer-portal/domain/common"; @Injectable() export class LocalStrategy extends PassportStrategy(Strategy) { @@ -17,7 +18,7 @@ export class LocalStrategy extends PassportStrategy(Strategy) { ): Promise<{ id: string; email: string; role: string }> { const user = await this.authFacade.validateUser(email, password, req); if (!user) { - throw new UnauthorizedException("Invalid credentials"); + throw new UnauthorizedException(ErrorMessages[ErrorCode.INVALID_CREDENTIALS]); } return user; } diff --git a/apps/bff/test/catalog-contract.spec.ts b/apps/bff/test/catalog-contract.spec.ts deleted file mode 100644 index e840a4ba..00000000 --- a/apps/bff/test/catalog-contract.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -/// -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 => - 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(); - }); -}); diff --git a/apps/bff/test/jest-e2e.json b/apps/bff/test/jest-e2e.json deleted file mode 100644 index dda3baf1..00000000 --- a/apps/bff/test/jest-e2e.json +++ /dev/null @@ -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/(.*)$": "/../src/$1" - }, - "globals": { - "ts-jest": { - "tsconfig": { - "types": ["jest", "node"] - } - } - } -} - diff --git a/apps/bff/test/tsconfig.json b/apps/bff/test/tsconfig.json deleted file mode 100644 index 1f8f422f..00000000 --- a/apps/bff/test/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "types": ["jest", "node", "supertest"] - }, - "include": ["./**/*.ts", "../src/**/*"] -} - diff --git a/apps/portal/next.config.mjs b/apps/portal/next.config.mjs index dd14c603..bb07c9b6 100644 --- a/apps/portal/next.config.mjs +++ b/apps/portal/next.config.mjs @@ -1,15 +1,10 @@ /* eslint-env node */ -import bundleAnalyzer from "@next/bundle-analyzer"; import path from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const workspaceRoot = path.resolve(__dirname, "..", ".."); -const withBundleAnalyzer = bundleAnalyzer({ - enabled: process.env.ANALYZE === "true", -}); - /** @type {import('next').NextConfig} */ const nextConfig = { output: process.env.NODE_ENV === "production" ? "standalone" : undefined, @@ -38,71 +33,6 @@ const nextConfig = { 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: { removeConsole: process.env.NODE_ENV === "production", }, @@ -114,4 +44,12 @@ const nextConfig = { 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; diff --git a/apps/portal/src/app/layout.tsx b/apps/portal/src/app/layout.tsx index d3541111..50df9a42 100644 --- a/apps/portal/src/app/layout.tsx +++ b/apps/portal/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; +import { headers } from "next/headers"; import "./globals.css"; import { QueryProvider } from "@/lib/providers"; 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 export const dynamic = 'force-dynamic'; -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ 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 ( - + {children} diff --git a/apps/portal/src/lib/api/runtime/client.ts b/apps/portal/src/lib/api/runtime/client.ts index dc41300f..aefda618 100644 --- a/apps/portal/src/lib/api/runtime/client.ts +++ b/apps/portal/src/lib/api/runtime/client.ts @@ -56,8 +56,9 @@ export interface ApiClient { /** * Resolve API base URL: - * - Browser: Use same origin (Next.js rewrites in dev, nginx proxy in prod) - * - SSR: Use NEXT_PUBLIC_API_BASE env var or fallback to localhost:4000 + * - If NEXT_PUBLIC_API_BASE is set, use it (enables direct BFF calls in dev via CORS) + * - Browser fallback: Use same origin (nginx proxy in prod) + * - SSR fallback: Use localhost:4000 */ export const resolveBaseUrl = (explicitBase?: string): string => { // 1. Explicit base URL provided (for testing/overrides) @@ -65,20 +66,20 @@ export const resolveBaseUrl = (explicitBase?: string): string => { return explicitBase.replace(/\/+$/, ""); } - // 2. Browser: use same origin - // - Development: Next.js rewrites proxy /api/* to localhost:4000 - // - Production: nginx proxies /api/* to backend - if (typeof window !== "undefined" && window.location?.origin) { - return window.location.origin; - } - - // 3. Server-side: check env var for SSR requests + // 2. Check NEXT_PUBLIC_API_BASE env var (works in both browser and SSR) + // In development: set to http://localhost:4000 for direct CORS calls + // In production: typically not set, falls through to same-origin const envBase = process.env.NEXT_PUBLIC_API_BASE; if (envBase?.trim() && envBase.startsWith("http")) { 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"; }; diff --git a/apps/portal/src/lib/providers.tsx b/apps/portal/src/lib/providers.tsx index 40e32a2d..8cde2984 100644 --- a/apps/portal/src/lib/providers.tsx +++ b/apps/portal/src/lib/providers.tsx @@ -1,6 +1,6 @@ /** * React Query Provider - * Simple provider setup for TanStack Query + * Simple provider setup for TanStack Query with CSP nonce support */ "use client"; @@ -10,7 +10,12 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { useState } from "react"; 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( () => new QueryClient({ @@ -46,7 +51,7 @@ export function QueryProvider({ children }: { children: React.ReactNode }) { return ( {children} - {process.env.NODE_ENV === "development" && } + {process.env.NODE_ENV === "development" && } ); } diff --git a/apps/portal/src/proxy.ts b/apps/portal/src/proxy.ts new file mode 100644 index 00000000..4b48a901 --- /dev/null +++ b/apps/portal/src/proxy.ts @@ -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" }, + ], + }, + ], +}; diff --git a/docker/portainer/PORTAINER-GUIDE.md b/docker/Prod - Portainer/PORTAINER-GUIDE.md similarity index 100% rename from docker/portainer/PORTAINER-GUIDE.md rename to docker/Prod - Portainer/PORTAINER-GUIDE.md diff --git a/docker/portainer/README.md b/docker/Prod - Portainer/README.md similarity index 100% rename from docker/portainer/README.md rename to docker/Prod - Portainer/README.md diff --git a/docker/portainer/docker-compose.yml b/docker/Prod - Portainer/docker-compose.yml similarity index 98% rename from docker/portainer/docker-compose.yml rename to docker/Prod - Portainer/docker-compose.yml index ca5439f3..a1e1016f 100644 --- a/docker/portainer/docker-compose.yml +++ b/docker/Prod - Portainer/docker-compose.yml @@ -151,7 +151,7 @@ services: cache: image: redis:7-alpine 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: - redis_data:/data restart: unless-stopped diff --git a/docker/Prod - Portainer/stack.env b/docker/Prod - Portainer/stack.env new file mode 100644 index 00000000..e3634cfc --- /dev/null +++ b/docker/Prod - Portainer/stack.env @@ -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= + +# ----------------------------------------------------------------------------- +# Salesforce Portal Config +# ----------------------------------------------------------------------------- +PORTAL_PRICEBOOK_ID=01sTL000008eLVlYAM +PORTAL_PRICEBOOK_NAME=Portal + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +LOG_LEVEL=info diff --git a/docker/portainer/stack.env.example b/docker/Prod - Portainer/stack.env.example similarity index 100% rename from docker/portainer/stack.env.example rename to docker/Prod - Portainer/stack.env.example diff --git a/docker/portainer/update-stack.sh b/docker/Prod - Portainer/update-stack.sh similarity index 100% rename from docker/portainer/update-stack.sh rename to docker/Prod - Portainer/update-stack.sh diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml deleted file mode 100644 index fc51f65d..00000000 --- a/docker/prod/docker-compose.yml +++ /dev/null @@ -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 - diff --git a/env/dev.env.sample b/env/dev.env.sample index 4d830232..6f42f45d 100644 --- a/env/dev.env.sample +++ b/env/dev.env.sample @@ -1,21 +1,21 @@ # ============================================================================= # Customer Portal - Development Environment # ============================================================================= -# Copy to .env in project root for local development -# This file configures both frontend and backend for dev mode +# Copy to .env in project root OR to apps/portal/.env.development and +# 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 -APP_NAME=customer-portal-bff -APP_BASE_URL=http://localhost:3000 +DATABASE_URL=postgresql://dev:dev@localhost:5432/portal_dev?schema=public -# Ports -BFF_PORT=4000 -NEXT_PORT=3000 +# Generate with: openssl rand -base64 32 +JWT_SECRET=HjHsUyTE3WhPn5N07iSvurdV4hk2VEkIuN+lIflHhVQ= # ----------------------------------------------------------------------------- # Frontend (Next.js) - Browser-exposed variables @@ -27,112 +27,82 @@ NEXT_PUBLIC_API_BASE=http://localhost:4000 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 -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) -# ----------------------------------------------------------------------------- - -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 +# Dev Overrides (relaxed limits, verbose logging) # ----------------------------------------------------------------------------- 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 -DISABLE_RATE_LIMIT=false -DISABLE_ACCOUNT_LOCKING=false +# WHMCS (Development/Sandbox) +WHMCS_DEV_BASE_URL= +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 diff --git a/env/portal-backend.env.sample b/env/portal-backend.env.sample index 24f6f9b9..71af8521 100644 --- a/env/portal-backend.env.sample +++ b/env/portal-backend.env.sample @@ -6,7 +6,7 @@ # ============================================================================= # ----------------------------------------------------------------------------- -# REQUIRED - Must be configured +# REQUIRED - Must be configured (no defaults) # ----------------------------------------------------------------------------- NODE_ENV=production @@ -17,117 +17,118 @@ JWT_SECRET=CHANGE_ME_GENERATE_WITH_openssl_rand_base64_32 # Core Application # ----------------------------------------------------------------------------- -APP_NAME=customer-portal-bff +# APP_NAME=customer-portal-bff # default APP_BASE_URL=https://your-domain.com -BFF_PORT=4000 +# BFF_PORT=4000 # default + +# ----------------------------------------------------------------------------- +# Cache & Session +# ----------------------------------------------------------------------------- + +REDIS_URL=redis://cache:6379/0 # ----------------------------------------------------------------------------- # 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 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 +# JWT (defaults shown) +# JWT_EXPIRES_IN=7d +# BCRYPT_ROUNDS=14 + # Redis token handling -AUTH_ALLOW_REDIS_TOKEN_FAILOPEN=false -AUTH_REQUIRE_REDIS_FOR_TOKENS=false - -# Maintenance mode (enable during deployments) -AUTH_MAINTENANCE_MODE=false +# AUTH_ALLOW_REDIS_TOKEN_FAILOPEN=false # default +# AUTH_REQUIRE_REDIS_FOR_TOKENS=false # default +# AUTH_MAINTENANCE_MODE=false # default # ----------------------------------------------------------------------------- -# Rate Limiting +# Rate Limiting (defaults shown - uncomment to override) # ----------------------------------------------------------------------------- -RATE_LIMIT_TTL=60 -RATE_LIMIT_LIMIT=100 -AUTH_RATE_LIMIT_TTL=900 -AUTH_RATE_LIMIT_LIMIT=3 -LOGIN_RATE_LIMIT_TTL=900 -LOGIN_RATE_LIMIT_LIMIT=5 +# RATE_LIMIT_TTL=60 +# RATE_LIMIT_LIMIT=100 +# AUTH_RATE_LIMIT_TTL=900 +# AUTH_RATE_LIMIT_LIMIT=3 +# LOGIN_RATE_LIMIT_TTL=900 +# LOGIN_RATE_LIMIT_LIMIT=5 -# CAPTCHA (optional - set provider to 'turnstile' or 'hcaptcha' to enable) -AUTH_CAPTCHA_PROVIDER=none -AUTH_CAPTCHA_SECRET= - -# Hide validation errors from clients in production -EXPOSE_VALIDATION_ERRORS=false +# CAPTCHA (optional) +# AUTH_CAPTCHA_PROVIDER=none # 'none', 'turnstile', or 'hcaptcha' +# AUTH_CAPTCHA_SECRET= # ----------------------------------------------------------------------------- -# WHMCS Integration +# EXTERNAL SERVICES - Configure all that apply # ----------------------------------------------------------------------------- +# --- WHMCS (Billing) --- WHMCS_BASE_URL=https://accounts.asolutions.co.jp -WHMCS_API_IDENTIFIER= -WHMCS_API_SECRET= -WHMCS_WEBHOOK_SECRET= +WHMCS_API_IDENTIFIER=CHANGE_ME +WHMCS_API_SECRET=CHANGE_ME -# Queue throttling -WHMCS_QUEUE_CONCURRENCY=15 -WHMCS_QUEUE_TIMEOUT_MS=30000 - -# ----------------------------------------------------------------------------- -# Salesforce Integration -# ----------------------------------------------------------------------------- +# Queue settings (defaults shown) +# WHMCS_QUEUE_CONCURRENCY=15 +# WHMCS_QUEUE_INTERVAL_CAP=300 +# WHMCS_QUEUE_TIMEOUT_MS=30000 +# --- Salesforce (CRM) --- SF_LOGIN_URL=https://asolutions.my.salesforce.com -SF_CLIENT_ID= -SF_USERNAME= +SF_CLIENT_ID=CHANGE_ME +SF_USERNAME=CHANGE_ME SF_PRIVATE_KEY_PATH=/app/secrets/sf-private.key -SF_WEBHOOK_SECRET= -# Queue throttling -SF_QUEUE_CONCURRENCY=15 -SF_QUEUE_TIMEOUT_MS=30000 -SF_QUEUE_LONG_RUNNING_TIMEOUT_MS=600000 +# Queue settings (defaults shown) +# SF_QUEUE_CONCURRENCY=15 +# SF_QUEUE_LONG_RUNNING_CONCURRENCY=22 +# 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 SF_EVENTS_ENABLED=true -SF_EVENTS_REPLAY=LATEST -SF_PUBSUB_ENDPOINT=api.pubsub.salesforce.com:7443 - -# ----------------------------------------------------------------------------- -# Freebit SIM Management -# ----------------------------------------------------------------------------- +# SF_EVENTS_REPLAY=LATEST # default +# 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_BASE_URL=https://i1.mvno.net/emptool/api -FREEBIT_OEM_ID=PASI -FREEBIT_OEM_KEY= -FREEBIT_TIMEOUT=30000 +FREEBIT_OEM_KEY=CHANGE_ME +# FREEBIT_OEM_ID=PASI # default +# FREEBIT_TIMEOUT=30000 # default +# FREEBIT_RETRY_ATTEMPTS=3 # default -# ----------------------------------------------------------------------------- -# Email (SendGrid) -# ----------------------------------------------------------------------------- - -EMAIL_ENABLED=true +# --- SendGrid (Email) --- +SENDGRID_API_KEY=CHANGE_ME +# EMAIL_ENABLED=true # default +# EMAIL_USE_QUEUE=true # default EMAIL_FROM=no-reply@asolutions.jp -EMAIL_FROM_NAME=Assist Solutions -SENDGRID_API_KEY= -SENDGRID_SANDBOX=false +# EMAIL_FROM_NAME=Assist Solutions # default +# SENDGRID_SANDBOX=false # default # ----------------------------------------------------------------------------- # Portal Configuration # ----------------------------------------------------------------------------- -PORTAL_PRICEBOOK_ID= -PORTAL_PRICEBOOK_NAME=Portal +PORTAL_PRICEBOOK_ID=CHANGE_ME +# PORTAL_PRICEBOOK_NAME=Portal # default # ----------------------------------------------------------------------------- # Logging # ----------------------------------------------------------------------------- LOG_LEVEL=info +# PRETTY_LOGS=false # auto-disabled in production # ----------------------------------------------------------------------------- # 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 -# if your Salesforce org uses non-standard field API names. -# Uncomment and modify only if needed. +# All fields have sensible defaults matching standard SF field API names. +# Only uncomment if your Salesforce org uses non-standard field names. # ============================================================================= -# --- Salesforce Field Mappings - Account --- +# --- Account Fields --- # ACCOUNT_INTERNET_ELIGIBILITY_FIELD=Internet_Eligibility__c # ACCOUNT_CUSTOMER_NUMBER_FIELD=SF_Account_No__c # ACCOUNT_PORTAL_STATUS_FIELD=Portal_Status__c # ACCOUNT_PORTAL_STATUS_SOURCE_FIELD=Portal_Registration_Source__c # ACCOUNT_PORTAL_LAST_SIGNED_IN_FIELD=Portal_Last_SignIn__c -# --- Salesforce Field Mappings - Product --- +# --- Product Fields --- # PRODUCT_SKU_FIELD=StockKeepingUnit # PRODUCT_PORTAL_CATEGORY_FIELD=Product2Categories1__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_OFFERING_TYPE_FIELD=Internet_Offering_Type__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_PLAN_TYPE_FIELD=SIM_Plan_Type__c # PRODUCT_SIM_HAS_FAMILY_DISCOUNT_FIELD=SIM_Has_Family_Discount__c # PRODUCT_VPN_REGION_FIELD=VPN_Region__c -# --- Salesforce Field Mappings - Order --- +# --- Order Fields --- # ORDER_TYPE_FIELD=Type # ORDER_ACTIVATION_TYPE_FIELD=Activation_Type__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_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_PRICEBOOKENTRY_CDC_CHANNEL=/data/PricebookEntryChangeEvent # SF_ORDER_CDC_CHANNEL=/data/OrderChangeEvent diff --git a/env/portal-frontend.env.sample b/env/portal-frontend.env.sample index 8cbe014c..6bf42691 100644 --- a/env/portal-frontend.env.sample +++ b/env/portal-frontend.env.sample @@ -2,16 +2,17 @@ # Customer Portal Frontend (Next.js) - Production Environment # ============================================================================= # 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 -# Application name shown in UI +# Application identity shown in UI NEXT_PUBLIC_APP_NAME=Assist Solutions Portal 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 # Disable React Query devtools in production diff --git a/package.json b/package.json index 0a7763b1..8fd983f4 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "scripts": { "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: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", "start": "pnpm --parallel --filter @customer-portal/portal --filter @customer-portal/bff run start", "test": "pnpm --recursive run test", diff --git a/portal-backend.latest.tar.gz.sha256 b/portal-backend.latest.tar.gz.sha256 index 49e2b1b3..1a4fba12 100644 --- a/portal-backend.latest.tar.gz.sha256 +++ b/portal-backend.latest.tar.gz.sha256 @@ -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 diff --git a/portal-frontend.latest.tar.gz.sha256 b/portal-frontend.latest.tar.gz.sha256 index 0d861f06..04a59da0 100644 --- a/portal-frontend.latest.tar.gz.sha256 +++ b/portal-frontend.latest.tar.gz.sha256 @@ -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