Remove validation package and update Dockerfiles for BFF and Portal
- Deleted the @customer-portal/validation package to streamline dependencies. - Updated Dockerfiles for BFF and Portal to reflect changes in package structure and optimize build processes. - Adjusted import statements in BFF controllers to use the new Zod validation approach. - Enhanced entrypoint script in BFF to include database and cache readiness checks before application startup. - Cleaned up .gitignore to ignore unnecessary files and maintain clarity in project structure.
This commit is contained in:
parent
68561fdf1d
commit
dc9a5d1448
83
.dockerignore
Normal file
83
.dockerignore
Normal file
@ -0,0 +1,83 @@
|
||||
# =============================================================================
|
||||
# Docker Build Ignore - Reduce context size for faster builds
|
||||
# =============================================================================
|
||||
|
||||
# Dependencies (installed fresh in containers)
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
|
||||
# Build outputs (built in container)
|
||||
dist/
|
||||
**/dist/
|
||||
.next/
|
||||
**/.next/
|
||||
.turbo/
|
||||
**/.turbo/
|
||||
|
||||
# Git
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# IDE and editors
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
|
||||
# Environment files (secrets passed via env vars)
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
env/
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Test and coverage
|
||||
coverage/
|
||||
.nyc_output/
|
||||
*.test.ts
|
||||
*.spec.ts
|
||||
**/__tests__/
|
||||
|
||||
# Documentation (not needed in runtime)
|
||||
docs/
|
||||
*.md
|
||||
!README.md
|
||||
|
||||
# Development tools
|
||||
.husky/
|
||||
.eslintcache
|
||||
tsconfig.tsbuildinfo
|
||||
**/tsconfig.tsbuildinfo
|
||||
|
||||
# Docker artifacts (don't send to context)
|
||||
*.tar
|
||||
*.tar.gz
|
||||
*.sha256
|
||||
.build-logs/
|
||||
|
||||
# Secrets (never include)
|
||||
secrets/
|
||||
*.pem
|
||||
*.key
|
||||
*.cert
|
||||
|
||||
# Misc
|
||||
tmp/
|
||||
temp/
|
||||
.cache/
|
||||
Thumbs.db
|
||||
|
||||
# Sim manager migration (one-time tool)
|
||||
sim-manager-migration/
|
||||
|
||||
# Build artifacts already saved
|
||||
portal-frontend.*.tar*
|
||||
portal-backend.*.tar*
|
||||
build-output.log
|
||||
|
||||
@ -1 +0,0 @@
|
||||
CSRF_SECRET_KEY=your-secure-csrf-secret-key-minimum-32-characters-long-for-development
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@ -140,9 +140,9 @@ temp/
|
||||
*.sqlite
|
||||
*.db
|
||||
|
||||
# Docker
|
||||
.dockerignore
|
||||
docker/
|
||||
# Docker - keep configs but ignore secrets
|
||||
docker/portainer/stack.env
|
||||
.build-logs/
|
||||
|
||||
# Prisma
|
||||
prisma/migrations/dev.db*
|
||||
|
||||
@ -1,122 +1,106 @@
|
||||
# 🚀 Backend (BFF) Dockerfile - Production Grade (pnpm v10)
|
||||
# - Uses pnpm's injected workspace packages (no legacy flags)
|
||||
# - pnpm deploy creates minimal production-only install
|
||||
# - Prisma + bcrypt built only for Alpine
|
||||
# - No redundant installs
|
||||
# =============================================================================
|
||||
# Backend (BFF) Dockerfile - Production Grade
|
||||
# =============================================================================
|
||||
# Uses Alpine throughout for consistent native module builds
|
||||
# Uses pnpm prune instead of pnpm deploy to preserve Prisma client
|
||||
# =============================================================================
|
||||
|
||||
# =====================================================
|
||||
# Stage 1: Dependencies (Debian for native builds)
|
||||
# =====================================================
|
||||
FROM node:22-bookworm-slim AS deps
|
||||
ARG PNPM_VERSION=10.15.0
|
||||
ARG NODE_VERSION=22
|
||||
|
||||
RUN apt-get update && apt-get install -y dumb-init ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& corepack enable && corepack prepare pnpm@10.15.0 --activate
|
||||
# =============================================================================
|
||||
# Stage 1: Builder - Install, build, and prune for production
|
||||
# =============================================================================
|
||||
FROM node:${NODE_VERSION}-alpine AS builder
|
||||
|
||||
ARG PNPM_VERSION
|
||||
|
||||
# Install build dependencies for native modules (bcrypt, prisma)
|
||||
RUN apk add --no-cache \
|
||||
python3 make g++ pkgconfig openssl-dev libc6-compat \
|
||||
&& corepack enable \
|
||||
&& corepack prepare pnpm@${PNPM_VERSION} --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy workspace configuration first (better layer caching)
|
||||
COPY .npmrc pnpm-workspace.yaml package.json pnpm-lock.yaml ./
|
||||
COPY packages/domain/package.json ./packages/domain/
|
||||
COPY packages/logging/package.json ./packages/logging/
|
||||
COPY packages/validation/package.json ./packages/validation/
|
||||
COPY apps/bff/package.json ./apps/bff/
|
||||
|
||||
RUN pnpm install --frozen-lockfile --prefer-offline --config.ignore-scripts=false
|
||||
# Install all dependencies
|
||||
ENV HUSKY=0
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# =====================================================
|
||||
# Stage 2: Builder (compile TypeScript)
|
||||
# =====================================================
|
||||
FROM node:22-bookworm-slim AS builder
|
||||
|
||||
RUN apt-get update && apt-get install -y ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& corepack enable && corepack prepare pnpm@10.15.0 --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY .npmrc pnpm-workspace.yaml package.json pnpm-lock.yaml tsconfig.json tsconfig.base.json ./
|
||||
# Copy source code
|
||||
COPY tsconfig.json tsconfig.base.json ./
|
||||
COPY packages/ ./packages/
|
||||
COPY apps/bff/ ./apps/bff/
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
|
||||
# No second pnpm install – reuse deps layer
|
||||
# Build workspace packages
|
||||
RUN pnpm --filter @customer-portal/domain build && \
|
||||
pnpm --filter @customer-portal/logging build
|
||||
|
||||
# Build shared packages
|
||||
RUN pnpm --filter @customer-portal/domain build \
|
||||
&& pnpm --filter @customer-portal/logging build \
|
||||
&& pnpm --filter @customer-portal/validation build
|
||||
# Generate Prisma client (for Alpine/musl)
|
||||
RUN cd apps/bff && pnpm exec prisma generate
|
||||
|
||||
# Build BFF (prisma types generated in dev, not needed here)
|
||||
# Build BFF
|
||||
RUN pnpm --filter @customer-portal/bff build
|
||||
|
||||
# =====================================================
|
||||
# Stage 3: Production Dependencies (Alpine, pnpm deploy)
|
||||
# =====================================================
|
||||
FROM node:22-alpine AS prod-deps
|
||||
# Prune dev dependencies IN PLACE - this keeps .prisma/client intact
|
||||
RUN pnpm prune --prod
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@10.15.0 --activate
|
||||
# Remove unnecessary files to reduce image size
|
||||
RUN rm -rf /app/packages/*/src /app/apps/bff/src \
|
||||
/app/packages/*/*.ts /app/apps/bff/*.ts \
|
||||
/app/**/*.map /app/**/*.tsbuildinfo \
|
||||
/root/.local/share/pnpm/store
|
||||
|
||||
WORKDIR /app
|
||||
# =============================================================================
|
||||
# Stage 2: Production - Minimal runtime image
|
||||
# =============================================================================
|
||||
FROM node:${NODE_VERSION}-alpine AS production
|
||||
|
||||
# Minimal manifests for dependency graph
|
||||
COPY .npmrc pnpm-workspace.yaml package.json pnpm-lock.yaml ./
|
||||
COPY packages/domain/package.json ./packages/domain/
|
||||
COPY packages/logging/package.json ./packages/logging/
|
||||
COPY packages/validation/package.json ./packages/validation/
|
||||
COPY apps/bff/package.json ./apps/bff/
|
||||
COPY apps/bff/prisma ./apps/bff/prisma
|
||||
LABEL org.opencontainers.image.title="Customer Portal BFF" \
|
||||
org.opencontainers.image.description="NestJS Backend-for-Frontend API" \
|
||||
org.opencontainers.image.vendor="Customer Portal"
|
||||
|
||||
ENV HUSKY=0
|
||||
|
||||
RUN apk add --no-cache --virtual .build-deps python3 make g++ pkgconfig openssl-dev \
|
||||
# 1) Install full deps (needed for prisma CLI + bcrypt build)
|
||||
&& pnpm install --frozen-lockfile --ignore-scripts \
|
||||
# 2) Rebuild bcrypt for musl
|
||||
&& pnpm rebuild bcrypt \
|
||||
# 3) Generate Prisma client for Alpine (musl) – the only runtime client
|
||||
&& cd apps/bff && pnpm exec prisma generate && cd ../.. \
|
||||
# 4) Create production-only deployment for BFF
|
||||
&& pnpm deploy --filter @customer-portal/bff --prod /app/deploy \
|
||||
# 5) Remove build-time node_modules and cleanup
|
||||
&& rm -rf /app/node_modules /app/pnpm-lock.yaml \
|
||||
/root/.cache /root/.npm /tmp/* /var/cache/apk/* \
|
||||
&& apk del .build-deps
|
||||
|
||||
# /app/deploy now contains: package.json + node_modules for BFF prod deps only
|
||||
|
||||
# =====================================================
|
||||
# Stage 4: Production Runtime (minimal)
|
||||
# =====================================================
|
||||
FROM node:22-alpine AS production
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs \
|
||||
&& adduser --system --uid 1001 nestjs
|
||||
|
||||
# Only tools needed at runtime
|
||||
RUN apk add --no-cache wget dumb-init openssl netcat-openbsd \
|
||||
# Runtime dependencies only
|
||||
RUN apk add --no-cache \
|
||||
dumb-init wget openssl netcat-openbsd libc6-compat \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup --system --gid 1001 nodejs && \
|
||||
adduser --system --uid 1001 nestjs
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Deploy tree (prod deps for BFF only)
|
||||
COPY --from=prod-deps --chown=nestjs:nodejs /app/deploy ./
|
||||
# Copy pruned node_modules (includes .prisma/client)
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/node_modules ./node_modules
|
||||
|
||||
# Compiled code and prisma schema
|
||||
# Copy workspace package outputs
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/packages/domain/dist ./packages/domain/dist
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/packages/domain/package.json ./packages/domain/package.json
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/packages/logging/dist ./packages/logging/dist
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/packages/validation/dist ./packages/validation/dist
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/packages/logging/package.json ./packages/logging/package.json
|
||||
|
||||
# Copy BFF
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/apps/bff/dist ./apps/bff/dist
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/apps/bff/prisma ./apps/bff/prisma
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/apps/bff/package.json ./apps/bff/package.json
|
||||
|
||||
# Entrypoint and runtime dirs
|
||||
# Copy entrypoint
|
||||
COPY --chown=nestjs:nodejs apps/bff/scripts/docker-entrypoint.sh /app/docker-entrypoint.sh
|
||||
RUN chmod +x /app/docker-entrypoint.sh \
|
||||
&& mkdir -p /app/secrets /app/logs \
|
||||
&& chown nestjs:nodejs /app/secrets /app/logs
|
||||
RUN chmod +x /app/docker-entrypoint.sh && \
|
||||
mkdir -p /app/secrets /app/logs && \
|
||||
chown nestjs:nodejs /app/secrets /app/logs
|
||||
|
||||
USER nestjs
|
||||
|
||||
EXPOSE 4000
|
||||
|
||||
ENV NODE_ENV=production PORT=4000
|
||||
|
||||
WORKDIR /app/apps/bff
|
||||
|
||||
@ -33,7 +33,6 @@
|
||||
"dependencies": {
|
||||
"@customer-portal/domain": "workspace:*",
|
||||
"@customer-portal/logging": "workspace:*",
|
||||
"@customer-portal/validation": "workspace:*",
|
||||
"@nestjs/bullmq": "^11.0.3",
|
||||
"@nestjs/common": "^11.1.6",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
|
||||
@ -5,8 +5,9 @@ set -e
|
||||
# Docker Entrypoint Script
|
||||
# =============================================================================
|
||||
# Handles runtime setup before starting the application:
|
||||
# - Waits for database and cache dependencies
|
||||
# - Decodes SF_PRIVATE_KEY_BASE64 to file if provided
|
||||
# - Runs Prisma migrations if DATABASE_URL is set
|
||||
# - Runs Prisma migrations if RUN_MIGRATIONS=true
|
||||
# =============================================================================
|
||||
|
||||
echo "🚀 Starting Customer Portal Backend..."
|
||||
@ -21,6 +22,34 @@ if [ -n "$SF_PRIVATE_KEY_BASE64" ]; then
|
||||
echo "✅ Salesforce private key configured"
|
||||
fi
|
||||
|
||||
# Wait for database if DATABASE_URL is set
|
||||
# Extract host:port from postgresql://user:pass@host:port/db
|
||||
if [ -n "$DATABASE_URL" ]; then
|
||||
DB_HOST=$(echo "$DATABASE_URL" | sed -E 's|.*@([^:/]+):([0-9]+)/.*|\1|')
|
||||
DB_PORT=$(echo "$DATABASE_URL" | sed -E 's|.*@([^:/]+):([0-9]+)/.*|\2|')
|
||||
if [ -n "$DB_HOST" ] && [ -n "$DB_PORT" ]; then
|
||||
echo "⏳ Waiting for database ($DB_HOST:$DB_PORT)..."
|
||||
until nc -z "$DB_HOST" "$DB_PORT" 2>/dev/null; do
|
||||
sleep 2
|
||||
done
|
||||
echo "✅ Database is ready"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Wait for Redis if REDIS_URL is set
|
||||
# Extract host:port from redis://host:port/db
|
||||
if [ -n "$REDIS_URL" ]; then
|
||||
REDIS_HOST=$(echo "$REDIS_URL" | sed -E 's|redis://([^:/]+):([0-9]+).*|\1|')
|
||||
REDIS_PORT=$(echo "$REDIS_URL" | sed -E 's|redis://([^:/]+):([0-9]+).*|\2|')
|
||||
if [ -n "$REDIS_HOST" ] && [ -n "$REDIS_PORT" ]; then
|
||||
echo "⏳ Waiting for cache ($REDIS_HOST:$REDIS_PORT)..."
|
||||
until nc -z "$REDIS_HOST" "$REDIS_PORT" 2>/dev/null; do
|
||||
sleep 2
|
||||
done
|
||||
echo "✅ Cache is ready"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run database migrations if enabled
|
||||
if [ "$RUN_MIGRATIONS" = "true" ] && [ -n "$DATABASE_URL" ]; then
|
||||
echo "🗄️ Running database migrations..."
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { APP_FILTER, APP_PIPE } from "@nestjs/core";
|
||||
import { APP_PIPE } from "@nestjs/core";
|
||||
import { RouterModule } from "@nestjs/core";
|
||||
import { ConfigModule, ConfigService } from "@nestjs/config";
|
||||
import { ThrottlerModule } from "@nestjs/throttler";
|
||||
import { ZodValidationPipe } from "nestjs-zod";
|
||||
import { ZodValidationExceptionFilter } from "@customer-portal/validation/nestjs";
|
||||
|
||||
// Configuration
|
||||
import { appConfig } from "@bff/core/config/app.config";
|
||||
@ -99,10 +98,6 @@ import { HealthModule } from "@bff/modules/health/health.module";
|
||||
provide: APP_PIPE,
|
||||
useClass: ZodValidationPipe,
|
||||
},
|
||||
{
|
||||
provide: APP_FILTER,
|
||||
useClass: ZodValidationExceptionFilter,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@ -22,7 +22,7 @@ import {
|
||||
} from "./guards/failed-login-throttle.guard";
|
||||
import { LoginResultInterceptor } from "./interceptors/login-result.interceptor";
|
||||
import { Public } from "../../decorators/public.decorator";
|
||||
import { ZodValidationPipe } from "@customer-portal/validation/nestjs";
|
||||
import { ZodValidationPipe } from "nestjs-zod";
|
||||
import type { RequestWithUser } from "@bff/modules/auth/auth.types";
|
||||
import { SalesforceReadThrottleGuard } from "@bff/integrations/salesforce/guards/salesforce-read-throttle.guard";
|
||||
import { SalesforceWriteThrottleGuard } from "@bff/integrations/salesforce/guards/salesforce-write-throttle.guard";
|
||||
|
||||
@ -13,7 +13,7 @@ import {
|
||||
import { InvoicesOrchestratorService } from "./services/invoices-orchestrator.service";
|
||||
import { WhmcsService } from "@bff/integrations/whmcs/whmcs.service";
|
||||
import { MappingsService } from "@bff/modules/id-mappings/mappings.service";
|
||||
import { ZodValidationPipe } from "@customer-portal/validation/nestjs";
|
||||
import { ZodValidationPipe } from "nestjs-zod";
|
||||
import type { RequestWithUser } from "@bff/modules/auth/auth.types";
|
||||
|
||||
import type {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Body, Controller, Post, Request, UsePipes, Inject, UseGuards } from "@nestjs/common";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { ZodValidationPipe } from "@customer-portal/validation/nestjs";
|
||||
import { ZodValidationPipe } from "nestjs-zod";
|
||||
import { CheckoutService } from "../services/checkout.service";
|
||||
import {
|
||||
CheckoutCart,
|
||||
|
||||
@ -15,7 +15,7 @@ import { Throttle, ThrottlerGuard } from "@nestjs/throttler";
|
||||
import { OrderOrchestrator } from "./services/order-orchestrator.service";
|
||||
import type { RequestWithUser } from "@bff/modules/auth/auth.types";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { ZodValidationPipe } from "@customer-portal/validation/nestjs";
|
||||
import { ZodValidationPipe } from "nestjs-zod";
|
||||
import {
|
||||
createOrderRequestSchema,
|
||||
orderCreateResponseSchema,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Body, Controller, Post, Request, UsePipes, Headers } from "@nestjs/common";
|
||||
import type { RequestWithUser } from "@bff/modules/auth/auth.types";
|
||||
import { SimOrderActivationService } from "./sim-order-activation.service";
|
||||
import { ZodValidationPipe } from "@customer-portal/validation/nestjs";
|
||||
import { ZodValidationPipe } from "nestjs-zod";
|
||||
import {
|
||||
simOrderActivationRequestSchema,
|
||||
type SimOrderActivationRequest,
|
||||
|
||||
@ -42,7 +42,7 @@ import {
|
||||
type SimCancelFullRequest,
|
||||
type SimChangePlanFullRequest,
|
||||
} from "@customer-portal/domain/sim";
|
||||
import { ZodValidationPipe } from "@customer-portal/validation/nestjs";
|
||||
import { ZodValidationPipe } from "nestjs-zod";
|
||||
import type { RequestWithUser } from "@bff/modules/auth/auth.types";
|
||||
import { SimPlanService } from "./sim-management/services/sim-plan.service";
|
||||
import { SimCancellationService } from "./sim-management/services/sim-cancellation.service";
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Controller, Get, Post, Query, Param, Body, Request } from "@nestjs/common";
|
||||
import { SupportService } from "./support.service";
|
||||
import { ZodValidationPipe } from "@customer-portal/validation/nestjs";
|
||||
import { ZodValidationPipe } from "nestjs-zod";
|
||||
import {
|
||||
supportCaseFilterSchema,
|
||||
createCaseRequestSchema,
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import { UsersFacade } from "./application/users.facade";
|
||||
import { ZodValidationPipe } from "@customer-portal/validation/nestjs";
|
||||
import { ZodValidationPipe } from "nestjs-zod";
|
||||
import {
|
||||
updateCustomerProfileRequestSchema,
|
||||
type UpdateCustomerProfileRequest,
|
||||
|
||||
@ -1,48 +1,43 @@
|
||||
# 🚀 Frontend (Portal) Dockerfile - Plesk Optimized
|
||||
# Multi-stage build for Next.js production deployment via Plesk
|
||||
# =============================================================================
|
||||
# Frontend (Portal) Dockerfile - Production Grade
|
||||
# =============================================================================
|
||||
# Uses Alpine throughout for consistency
|
||||
# Next.js standalone output for minimal production image
|
||||
# =============================================================================
|
||||
|
||||
# =====================================================
|
||||
# Stage 1: Dependencies - Install all dependencies
|
||||
# =====================================================
|
||||
FROM node:22-alpine AS deps
|
||||
ARG PNPM_VERSION=10.15.0
|
||||
ARG NODE_VERSION=22
|
||||
|
||||
RUN apk add --no-cache libc6-compat dumb-init \
|
||||
&& corepack enable && corepack prepare pnpm@10.15.0 --activate
|
||||
# =============================================================================
|
||||
# Stage 1: Builder - Install dependencies and build Next.js
|
||||
# =============================================================================
|
||||
FROM node:${NODE_VERSION}-alpine AS builder
|
||||
|
||||
ARG PNPM_VERSION
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache libc6-compat \
|
||||
&& corepack enable \
|
||||
&& corepack prepare pnpm@${PNPM_VERSION} --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy workspace configuration
|
||||
# Copy workspace configuration first (better layer caching)
|
||||
COPY .npmrc pnpm-workspace.yaml package.json pnpm-lock.yaml ./
|
||||
COPY packages/domain/package.json ./packages/domain/
|
||||
COPY packages/validation/package.json ./packages/validation/
|
||||
COPY apps/portal/package.json ./apps/portal/
|
||||
|
||||
# Install all dependencies with scripts enabled (esbuild, sharp, etc.)
|
||||
RUN pnpm install --frozen-lockfile --prefer-offline --config.ignore-scripts=false
|
||||
# Install all dependencies
|
||||
ENV HUSKY=0
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# =====================================================
|
||||
# Stage 2: Builder - Compile and build Next.js
|
||||
# =====================================================
|
||||
FROM node:22-alpine AS builder
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@10.15.0 --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy workspace configuration and source
|
||||
COPY .npmrc pnpm-workspace.yaml package.json pnpm-lock.yaml tsconfig.json tsconfig.base.json ./
|
||||
# Copy source code
|
||||
COPY tsconfig.json tsconfig.base.json ./
|
||||
COPY packages/ ./packages/
|
||||
COPY apps/portal/ ./apps/portal/
|
||||
|
||||
# Ensure public directory exists
|
||||
RUN mkdir -p /app/apps/portal/public
|
||||
|
||||
# Copy pre-installed node_modules from deps
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
|
||||
# Build shared packages
|
||||
RUN pnpm --filter @customer-portal/domain build && \
|
||||
pnpm --filter @customer-portal/validation build
|
||||
# Build domain package
|
||||
RUN pnpm --filter @customer-portal/domain build
|
||||
|
||||
# Build-time environment variables (baked into Next.js client bundle)
|
||||
ARG NEXT_PUBLIC_API_BASE=/api
|
||||
@ -54,24 +49,33 @@ ENV NODE_ENV=production \
|
||||
NEXT_PUBLIC_APP_NAME=${NEXT_PUBLIC_APP_NAME} \
|
||||
NEXT_PUBLIC_APP_VERSION=${NEXT_PUBLIC_APP_VERSION}
|
||||
|
||||
WORKDIR /app/apps/portal
|
||||
RUN pnpm build
|
||||
# Build Next.js (creates standalone output)
|
||||
RUN pnpm --filter @customer-portal/portal build
|
||||
|
||||
# =====================================================
|
||||
# Stage 3: Production - Minimal Alpine runtime image
|
||||
# =====================================================
|
||||
FROM node:22-alpine AS production
|
||||
# =============================================================================
|
||||
# Stage 2: Production - Minimal runtime image
|
||||
# =============================================================================
|
||||
FROM node:${NODE_VERSION}-alpine AS production
|
||||
|
||||
RUN apk add --no-cache wget curl dumb-init libc6-compat \
|
||||
LABEL org.opencontainers.image.title="Customer Portal Frontend" \
|
||||
org.opencontainers.image.description="Customer Portal Application" \
|
||||
org.opencontainers.image.vendor="Customer Portal"
|
||||
|
||||
# Runtime dependencies only
|
||||
RUN apk add --no-cache \
|
||||
dumb-init \
|
||||
wget \
|
||||
curl \
|
||||
libc6-compat \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup --system --gid 1001 nodejs && \
|
||||
adduser --system --uid 1001 nextjs
|
||||
|
||||
# Copy Next.js standalone build
|
||||
WORKDIR /app
|
||||
|
||||
# Copy Next.js standalone build (includes all bundled dependencies)
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/portal/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/portal/.next/static ./apps/portal/.next/static
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/portal/public ./apps/portal/public
|
||||
|
||||
@ -19,7 +19,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@customer-portal/domain": "workspace:*",
|
||||
"@customer-portal/validation": "workspace:*",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@hookform/resolvers": "^5.2.1",
|
||||
"@tanstack/react-query": "^5.85.5",
|
||||
|
||||
@ -7,7 +7,7 @@ import {
|
||||
addressFormToRequest,
|
||||
type AddressFormData,
|
||||
} from "@customer-portal/domain/customer";
|
||||
import { useZodForm } from "@customer-portal/validation";
|
||||
import { useZodForm } from "@/hooks/useZodForm";
|
||||
|
||||
export function useAddressEdit(initial: AddressFormData) {
|
||||
const handleSave = useCallback(async (formData: AddressFormData) => {
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
type ProfileEditFormData,
|
||||
} from "@customer-portal/domain/customer";
|
||||
import { type UpdateCustomerProfileRequest } from "@customer-portal/domain/auth";
|
||||
import { useZodForm } from "@customer-portal/validation";
|
||||
import { useZodForm } from "@/hooks/useZodForm";
|
||||
|
||||
export function useProfileEdit(initial: ProfileEditFormData) {
|
||||
const handleSave = useCallback(async (formData: ProfileEditFormData) => {
|
||||
|
||||
@ -9,7 +9,7 @@ import { Button, Input, ErrorMessage } from "@/components/atoms";
|
||||
import { FormField } from "@/components/molecules/FormField/FormField";
|
||||
import { useWhmcsLink } from "@/features/auth/hooks";
|
||||
import { linkWhmcsRequestSchema, type LinkWhmcsResponse } from "@customer-portal/domain/auth";
|
||||
import { useZodForm } from "@customer-portal/validation";
|
||||
import { useZodForm } from "@/hooks/useZodForm";
|
||||
|
||||
interface LinkWhmcsFormProps {
|
||||
onTransferred?: (result: LinkWhmcsResponse) => void;
|
||||
|
||||
@ -11,7 +11,7 @@ import { Button, Input, ErrorMessage } from "@/components/atoms";
|
||||
import { FormField } from "@/components/molecules/FormField/FormField";
|
||||
import { useLogin } from "../../hooks/use-auth";
|
||||
import { loginRequestSchema } from "@customer-portal/domain/auth";
|
||||
import { useZodForm } from "@customer-portal/validation";
|
||||
import { useZodForm } from "@/hooks/useZodForm";
|
||||
import { z } from "zod";
|
||||
|
||||
interface LoginFormProps {
|
||||
|
||||
@ -10,7 +10,7 @@ import Link from "next/link";
|
||||
import { Button, Input, ErrorMessage } from "@/components/atoms";
|
||||
import { FormField } from "@/components/molecules/FormField/FormField";
|
||||
import { usePasswordReset } from "../../hooks/use-auth";
|
||||
import { useZodForm } from "@customer-portal/validation";
|
||||
import { useZodForm } from "@/hooks/useZodForm";
|
||||
import { passwordResetRequestSchema, passwordResetSchema } from "@customer-portal/domain/auth";
|
||||
import { z } from "zod";
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import Link from "next/link";
|
||||
import { Button, Input, ErrorMessage } from "@/components/atoms";
|
||||
import { FormField } from "@/components/molecules/FormField/FormField";
|
||||
import { useWhmcsLink } from "../../hooks/use-auth";
|
||||
import { useZodForm } from "@customer-portal/validation";
|
||||
import { useZodForm } from "@/hooks/useZodForm";
|
||||
import {
|
||||
setPasswordRequestSchema,
|
||||
checkPasswordStrength,
|
||||
|
||||
@ -13,7 +13,7 @@ import {
|
||||
buildSignupRequest,
|
||||
} from "@customer-portal/domain/auth";
|
||||
import { addressFormSchema } from "@customer-portal/domain/customer";
|
||||
import { useZodForm } from "@customer-portal/validation";
|
||||
import { useZodForm } from "@/hooks/useZodForm";
|
||||
import { z } from "zod";
|
||||
|
||||
import { MultiStepForm } from "./MultiStepForm";
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import { useEffect } from "react";
|
||||
import { MapPinIcon, ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||
import { COUNTRY_OPTIONS, getCountryCodeByName } from "@/lib/constants/countries";
|
||||
import { useZodForm } from "@customer-portal/validation";
|
||||
import { useZodForm } from "@/hooks/useZodForm";
|
||||
import {
|
||||
addressFormSchema,
|
||||
type AddressFormData,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Framework-agnostic Zod form utilities for React environments.
|
||||
* Provides predictable error and touched state handling.
|
||||
* Zod form utilities for React
|
||||
* Provides predictable error and touched state handling for forms
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
@ -215,8 +215,6 @@ export function useZodForm<TValues extends Record<string, unknown>>({
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setSubmitError(message);
|
||||
setErrors(prev => ({ ...prev, _form: message }));
|
||||
// Errors are captured in state so we avoid rethrowing to prevent unhandled rejections in callers
|
||||
// Note: Logging should be handled by the consuming application
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
@ -250,3 +248,4 @@ export function useZodForm<TValues extends Record<string, unknown>>({
|
||||
reset,
|
||||
};
|
||||
}
|
||||
|
||||
42
docker/dev/docker-compose.yml
Normal file
42
docker/dev/docker-compose.yml
Normal file
@ -0,0 +1,42 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
container_name: portal_dev_postgres
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-dev}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-dev}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-portal_dev}
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-dev} -d ${POSTGRES_DB:-portal_dev}"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
restart: unless-stopped
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: portal_dev_redis
|
||||
command: ["redis-server", "--save", "20", "1", "--loglevel", "warning"]
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: portal_dev
|
||||
|
||||
452
docker/portainer/PORTAINER-GUIDE.md
Normal file
452
docker/portainer/PORTAINER-GUIDE.md
Normal file
@ -0,0 +1,452 @@
|
||||
# Complete Portainer Guide for Customer Portal
|
||||
|
||||
## Table of Contents
|
||||
1. [Creating a Stack in Portainer](#creating-a-stack-in-portainer)
|
||||
2. [Repository vs Upload vs Web Editor](#stack-creation-methods)
|
||||
3. [Security Concerns & Best Practices](#security-concerns)
|
||||
4. [Auto-Updating Images](#auto-updating-images)
|
||||
5. [Recommended Setup for Production](#recommended-production-setup)
|
||||
|
||||
---
|
||||
|
||||
## Creating a Stack in Portainer
|
||||
|
||||
### Step 1: Access Portainer
|
||||
|
||||
1. Open Portainer UI (typically at `https://your-server:9443` or via Plesk)
|
||||
2. Select your environment (usually "local" for Plesk)
|
||||
3. Go to **Stacks** in the left sidebar
|
||||
|
||||
### Step 2: Create New Stack
|
||||
|
||||
Click **"+ Add stack"** button
|
||||
|
||||
You'll see three creation methods:
|
||||
- **Web editor** - Paste compose file directly
|
||||
- **Upload** - Upload a compose file
|
||||
- **Repository** - Pull from Git repository
|
||||
|
||||
### Step 3: Configure the Stack
|
||||
|
||||
**Name:** `customer-portal` (lowercase, no spaces)
|
||||
|
||||
**Compose content:** Use one of the methods below
|
||||
|
||||
**Environment variables:** Add your configuration
|
||||
|
||||
### Step 4: Deploy
|
||||
|
||||
Click **"Deploy the stack"**
|
||||
|
||||
---
|
||||
|
||||
## Stack Creation Methods
|
||||
|
||||
### Method 1: Web Editor (Simplest)
|
||||
|
||||
**How:**
|
||||
1. Select "Web editor"
|
||||
2. Paste your `docker-compose.yml` content
|
||||
3. Add environment variables manually or load from file
|
||||
|
||||
**Pros:**
|
||||
- ✅ Quick and simple
|
||||
- ✅ No external dependencies
|
||||
- ✅ Full control over content
|
||||
|
||||
**Cons:**
|
||||
- ❌ Manual updates required
|
||||
- ❌ No version control
|
||||
- ❌ Easy to make mistakes when editing
|
||||
|
||||
**Best for:** Quick testing, simple deployments
|
||||
|
||||
---
|
||||
|
||||
### Method 2: Upload (Recommended for Your Case)
|
||||
|
||||
**How:**
|
||||
1. Select "Upload"
|
||||
2. Upload your `docker-compose.yml` file
|
||||
3. Optionally upload a `.env` file for environment variables
|
||||
|
||||
**Pros:**
|
||||
- ✅ Version control on your local machine
|
||||
- ✅ Can prepare and test locally
|
||||
- ✅ No external network dependencies
|
||||
- ✅ Works in air-gapped environments
|
||||
|
||||
**Cons:**
|
||||
- ❌ Manual upload for each update
|
||||
- ❌ Need to manage files locally
|
||||
|
||||
**Best for:** Production deployments with manual control
|
||||
|
||||
---
|
||||
|
||||
### Method 3: Repository (Git Integration)
|
||||
|
||||
**How:**
|
||||
1. Select "Repository"
|
||||
2. Enter repository URL (GitHub, GitLab, Bitbucket, etc.)
|
||||
3. Specify branch and compose file path
|
||||
4. Add authentication if private repo
|
||||
|
||||
**Example Configuration:**
|
||||
```
|
||||
Repository URL: https://github.com/your-org/customer-portal
|
||||
Reference: main
|
||||
Compose path: docker/portainer/docker-compose.yml
|
||||
```
|
||||
|
||||
**For Private Repos:**
|
||||
- Use a Personal Access Token (PAT) as password
|
||||
- Or use deploy keys
|
||||
|
||||
**Pros:**
|
||||
- ✅ Version controlled
|
||||
- ✅ Easy to update (just click "Pull and redeploy")
|
||||
- ✅ Team can review changes via PR
|
||||
- ✅ Audit trail of changes
|
||||
|
||||
**Cons:**
|
||||
- ❌ Requires network access to repo
|
||||
- ❌ Secrets in repo = security risk
|
||||
- ❌ Need to manage repo access tokens
|
||||
- ❌ Compose file changes require git push
|
||||
|
||||
**Best for:** Teams, CI/CD pipelines, frequent updates
|
||||
|
||||
---
|
||||
|
||||
### 📌 My Recommendation for Your Case
|
||||
|
||||
**Use: Upload + Environment Variables in Portainer UI**
|
||||
|
||||
Why:
|
||||
1. Your compose file rarely changes (it's just orchestration)
|
||||
2. Sensitive data stays in Portainer, not in Git
|
||||
3. Image updates are done via environment variables
|
||||
4. No external dependencies during deployment
|
||||
|
||||
---
|
||||
|
||||
## Security Concerns
|
||||
|
||||
### 🔴 Critical Security Issues
|
||||
|
||||
#### 1. Never Store Secrets in Git
|
||||
```yaml
|
||||
# ❌ BAD - Secrets in compose file
|
||||
environment:
|
||||
JWT_SECRET: "my-actual-secret-here"
|
||||
DATABASE_URL: "postgresql://user:password@db/prod"
|
||||
|
||||
# ✅ GOOD - Use environment variables
|
||||
environment:
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
DATABASE_URL: ${DATABASE_URL}
|
||||
```
|
||||
|
||||
#### 2. Never Store Secrets in Docker Images
|
||||
```dockerfile
|
||||
# ❌ BAD - Secrets baked into image
|
||||
ENV JWT_SECRET="my-secret"
|
||||
COPY secrets/ /app/secrets/
|
||||
|
||||
# ✅ GOOD - Mount at runtime
|
||||
# (secrets passed via env vars or volume mounts)
|
||||
```
|
||||
|
||||
#### 3. Portainer Access Control
|
||||
```
|
||||
⚠️ Portainer has full Docker access = root on the host
|
||||
|
||||
Best practices:
|
||||
- Use strong passwords
|
||||
- Enable 2FA if available
|
||||
- Restrict network access to Portainer UI
|
||||
- Use HTTPS only
|
||||
- Create separate users with limited permissions
|
||||
```
|
||||
|
||||
### 🟡 Medium Security Concerns
|
||||
|
||||
#### 4. Environment Variables in Portainer
|
||||
```
|
||||
Portainer stores env vars in its database.
|
||||
This is generally safe, but consider:
|
||||
|
||||
- Portainer database is at /data/portainer.db
|
||||
- Anyone with Portainer admin = sees all secrets
|
||||
- Backup files may contain secrets
|
||||
|
||||
Mitigation:
|
||||
- Limit Portainer admin access
|
||||
- Use Docker secrets for highly sensitive data
|
||||
- Encrypt backups
|
||||
```
|
||||
|
||||
#### 5. Image Trust
|
||||
```
|
||||
⚠️ You're loading .tar files - verify their integrity
|
||||
|
||||
Best practice:
|
||||
- Generate checksums when building
|
||||
- Verify checksums before loading
|
||||
- Use signed images if possible
|
||||
```
|
||||
|
||||
Add to build script:
|
||||
```bash
|
||||
# Generate checksums
|
||||
sha256sum portal-frontend.latest.tar > portal-frontend.latest.tar.sha256
|
||||
sha256sum portal-backend.latest.tar > portal-backend.latest.tar.sha256
|
||||
|
||||
# Verify on server
|
||||
sha256sum -c portal-frontend.latest.tar.sha256
|
||||
sha256sum -c portal-backend.latest.tar.sha256
|
||||
```
|
||||
|
||||
#### 6. Network Exposure
|
||||
```yaml
|
||||
# ❌ BAD - Database exposed to host
|
||||
database:
|
||||
ports:
|
||||
- "5432:5432" # Accessible from outside!
|
||||
|
||||
# ✅ GOOD - Internal network only
|
||||
database:
|
||||
# No ports exposed - only accessible via portal-network
|
||||
networks:
|
||||
- portal-network
|
||||
```
|
||||
|
||||
### 🟢 Good Security Practices (Already in Place)
|
||||
|
||||
Your current setup does these right:
|
||||
- ✅ Non-root users in containers
|
||||
- ✅ Health checks configured
|
||||
- ✅ Database/Redis not exposed externally
|
||||
- ✅ Secrets mounted as read-only volumes
|
||||
- ✅ Production error messages hide sensitive info
|
||||
|
||||
---
|
||||
|
||||
## Auto-Updating Images
|
||||
|
||||
### Option 1: Watchtower (NOT Recommended for Production)
|
||||
|
||||
Watchtower automatically updates containers when new images are available.
|
||||
|
||||
```yaml
|
||||
# Add to your stack (if using registry)
|
||||
watchtower:
|
||||
image: containrrr/watchtower
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
- WATCHTOWER_POLL_INTERVAL=300
|
||||
- WATCHTOWER_CLEANUP=true
|
||||
command: --include-stopped portal-frontend portal-backend
|
||||
```
|
||||
|
||||
**Why NOT recommended:**
|
||||
- ❌ No control over when updates happen
|
||||
- ❌ No rollback mechanism
|
||||
- ❌ Can break production unexpectedly
|
||||
- ❌ Requires images in a registry (not .tar files)
|
||||
|
||||
We've disabled Watchtower in your compose:
|
||||
```yaml
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=false"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Option 2: Portainer Webhooks (Semi-Automatic)
|
||||
|
||||
Portainer can expose a webhook URL that triggers stack redeployment.
|
||||
|
||||
**Setup:**
|
||||
1. Go to Stack → Settings
|
||||
2. Enable "Webhook"
|
||||
3. Copy the webhook URL
|
||||
|
||||
**Trigger from CI/CD:**
|
||||
```bash
|
||||
# In your GitHub Actions / GitLab CI
|
||||
curl -X POST "https://your-portainer:9443/api/stacks/webhook/abc123"
|
||||
```
|
||||
|
||||
**Workflow:**
|
||||
```
|
||||
Build Images → Push to Registry → Trigger Webhook → Portainer Redeploys
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- ✅ Controlled updates
|
||||
- ✅ Integrated with CI/CD
|
||||
- ✅ Can add approval gates
|
||||
|
||||
**Cons:**
|
||||
- ❌ Requires images in a registry
|
||||
- ❌ Webhook URL is a secret
|
||||
- ❌ Limited rollback options
|
||||
|
||||
---
|
||||
|
||||
### Option 3: Manual Script (Recommended for Your Case) ✅
|
||||
|
||||
Since you're using `.tar` files (no registry), a manual update script is best:
|
||||
|
||||
```bash
|
||||
# On your local machine after building:
|
||||
./scripts/plesk/build-images.sh --tag v1.2.3
|
||||
|
||||
# Upload to server
|
||||
scp portal-*.v1.2.3.tar user@server:/path/to/images/
|
||||
|
||||
# SSH and run update
|
||||
ssh user@server "cd /path/to/portal && ./update-stack.sh v1.2.3"
|
||||
```
|
||||
|
||||
**Make it a one-liner:**
|
||||
```bash
|
||||
# deploy.sh - Run locally
|
||||
#!/bin/bash
|
||||
TAG=$1
|
||||
SERVER="user@your-server"
|
||||
REMOTE_PATH="/var/www/vhosts/domain/portal"
|
||||
|
||||
# Build
|
||||
./scripts/plesk/build-images.sh --tag "$TAG"
|
||||
|
||||
# Upload
|
||||
scp portal-frontend.${TAG}.tar portal-backend.${TAG}.tar ${SERVER}:${REMOTE_PATH}/images/
|
||||
|
||||
# Deploy
|
||||
ssh $SERVER "cd ${REMOTE_PATH} && ./update-stack.sh ${TAG}"
|
||||
|
||||
echo "✅ Deployed ${TAG}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Option 4: Use a Container Registry (Most Professional)
|
||||
|
||||
If you want auto-updates, use a registry:
|
||||
|
||||
**Free Options:**
|
||||
- GitHub Container Registry (ghcr.io) - free for public repos
|
||||
- GitLab Container Registry - free
|
||||
- Docker Hub - 1 private repo free
|
||||
|
||||
**Setup:**
|
||||
```bash
|
||||
# Build and push
|
||||
./scripts/plesk/build-images.sh --tag v1.2.3 --push ghcr.io/your-org
|
||||
|
||||
# Update compose to use registry
|
||||
services:
|
||||
frontend:
|
||||
image: ghcr.io/your-org/portal-frontend:${TAG:-latest}
|
||||
```
|
||||
|
||||
**Then use Watchtower or webhooks for auto-updates.**
|
||||
|
||||
---
|
||||
|
||||
## Recommended Production Setup
|
||||
|
||||
### For Your Current Situation (No Registry)
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
||||
│ Local Dev │ │ Plesk Server │ │ Portainer │
|
||||
│ │ │ │ │ │
|
||||
│ 1. Build images │───▶│ 2. Load .tar │───▶│ 3. Update stack │
|
||||
│ with tag │ │ files │ │ env vars │
|
||||
│ │ │ │ │ │
|
||||
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
||||
▲ │
|
||||
│ ▼
|
||||
└──────────────────────────────────────────────┘
|
||||
4. Verify & rollback if needed
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
1. Build: `./scripts/plesk/build-images.sh --tag 20241201-abc`
|
||||
2. Upload: `scp *.tar server:/path/images/`
|
||||
3. Load: `docker load -i *.tar`
|
||||
4. Update: Change `FRONTEND_IMAGE` and `BACKEND_IMAGE` in Portainer
|
||||
5. Redeploy: Click "Update the stack" in Portainer
|
||||
|
||||
### For Future (With Registry)
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
||||
│ GitHub │ │ GitHub │ │ Portainer │
|
||||
│ (Code) │───▶│ Actions │───▶│ (Webhook) │
|
||||
│ │ │ (Build & Push) │ │ │
|
||||
└─────────────────┘ └────────┬─────────┘ └────────┬────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌────────────────┐ ┌────────────────┐
|
||||
│ ghcr.io │ │ Plesk Server │
|
||||
│ (Registry) │◀─────│ (Pull Image) │
|
||||
└────────────────┘ └────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference: Portainer Stack Commands
|
||||
|
||||
### Via Portainer UI
|
||||
| Action | Steps |
|
||||
|--------|-------|
|
||||
| Create stack | Stacks → Add stack → Configure → Deploy |
|
||||
| Update stack | Stacks → Select → Editor → Update |
|
||||
| Change image | Stacks → Select → Env vars → Change IMAGE → Update |
|
||||
| View logs | Stacks → Select → Container → Logs |
|
||||
| Restart | Stacks → Select → Container → Restart |
|
||||
| Stop | Stacks → Select → Stop |
|
||||
| Delete | Stacks → Select → Delete |
|
||||
|
||||
### Via CLI (on server)
|
||||
```bash
|
||||
# Navigate to stack directory
|
||||
cd /path/to/portal
|
||||
|
||||
# View status
|
||||
docker compose --env-file stack.env ps
|
||||
|
||||
# View logs
|
||||
docker compose --env-file stack.env logs -f
|
||||
|
||||
# Restart
|
||||
docker compose --env-file stack.env restart
|
||||
|
||||
# Update (after changing stack.env)
|
||||
docker compose --env-file stack.env up -d
|
||||
|
||||
# Stop
|
||||
docker compose --env-file stack.env down
|
||||
|
||||
# Stop and remove volumes (⚠️ DATA LOSS)
|
||||
docker compose --env-file stack.env down -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Aspect | Recommendation |
|
||||
|--------|---------------|
|
||||
| Stack creation | **Upload** method (version control locally, no secrets in git) |
|
||||
| Secrets management | **Portainer env vars** or **mounted secrets volume** |
|
||||
| Image updates | **Manual script** for now, migrate to **registry + webhook** later |
|
||||
| Auto-updates | **Not recommended** for production; use controlled deployments |
|
||||
| Rollback | Keep previous image tags, update env vars to rollback |
|
||||
|
||||
126
docker/portainer/README.md
Normal file
126
docker/portainer/README.md
Normal file
@ -0,0 +1,126 @@
|
||||
# Customer Portal - Portainer Deployment
|
||||
|
||||
## Quick Setup
|
||||
|
||||
### 1. Load Docker Images
|
||||
|
||||
Upload your images to the server and load them:
|
||||
|
||||
```bash
|
||||
docker load < portal-frontend-latest.tar
|
||||
docker load < portal-backend-latest.tar
|
||||
```
|
||||
|
||||
Verify they're loaded:
|
||||
|
||||
```bash
|
||||
docker images | grep portal
|
||||
```
|
||||
|
||||
### 2. Create Stack in Portainer
|
||||
|
||||
1. Go to Portainer → Stacks → Add Stack
|
||||
2. Name: `customer-portal`
|
||||
3. Paste contents of `docker-compose.yml`
|
||||
4. Scroll down to **Environment Variables**
|
||||
5. Copy variables from `stack.env.example` and fill in your values
|
||||
|
||||
### 3. Generate Secrets
|
||||
|
||||
Generate secure random values for:
|
||||
|
||||
```bash
|
||||
# JWT Secret
|
||||
openssl rand -base64 32
|
||||
|
||||
# CSRF Secret
|
||||
openssl rand -base64 32
|
||||
|
||||
# PostgreSQL Password
|
||||
openssl rand -base64 24 | tr -d '/+=' | cut -c1-32
|
||||
```
|
||||
|
||||
Encode your Salesforce private key:
|
||||
|
||||
```bash
|
||||
base64 -w 0 < sf-private.key
|
||||
```
|
||||
|
||||
### 4. Deploy
|
||||
|
||||
Click **Deploy the stack** in Portainer.
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
This setup uses `network_mode: bridge` with Docker `links` to avoid the iptables/IPv6 issues that can occur on some servers.
|
||||
|
||||
### Key Points
|
||||
|
||||
- **No custom networks** - Uses Docker's default bridge network
|
||||
- **Service discovery via links** - Services reference each other by container name
|
||||
- **Localhost binding** - Ports are bound to `127.0.0.1` for security (use Nginx/Plesk to proxy)
|
||||
- **All config via Portainer** - No external env files needed
|
||||
|
||||
### Service URLs (internal)
|
||||
|
||||
| Service | Internal URL |
|
||||
|----------|-------------------------|
|
||||
| Backend | http://backend:4000 |
|
||||
| Database | postgresql://database:5432 |
|
||||
| Redis | redis://cache:6379 |
|
||||
|
||||
### Nginx/Plesk Proxy
|
||||
|
||||
Configure your domain to proxy to:
|
||||
|
||||
- Frontend: `http://127.0.0.1:3000`
|
||||
- Backend API: `http://127.0.0.1:4000`
|
||||
|
||||
---
|
||||
|
||||
## Updating the Stack
|
||||
|
||||
1. Load new images:
|
||||
```bash
|
||||
docker load < portal-frontend-latest.tar
|
||||
docker load < portal-backend-latest.tar
|
||||
```
|
||||
|
||||
2. In Portainer: Stacks → customer-portal → **Update the stack**
|
||||
|
||||
3. Check **Re-pull image** and click **Update**
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### View Logs
|
||||
|
||||
```bash
|
||||
docker logs portal-frontend
|
||||
docker logs portal-backend
|
||||
docker logs portal-database
|
||||
docker logs portal-cache
|
||||
```
|
||||
|
||||
### Check Container Health
|
||||
|
||||
```bash
|
||||
docker ps
|
||||
```
|
||||
|
||||
### Access Database
|
||||
|
||||
```bash
|
||||
docker exec -it portal-database psql -U portal -d portal_prod
|
||||
```
|
||||
|
||||
### Test Service Connectivity
|
||||
|
||||
```bash
|
||||
# From backend container
|
||||
docker exec -it portal-backend sh -c "nc -zv database 5432"
|
||||
docker exec -it portal-backend sh -c "nc -zv cache 6379"
|
||||
```
|
||||
169
docker/portainer/docker-compose.yml
Normal file
169
docker/portainer/docker-compose.yml
Normal file
@ -0,0 +1,169 @@
|
||||
# =============================================================================
|
||||
# Customer Portal - Portainer Stack (Bridge Network Mode)
|
||||
# =============================================================================
|
||||
# Uses Docker's default bridge network to avoid iptables issues
|
||||
# All env vars passed via Portainer UI
|
||||
# =============================================================================
|
||||
|
||||
services:
|
||||
# ---------------------------------------------------------------------------
|
||||
# Frontend (Next.js)
|
||||
# ---------------------------------------------------------------------------
|
||||
frontend:
|
||||
image: ${FRONTEND_IMAGE:-portal-frontend:latest}
|
||||
container_name: portal-frontend
|
||||
ports:
|
||||
- "127.0.0.1:${FRONTEND_PORT:-3000}:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- HOSTNAME=0.0.0.0
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- backend
|
||||
network_mode: bridge
|
||||
links:
|
||||
- backend
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
start_period: 40s
|
||||
retries: 3
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Backend (NestJS BFF)
|
||||
# ---------------------------------------------------------------------------
|
||||
backend:
|
||||
image: ${BACKEND_IMAGE:-portal-backend:latest}
|
||||
container_name: portal-backend
|
||||
ports:
|
||||
- "127.0.0.1:${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 - use "database" as host (via links)
|
||||
- DATABASE_URL=postgresql://${POSTGRES_USER:-portal}:${POSTGRES_PASSWORD}@database:5432/${POSTGRES_DB:-portal_prod}?schema=public
|
||||
|
||||
# Redis - use "cache" as host (via links)
|
||||
- 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}
|
||||
|
||||
# Enable automatic database migrations on startup
|
||||
- RUN_MIGRATIONS=true
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- database
|
||||
- cache
|
||||
network_mode: bridge
|
||||
links:
|
||||
- database
|
||||
- cache
|
||||
# Uses the built-in entrypoint which handles:
|
||||
# - SF key decoding from SF_PRIVATE_KEY_BASE64
|
||||
# - Database migration when RUN_MIGRATIONS=true
|
||||
# - Waiting for dependencies (nc checks in entrypoint)
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:4000/health"]
|
||||
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
|
||||
network_mode: bridge
|
||||
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
|
||||
network_mode: bridge
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
redis_data:
|
||||
driver: local
|
||||
94
docker/portainer/stack.env.example
Normal file
94
docker/portainer/stack.env.example
Normal file
@ -0,0 +1,94 @@
|
||||
# =============================================================================
|
||||
# Customer Portal - Portainer Environment Variables (TEMPLATE)
|
||||
# =============================================================================
|
||||
# Copy this file and fill in your actual values.
|
||||
# DO NOT commit files with real secrets to version control.
|
||||
# =============================================================================
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Images & Ports
|
||||
# -----------------------------------------------------------------------------
|
||||
FRONTEND_IMAGE=portal-frontend:latest
|
||||
BACKEND_IMAGE=portal-backend:latest
|
||||
FRONTEND_PORT=3000
|
||||
BACKEND_PORT=4000
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Application
|
||||
# -----------------------------------------------------------------------------
|
||||
APP_NAME=customer-portal-bff
|
||||
APP_BASE_URL=https://your-domain.com
|
||||
CORS_ORIGIN=https://your-domain.com
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Database (PostgreSQL)
|
||||
# -----------------------------------------------------------------------------
|
||||
POSTGRES_DB=portal_prod
|
||||
POSTGRES_USER=portal
|
||||
# Generate with: openssl rand -base64 24
|
||||
POSTGRES_PASSWORD=<GENERATE-SECURE-PASSWORD>
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Security & Auth
|
||||
# -----------------------------------------------------------------------------
|
||||
# Generate with: openssl rand -base64 32
|
||||
JWT_SECRET=<GENERATE-WITH-openssl-rand-base64-32>
|
||||
JWT_EXPIRES_IN=7d
|
||||
BCRYPT_ROUNDS=12
|
||||
# Generate with: openssl rand -base64 32
|
||||
CSRF_SECRET_KEY=<GENERATE-WITH-openssl-rand-base64-32>
|
||||
|
||||
# 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://your-whmcs-instance.com
|
||||
WHMCS_API_IDENTIFIER=<YOUR-WHMCS-API-IDENTIFIER>
|
||||
WHMCS_API_SECRET=<YOUR-WHMCS-API-SECRET>
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Salesforce Integration
|
||||
# -----------------------------------------------------------------------------
|
||||
SF_LOGIN_URL=https://your-org.my.salesforce.com
|
||||
SF_CLIENT_ID=<YOUR-SF-CONNECTED-APP-CLIENT-ID>
|
||||
SF_USERNAME=<YOUR-SF-INTEGRATION-USERNAME>
|
||||
SF_EVENTS_ENABLED=true
|
||||
|
||||
# Salesforce Private Key (Base64 encoded)
|
||||
# To encode: base64 -w 0 < sf-private.key
|
||||
SF_PRIVATE_KEY_BASE64=<BASE64-ENCODED-PRIVATE-KEY>
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Freebit SIM API
|
||||
# -----------------------------------------------------------------------------
|
||||
FREEBIT_BASE_URL=https://i1.mvno.net/emptool/api
|
||||
FREEBIT_OEM_ID=<YOUR-OEM-ID>
|
||||
FREEBIT_OEM_KEY=<YOUR-OEM-KEY>
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Email (SendGrid)
|
||||
# -----------------------------------------------------------------------------
|
||||
EMAIL_ENABLED=true
|
||||
EMAIL_FROM=no-reply@your-domain.com
|
||||
EMAIL_FROM_NAME=Your Company Name
|
||||
SENDGRID_API_KEY=<YOUR-SENDGRID-API-KEY>
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Salesforce Portal Config
|
||||
# -----------------------------------------------------------------------------
|
||||
PORTAL_PRICEBOOK_ID=<YOUR-PRICEBOOK-ID>
|
||||
PORTAL_PRICEBOOK_NAME=Portal
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
LOG_LEVEL=info
|
||||
|
||||
72
docker/portainer/update-stack.sh
Executable file
72
docker/portainer/update-stack.sh
Executable file
@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# Customer Portal - Image Loader Script for Portainer
|
||||
# =============================================================================
|
||||
# Usage: ./update-stack.sh <image-tag>
|
||||
# Example: ./update-stack.sh 20241201-abc123
|
||||
# ./update-stack.sh latest
|
||||
#
|
||||
# Note: After loading images, update the stack in Portainer UI
|
||||
# =============================================================================
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
# Configuration
|
||||
IMAGES_DIR="${IMAGES_DIR:-$(pwd)}"
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${GREEN}[DEPLOY]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[DEPLOY]${NC} $*"; }
|
||||
fail() { echo -e "${RED}[DEPLOY] ERROR:${NC} $*"; exit 1; }
|
||||
|
||||
# Check arguments
|
||||
TAG="${1:-latest}"
|
||||
|
||||
echo ""
|
||||
log "Loading images with tag: ${TAG}"
|
||||
echo ""
|
||||
|
||||
# Look for image files
|
||||
FRONTEND_TAR="${IMAGES_DIR}/portal-frontend-${TAG}.tar"
|
||||
BACKEND_TAR="${IMAGES_DIR}/portal-backend-${TAG}.tar"
|
||||
|
||||
# Also check alternative naming
|
||||
if [[ ! -f "$FRONTEND_TAR" ]]; then
|
||||
FRONTEND_TAR="${IMAGES_DIR}/portal-frontend.${TAG}.tar"
|
||||
fi
|
||||
if [[ ! -f "$BACKEND_TAR" ]]; then
|
||||
BACKEND_TAR="${IMAGES_DIR}/portal-backend.${TAG}.tar"
|
||||
fi
|
||||
|
||||
# Load frontend
|
||||
if [[ -f "$FRONTEND_TAR" ]]; then
|
||||
log "Loading frontend image from: $FRONTEND_TAR"
|
||||
docker load -i "$FRONTEND_TAR"
|
||||
else
|
||||
warn "Frontend tarball not found: $FRONTEND_TAR"
|
||||
fi
|
||||
|
||||
# Load backend
|
||||
if [[ -f "$BACKEND_TAR" ]]; then
|
||||
log "Loading backend image from: $BACKEND_TAR"
|
||||
docker load -i "$BACKEND_TAR"
|
||||
else
|
||||
warn "Backend tarball not found: $BACKEND_TAR"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
log "Current portal images:"
|
||||
docker images | grep portal || echo "No portal images found"
|
||||
|
||||
echo ""
|
||||
log "Next steps:"
|
||||
echo " 1. Go to Portainer UI"
|
||||
echo " 2. Navigate to Stacks → customer-portal"
|
||||
echo " 3. Update FRONTEND_IMAGE and BACKEND_IMAGE if using specific tag"
|
||||
echo " 4. Click 'Update the stack' with 'Re-pull image' checked"
|
||||
echo ""
|
||||
@ -3,8 +3,13 @@
|
||||
"version": "1.0.0",
|
||||
"type": "commonjs",
|
||||
"description": "Unified domain layer with contracts, schemas, and provider mappers",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./auth": "./dist/auth/index.js",
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
{
|
||||
"name": "@customer-portal/validation",
|
||||
"version": "1.0.0",
|
||||
"description": "Unified validation service for customer portal (NestJS + React)",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./react": {
|
||||
"types": "./dist/react/index.d.ts",
|
||||
"default": "./dist/react/index.js"
|
||||
},
|
||||
"./nestjs": {
|
||||
"types": "./dist/nestjs/index.d.ts",
|
||||
"default": "./dist/nestjs/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -b",
|
||||
"dev": "tsc -b -w --preserveWatchOutput",
|
||||
"clean": "rm -rf dist",
|
||||
"type-check": "NODE_OPTIONS=\"--max-old-space-size=2048 --max-semi-space-size=128\" tsc --project tsconfig.json --noEmit",
|
||||
"test": "jest",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@customer-portal/domain": "workspace:*",
|
||||
"@nestjs/common": "^11.1.6",
|
||||
"nestjs-pino": "^4.4.0",
|
||||
"nestjs-zod": "^5.0.1",
|
||||
"zod": "^4.1.9"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^11.0.0",
|
||||
"react": "^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@nestjs/common": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.10",
|
||||
"@nestjs/common": "^11.1.6",
|
||||
"react": "19.1.1",
|
||||
"typescript": "^5.9.2",
|
||||
"jest": "^30.0.5",
|
||||
"@types/jest": "^30.0.0",
|
||||
"nestjs-zod": "^5.0.1",
|
||||
"express": "^5.1.0",
|
||||
"@types/express": "^5.0.3"
|
||||
}
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
/**
|
||||
* Shared Validation Schemas
|
||||
* Pure Zod schemas for API contracts - shared between frontend and backend
|
||||
*/
|
||||
|
||||
// Re-export Zod for convenience
|
||||
export { z } from "zod";
|
||||
|
||||
// Framework-specific exports
|
||||
export * from "./react";
|
||||
@ -1,2 +0,0 @@
|
||||
export { ZodValidationPipe, createZodDto, ZodValidationException } from "nestjs-zod";
|
||||
export { ZodValidationExceptionFilter } from "./zod-exception.filter";
|
||||
@ -1,68 +0,0 @@
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus, Inject } from "@nestjs/common";
|
||||
import type { Request, Response } from "express";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { ZodValidationException } from "nestjs-zod";
|
||||
import type { ZodError, ZodIssue } from "zod";
|
||||
|
||||
interface ZodIssueResponse {
|
||||
path: string;
|
||||
message: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
@Catch(ZodValidationException)
|
||||
export class ZodValidationExceptionFilter implements ExceptionFilter {
|
||||
constructor(@Inject(Logger) private readonly logger: Logger) {}
|
||||
|
||||
catch(exception: ZodValidationException, host: ArgumentsHost): void {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
const rawZodError = exception.getZodError();
|
||||
let issues: ZodIssueResponse[] = [];
|
||||
|
||||
if (!this.isZodError(rawZodError)) {
|
||||
this.logger.error("ZodValidationException did not contain a ZodError", {
|
||||
path: request.url,
|
||||
method: request.method,
|
||||
providedType: typeof rawZodError,
|
||||
});
|
||||
} else {
|
||||
issues = this.mapIssues(rawZodError.issues);
|
||||
}
|
||||
|
||||
this.logger.warn("Request validation failed", {
|
||||
path: request.url,
|
||||
method: request.method,
|
||||
issues,
|
||||
});
|
||||
|
||||
response.status(HttpStatus.BAD_REQUEST).json({
|
||||
success: false as const,
|
||||
error: {
|
||||
code: "VALIDATION_FAILED",
|
||||
message: "Request validation failed",
|
||||
details: {
|
||||
issues,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private isZodError(error: unknown): error is ZodError {
|
||||
return Boolean(
|
||||
error && typeof error === "object" && Array.isArray((error as { issues?: unknown }).issues)
|
||||
);
|
||||
}
|
||||
|
||||
private mapIssues(issues: ZodIssue[]): ZodIssueResponse[] {
|
||||
return issues.map(issue => ({
|
||||
path: issue.path.join(".") || "root",
|
||||
message: issue.message,
|
||||
code: issue.code,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
/**
|
||||
* React validation exports
|
||||
* Simple Zod validation for React
|
||||
*/
|
||||
|
||||
export { useZodForm } from "../zod-form";
|
||||
export type { ZodFormOptions, UseZodFormReturn, FormErrors, FormTouched } from "../zod-form";
|
||||
@ -1,47 +0,0 @@
|
||||
/**
|
||||
* Simple Zod Validation Pipe for NestJS
|
||||
* Just uses Zod as-is with clean error formatting
|
||||
*/
|
||||
|
||||
import type { PipeTransform, ArgumentMetadata } from "@nestjs/common";
|
||||
import { Injectable, BadRequestException } from "@nestjs/common";
|
||||
import type { ZodSchema } from "zod";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
@Injectable()
|
||||
export class ZodValidationPipe implements PipeTransform {
|
||||
constructor(private readonly schema: ZodSchema) {}
|
||||
|
||||
transform(value: unknown, _metadata: ArgumentMetadata): unknown {
|
||||
try {
|
||||
return this.schema.parse(value);
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const errors = error.issues.map(issue => ({
|
||||
field: issue.path.join(".") || "root",
|
||||
message: issue.message,
|
||||
code: issue.code,
|
||||
}));
|
||||
|
||||
throw new BadRequestException({
|
||||
message: "Validation failed",
|
||||
errors,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const message = error instanceof Error ? error.message : "Validation failed";
|
||||
throw new BadRequestException(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create Zod pipe (main export)
|
||||
*/
|
||||
export const ZodPipe = (schema: ZodSchema) => new ZodValidationPipe(schema);
|
||||
|
||||
/**
|
||||
* Alternative factory function
|
||||
*/
|
||||
export const createZodPipe = (schema: ZodSchema) => new ZodValidationPipe(schema);
|
||||
@ -1,17 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "dist/.tsbuildinfo",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["dist", "node_modules", "**/*.test.ts", "**/*.spec.ts"],
|
||||
"references": [{ "path": "../domain" }]
|
||||
}
|
||||
46
pnpm-lock.yaml
generated
46
pnpm-lock.yaml
generated
@ -70,9 +70,6 @@ importers:
|
||||
'@customer-portal/logging':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/logging
|
||||
'@customer-portal/validation':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/validation
|
||||
'@nestjs/bullmq':
|
||||
specifier: ^11.0.3
|
||||
version: 11.0.3(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(bullmq@5.58.5)
|
||||
@ -272,9 +269,6 @@ importers:
|
||||
'@customer-portal/domain':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/domain
|
||||
'@customer-portal/validation':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/validation
|
||||
'@heroicons/react':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0(react@19.1.1)
|
||||
@ -375,46 +369,6 @@ importers:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.2
|
||||
|
||||
packages/validation:
|
||||
dependencies:
|
||||
'@customer-portal/domain':
|
||||
specifier: workspace:*
|
||||
version: link:../domain
|
||||
'@nestjs/common':
|
||||
specifier: ^11.1.6
|
||||
version: 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
nestjs-pino:
|
||||
specifier: ^4.4.0
|
||||
version: 4.4.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@10.5.0)(pino@9.9.5)(rxjs@7.8.2)
|
||||
nestjs-zod:
|
||||
specifier: ^5.0.1
|
||||
version: 5.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.1.9)
|
||||
zod:
|
||||
specifier: ^4.1.9
|
||||
version: 4.1.9
|
||||
devDependencies:
|
||||
'@types/express':
|
||||
specifier: ^5.0.3
|
||||
version: 5.0.3
|
||||
'@types/jest':
|
||||
specifier: ^30.0.0
|
||||
version: 30.0.0
|
||||
'@types/react':
|
||||
specifier: ^19.1.10
|
||||
version: 19.1.12
|
||||
express:
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0
|
||||
jest:
|
||||
specifier: ^30.0.5
|
||||
version: 30.1.3(@types/node@24.3.1)(ts-node@10.9.2(@types/node@24.3.1)(typescript@5.9.2))
|
||||
react:
|
||||
specifier: 19.1.1
|
||||
version: 19.1.1
|
||||
typescript:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.2
|
||||
|
||||
packages:
|
||||
|
||||
'@alloc/quick-lru@5.2.0':
|
||||
|
||||
@ -1 +0,0 @@
|
||||
735d984b4fc0c5de1404ee95991e6a0ab627e815a46fbb2e3002240a551146a2 /home/barsa/projects/customer_portal/customer-portal/portal-backend.latest.tar.gz
|
||||
@ -1 +0,0 @@
|
||||
de99755961ca5a0d2b8713b1a57b6d818cb860d0eb87387c4ff508882d2f6984 /home/barsa/projects/customer_portal/customer-portal/portal-backend.latest.tar
|
||||
@ -1 +0,0 @@
|
||||
2d1c7887410361baefcc3f2038dce9079ca6fa19d5afa29e8281c99a40d020c7 /home/barsa/projects/customer_portal/customer-portal/portal-frontend.latest.tar.gz
|
||||
@ -1 +0,0 @@
|
||||
ea3c21988f94a9f8755e1024d45187afad435df399c79c17934e701ca7c4ad9b /home/barsa/projects/customer_portal/customer-portal/portal-frontend.latest.tar
|
||||
@ -77,7 +77,8 @@ cd "$PROJECT_ROOT"
|
||||
# Enable BuildKit
|
||||
export DOCKER_BUILDKIT=1
|
||||
|
||||
# Build args
|
||||
# Build args (can be overridden via env vars)
|
||||
PNPM_VERSION="${PNPM_VERSION:-10.15.0}"
|
||||
NEXT_PUBLIC_API_BASE="${NEXT_PUBLIC_API_BASE:-/api}"
|
||||
NEXT_PUBLIC_APP_NAME="${NEXT_PUBLIC_APP_NAME:-Customer Portal}"
|
||||
GIT_SOURCE="$(git config --get remote.origin.url 2>/dev/null || echo unknown)"
|
||||
@ -90,6 +91,7 @@ mkdir -p "$LOG_DIR"
|
||||
build_frontend() {
|
||||
local logfile="$LOG_DIR/frontend.log"
|
||||
docker build -f apps/portal/Dockerfile \
|
||||
--build-arg "PNPM_VERSION=${PNPM_VERSION}" \
|
||||
--build-arg "NEXT_PUBLIC_API_BASE=${NEXT_PUBLIC_API_BASE}" \
|
||||
--build-arg "NEXT_PUBLIC_APP_NAME=${NEXT_PUBLIC_APP_NAME}" \
|
||||
--build-arg "NEXT_PUBLIC_APP_VERSION=${IMAGE_TAG}" \
|
||||
@ -110,6 +112,7 @@ build_frontend() {
|
||||
build_backend() {
|
||||
local logfile="$LOG_DIR/backend.log"
|
||||
docker build -f apps/bff/Dockerfile \
|
||||
--build-arg "PNPM_VERSION=${PNPM_VERSION}" \
|
||||
-t "${IMAGE_BACKEND}:latest" -t "${IMAGE_BACKEND}:${IMAGE_TAG}" \
|
||||
--label "org.opencontainers.image.version=${IMAGE_TAG}" \
|
||||
--label "org.opencontainers.image.source=${GIT_SOURCE}" \
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user