diff --git a/DOCKER.md b/DOCKER.md index cb62725..c8319fa 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -167,6 +167,21 @@ docker compose -f traefik.yaml up -d npm run docker:prod:up ``` +Or use the new helper scripts if you want to start one production container at a time: + +```bash +npm run docker:prod:start:traefik +npm run docker:prod:start:postgres +npm run docker:prod:start:redis +npm run docker:prod:start:api +npm run docker:prod:start:marketplace +npm run docker:prod:start:dashboard +npm run docker:prod:start:admin +npm run docker:prod:start:frontends +npm run docker:prod:start:pgmanage +npm run docker:prod:start:all +``` + Docker will: 1. Build the monorepo image 2. Start all app services (`api`, `marketplace`, `dashboard`, `admin`, `pgmanage`) diff --git a/apps/api/src/routes/admin.ts b/apps/api/src/routes/admin.ts index df946d9..848147b 100644 --- a/apps/api/src/routes/admin.ts +++ b/apps/api/src/routes/admin.ts @@ -316,6 +316,22 @@ router.post('/auth/2fa/verify', requireAdminAuth, async (req, res, next) => { const ADMIN_RESET_TOKEN_TTL_MINUTES = 60 +function ensureAppBasePath(baseUrl: string, basePath: string) { + try { + const url = new URL(baseUrl) + const normalizedPathname = url.pathname.replace(/\/$/, '') + if (normalizedPathname === basePath || normalizedPathname.endsWith(basePath)) { + return url.toString().replace(/\/$/, '') + } + + url.pathname = `${normalizedPathname}${basePath}` || basePath + return url.toString().replace(/\/$/, '') + } catch { + const trimmed = baseUrl.replace(/\/$/, '') + return trimmed.endsWith(basePath) ? trimmed : `${trimmed}${basePath}` + } +} + router.post('/auth/forgot-password', async (req, res, next) => { try { const { email } = z.object({ email: z.string().email().max(255).trim().toLowerCase() }).parse(req.body) @@ -330,7 +346,10 @@ router.post('/auth/forgot-password', async (req, res, next) => { data: { passwordResetToken: rawToken, passwordResetExpiresAt: expiresAt }, }) - const adminUrl = process.env.ADMIN_URL ?? 'http://localhost:3002' + const adminUrl = ensureAppBasePath( + process.env.ADMIN_URL ?? process.env.NEXT_PUBLIC_ADMIN_URL ?? 'http://localhost:3002/admin', + '/admin', + ) const resetUrl = `${adminUrl}/reset-password?token=${rawToken}` await sendTransactionalEmail({ diff --git a/apps/api/src/routes/auth.employee.ts b/apps/api/src/routes/auth.employee.ts index bf587a4..7ae6491 100644 --- a/apps/api/src/routes/auth.employee.ts +++ b/apps/api/src/routes/auth.employee.ts @@ -10,6 +10,22 @@ const router = Router() const RESET_TOKEN_TTL_MINUTES = 60 +function ensureAppBasePath(baseUrl: string, basePath: string) { + try { + const url = new URL(baseUrl) + const normalizedPathname = url.pathname.replace(/\/$/, '') + if (normalizedPathname === basePath || normalizedPathname.endsWith(basePath)) { + return url.toString().replace(/\/$/, '') + } + + url.pathname = `${normalizedPathname}${basePath}` || basePath + return url.toString().replace(/\/$/, '') + } catch { + const trimmed = baseUrl.replace(/\/$/, '') + return trimmed.endsWith(basePath) ? trimmed : `${trimmed}${basePath}` + } +} + function signEmployeeToken(employeeId: string) { return jwt.sign( { sub: employeeId, type: 'employee' }, @@ -150,7 +166,10 @@ router.post('/forgot-password', async (req, res, next) => { data: { passwordResetToken: rawToken, passwordResetExpiresAt: expiresAt }, }) - const dashboardUrl = process.env.DASHBOARD_URL ?? 'http://localhost:3001' + const dashboardUrl = ensureAppBasePath( + process.env.DASHBOARD_URL ?? process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3001/dashboard', + '/dashboard', + ) const resetUrl = `${dashboardUrl}/reset-password?token=${rawToken}` await sendTransactionalEmail({ diff --git a/apps/dashboard/src/lib/api.ts b/apps/dashboard/src/lib/api.ts index 4087487..4d71138 100644 --- a/apps/dashboard/src/lib/api.ts +++ b/apps/dashboard/src/lib/api.ts @@ -1,7 +1,7 @@ export const API_BASE = typeof window === 'undefined' ? (process.env.API_INTERNAL_URL || 'http://localhost:4000/api/v1') - : (process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000/api/v1') + : (process.env.NEXT_PUBLIC_API_URL || '/dashboard/api/v1') export const EMPLOYEE_TOKEN_KEY = 'employee_token' export const EMPLOYEE_PROFILE_KEY = 'employee_profile' diff --git a/package.json b/package.json index 5a60321..20acb85 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,16 @@ "dev": "turbo dev", "lint": "turbo lint", "type-check": "turbo type-check", + "docker:prod:start:all": "bash scripts/docker-prod-up-all.sh", + "docker:prod:start:traefik": "bash scripts/docker-prod-up-traefik.sh", + "docker:prod:start:postgres": "bash scripts/docker-prod-up-postgres.sh", + "docker:prod:start:redis": "bash scripts/docker-prod-up-redis.sh", + "docker:prod:start:api": "bash scripts/docker-prod-up-api.sh", + "docker:prod:start:marketplace": "bash scripts/docker-prod-up-marketplace.sh", + "docker:prod:start:dashboard": "bash scripts/docker-prod-up-dashboard.sh", + "docker:prod:start:admin": "bash scripts/docker-prod-up-admin.sh", + "docker:prod:start:frontends": "bash scripts/docker-prod-up-frontends.sh", + "docker:prod:start:pgmanage": "bash scripts/docker-prod-up-pgmanage.sh", "docker:prod:up": "docker compose -p rentaldrivego-prod --env-file .env.docker.production -f docker-compose.production.yml up --build -d", "docker:prod:down": "docker compose -p rentaldrivego-prod --env-file .env.docker.production -f docker-compose.production.yml down", "docker:prod:logs": "docker compose -p rentaldrivego-prod --env-file .env.docker.production -f docker-compose.production.yml logs -f", diff --git a/scripts/docker-prod-common.sh b/scripts/docker-prod-common.sh new file mode 100755 index 0000000..97c26c0 --- /dev/null +++ b/scripts/docker-prod-common.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PROD_COMPOSE_FILE="${ROOT_DIR}/docker-compose.production.yml" +TRAEFIK_COMPOSE_FILE="${ROOT_DIR}/traefik.yaml" +ENV_FILE="${ROOT_DIR}/.env.docker.production" +PROJECT_NAME="${DOCKER_PROD_PROJECT_NAME:-rentaldrivego-prod}" +TRAEFIK_NETWORK="${DOCKER_PROD_TRAEFIK_NETWORK:-traefik-proxy}" + +ensure_env_file() { + if [[ ! -f "${ENV_FILE}" ]]; then + echo "Missing ${ENV_FILE}" >&2 + echo "Create it from .env.docker.production.example before starting production containers." >&2 + exit 1 + fi +} + +ensure_traefik_network() { + if ! docker network inspect "${TRAEFIK_NETWORK}" >/dev/null 2>&1; then + echo "Creating Docker network: ${TRAEFIK_NETWORK}" + docker network create "${TRAEFIK_NETWORK}" >/dev/null + fi +} + +prod_compose() { + docker compose -p "${PROJECT_NAME}" --env-file "${ENV_FILE}" -f "${PROD_COMPOSE_FILE}" "$@" +} + +traefik_compose() { + docker compose -f "${TRAEFIK_COMPOSE_FILE}" "$@" +} + +start_prod_services() { + ensure_env_file + prod_compose up --build -d "$@" +} + +start_traefik() { + ensure_env_file + ensure_traefik_network + traefik_compose up -d +} diff --git a/scripts/docker-prod-up-admin.sh b/scripts/docker-prod-up-admin.sh new file mode 100755 index 0000000..2f5f17d --- /dev/null +++ b/scripts/docker-prod-up-admin.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "${ROOT_DIR}/scripts/docker-prod-common.sh" + +start_prod_services admin + +echo "Admin is starting." diff --git a/scripts/docker-prod-up-all.sh b/scripts/docker-prod-up-all.sh new file mode 100755 index 0000000..3a653f2 --- /dev/null +++ b/scripts/docker-prod-up-all.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "${ROOT_DIR}/scripts/docker-prod-common.sh" + +start_traefik +start_prod_services + +echo "Production stack is starting: traefik, postgres, redis, api, marketplace, dashboard, admin, pgmanage" diff --git a/scripts/docker-prod-up-api.sh b/scripts/docker-prod-up-api.sh new file mode 100755 index 0000000..9dd2947 --- /dev/null +++ b/scripts/docker-prod-up-api.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "${ROOT_DIR}/scripts/docker-prod-common.sh" + +start_prod_services api + +echo "API is starting." diff --git a/scripts/docker-prod-up-dashboard.sh b/scripts/docker-prod-up-dashboard.sh new file mode 100755 index 0000000..4964707 --- /dev/null +++ b/scripts/docker-prod-up-dashboard.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "${ROOT_DIR}/scripts/docker-prod-common.sh" + +start_prod_services dashboard + +echo "Dashboard is starting." diff --git a/scripts/docker-prod-up-frontends.sh b/scripts/docker-prod-up-frontends.sh new file mode 100755 index 0000000..455b832 --- /dev/null +++ b/scripts/docker-prod-up-frontends.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "${ROOT_DIR}/scripts/docker-prod-common.sh" + +start_prod_services marketplace dashboard admin + +echo "Frontend production services are starting: marketplace, dashboard, admin" diff --git a/scripts/docker-prod-up-marketplace.sh b/scripts/docker-prod-up-marketplace.sh new file mode 100755 index 0000000..77e64b2 --- /dev/null +++ b/scripts/docker-prod-up-marketplace.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "${ROOT_DIR}/scripts/docker-prod-common.sh" + +start_prod_services marketplace + +echo "Marketplace is starting." diff --git a/scripts/docker-prod-up-pgmanage.sh b/scripts/docker-prod-up-pgmanage.sh new file mode 100755 index 0000000..606259c --- /dev/null +++ b/scripts/docker-prod-up-pgmanage.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "${ROOT_DIR}/scripts/docker-prod-common.sh" + +start_prod_services pgmanage + +echo "pgManage is starting." diff --git a/scripts/docker-prod-up-postgres.sh b/scripts/docker-prod-up-postgres.sh new file mode 100755 index 0000000..e83f540 --- /dev/null +++ b/scripts/docker-prod-up-postgres.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "${ROOT_DIR}/scripts/docker-prod-common.sh" + +start_prod_services postgres + +echo "Postgres is starting." diff --git a/scripts/docker-prod-up-redis.sh b/scripts/docker-prod-up-redis.sh new file mode 100755 index 0000000..fd6ebf3 --- /dev/null +++ b/scripts/docker-prod-up-redis.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "${ROOT_DIR}/scripts/docker-prod-common.sh" + +start_prod_services redis + +echo "Redis is starting." diff --git a/scripts/docker-prod-up-traefik.sh b/scripts/docker-prod-up-traefik.sh new file mode 100755 index 0000000..61f8bef --- /dev/null +++ b/scripts/docker-prod-up-traefik.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "${ROOT_DIR}/scripts/docker-prod-common.sh" + +start_traefik + +echo "Traefik is starting."