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
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.production
|
||||
.env.test
|
||||
|
||||
|
||||
@ -89,7 +89,13 @@ 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"
|
||||
|
||||
@ -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
|
||||
],
|
||||
};
|
||||
|
||||
@ -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,15 +33,15 @@ import { LoggerModule } from "nestjs-pino";
|
||||
statusCode: res.statusCode,
|
||||
}),
|
||||
},
|
||||
transport:
|
||||
process.env.NODE_ENV === "development"
|
||||
transport: prettyLogsEnabled
|
||||
? {
|
||||
target: "pino-pretty",
|
||||
options: {
|
||||
colorize: true,
|
||||
translateTime: "yyyy-mm-dd HH:MM:ss",
|
||||
ignore: "pid,hostname,req,res",
|
||||
messageFormat: "{msg}",
|
||||
translateTime: false,
|
||||
singleLine: true,
|
||||
// keep level for coloring but drop other noisy metadata
|
||||
ignore: "pid,req,res,context,name,time",
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 */
|
||||
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;
|
||||
|
||||
@ -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 (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
|
||||
<QueryProvider>
|
||||
<QueryProvider nonce={nonce}>
|
||||
{children}
|
||||
<SessionTimeoutWarning />
|
||||
</QueryProvider>
|
||||
|
||||
@ -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";
|
||||
};
|
||||
|
||||
|
||||
@ -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 (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
{process.env.NODE_ENV === "development" && <ReactQueryDevtools />}
|
||||
{process.env.NODE_ENV === "development" && <ReactQueryDevtools nonce={nonce} />}
|
||||
</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:
|
||||
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
|
||||
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
|
||||
# =============================================================================
|
||||
# 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
|
||||
|
||||
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
|
||||
@ -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
|
||||
|
||||
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
|
||||
# =============================================================================
|
||||
# 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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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