diff --git a/.env.docker.production.example b/.env.docker.production.example new file mode 100644 index 0000000..b10a2e8 --- /dev/null +++ b/.env.docker.production.example @@ -0,0 +1,74 @@ +# Traefik / public domains +ACME_EMAIL=rentaldrivego@gmail.com +API_DOMAIN=api.rentaldrivego.ma +PUBLIC_SITE_DOMAIN=rentaldrivego.ma +PGMANAGE_DOMAIN=pgmanage.rentaldrivego.ma + +# Database +DATABASE_URL_FROM_POSTGRES=true +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 +POSTGRES_DB=rentaldrivego +POSTGRES_USER=postgres +POSTGRES_PASSWORD=PMPS5k0D7rUeJOk0NkhI5bRtoGjkUqjK + +# Cache +REDIS_URL=redis://redis:6379 + +# API +API_PORT=4000 +API_INTERNAL_URL=http://api:4000/api/v1 +API_URL=https://api.rentaldrivego.ma +NEXT_PUBLIC_API_URL=https://api.rentaldrivego.ma/api/v1 + +# Frontend public URLs +NEXT_PUBLIC_MARKETING_URL=https://rentaldrivego.ma +NEXT_PUBLIC_MARKETPLACE_URL=https://rentaldrivego.ma/marketplace +NEXT_PUBLIC_DASHBOARD_URL=https://rentaldrivego.ma/dashboard +NEXT_PUBLIC_ADMIN_URL=https://rentaldrivego.ma/admin +NEXT_PUBLIC_PUBLIC_SITE_DOMAIN=rentaldrivego.ma +DASHBOARD_URL=https://rentaldrivego.ma/dashboard +ADMIN_URL=https://rentaldrivego.ma/admin + +# CORS +CORS_ORIGINS=https://rentaldrivego.ma + +# Auth +JWT_SECRET=PMPS5k0D7rUeJOk0NkhI5bRtoGjkUqjK +JWT_EXPIRY=8h +RENTER_JWT_EXPIRY=7d +NODE_ENV=production + +# Email +RESEND_API_KEY=C8qPDuFwsv5l@KsGhL/V +EMAIL_FROM=rentaldrivego@gmail.com +EMAIL_FROM_NAME=RentalDriveGo +MAIL_HOST=smtp.gmail.com +MAIL_PORT=587 +MAIL_SCHEME=smtp +MAIL_USERNAME=rentaldrivego@gmail.com +MAIL_PASSWORD=xmhg ibqy muoc rntc +MAIL_FROM_ADDRESS=rentaldrivego@gmail.com +MAIL_FROM_NAME=RentalDriveGo +MAIL_REPLY_TO_ADDRESS=rentaldrivego@gmail.com +MAIL_REPLY_TO_NAME=RentalDriveGo + +# Optional SMTP instead of Resend +# MAIL_HOST=smtp.rentaldrivego.ma +# MAIL_PORT=587 +# MAIL_USERNAME=smtp-user +# MAIL_PASSWORD=smtp-password +# MAIL_SCHEME=smtp +# MAIL_FROM_ADDRESS=noreply@rentaldrivego.ma +# MAIL_FROM_NAME=RentalDriveGo + +# Optional Firebase push notifications +# FIREBASE_PROJECT_ID=your-firebase-project-id +# FIREBASE_CLIENT_EMAIL=firebase-adminsdk@your-project.iam.gserviceaccount.com +# FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n" + +# Optional Twilio SMS / WhatsApp +# TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +# TWILIO_AUTH_TOKEN=your_auth_token +# TWILIO_PHONE_NUMBER=+1234567890 +# TWILIO_WHATSAPP_NUMBER=+1234567890 diff --git a/DOCKER.md b/DOCKER.md index 09a681e..dba8b1b 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -152,6 +152,8 @@ Open `.env.docker.production` and fill in every value. The minimum required secr | `ACME_EMAIL` | Your email for Let's Encrypt notifications | | `RESEND_API_KEY` | Resend API key (or configure SMTP vars instead) | +Production now derives `DATABASE_URL` inside the app container from `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_DB`, `POSTGRES_USER`, and `POSTGRES_PASSWORD` when `DATABASE_URL_FROM_POSTGRES=true`. That avoids Prisma auth failures when the database password contains reserved URL characters such as `@`, `:`, or `/`. + The example file uses `rentaldrivego.ma` for the public site and `app.rentaldrivego.ma` for the marketplace. Adjust them only if you use different hostnames. #### 5. Start Traefik diff --git a/docker-compose.production.yml b/docker-compose.production.yml index a11eeb2..9b23783 100644 --- a/docker-compose.production.yml +++ b/docker-compose.production.yml @@ -46,6 +46,12 @@ services: condition: service_started env_file: - .env.docker.production + environment: + DATABASE_URL_FROM_POSTGRES: ${DATABASE_URL_FROM_POSTGRES:-true} + POSTGRES_HOST: ${POSTGRES_HOST:-postgres} + POSTGRES_PORT: ${POSTGRES_PORT:-5432} + POSTGRES_DB: ${POSTGRES_DB:-rentaldrivego} + POSTGRES_USER: ${POSTGRES_USER:-postgres} restart: unless-stopped labels: - traefik.enable=true diff --git a/packages/database/prisma/seed.ts b/packages/database/prisma/seed.ts index 408384b..dbd9ce7 100644 --- a/packages/database/prisma/seed.ts +++ b/packages/database/prisma/seed.ts @@ -1,5 +1,8 @@ import { PrismaClient } from '../generated' import bcrypt from 'bcryptjs' +import { ensureDatabaseUrl } from '../src/runtime-config' + +ensureDatabaseUrl() const prisma = new PrismaClient() diff --git a/packages/database/scripts/db-deploy.cjs b/packages/database/scripts/db-deploy.cjs index 0cbe331..021dd13 100644 --- a/packages/database/scripts/db-deploy.cjs +++ b/packages/database/scripts/db-deploy.cjs @@ -3,6 +3,7 @@ const { spawnSync } = require('node:child_process') const { readdirSync } = require('node:fs') const path = require('node:path') +const { ensureDatabaseUrl } = require('../src/runtime-config') const { PrismaClient } = require('../generated') const repoRoot = path.resolve(__dirname, '../../..') @@ -23,6 +24,7 @@ function run(command, args) { } async function main() { + ensureDatabaseUrl() const prisma = new PrismaClient() try { diff --git a/packages/database/src/index.js b/packages/database/src/index.js index 9dcf0c6..98a1eee 100644 --- a/packages/database/src/index.js +++ b/packages/database/src/index.js @@ -1,5 +1,9 @@ +const { ensureDatabaseUrl } = require('./runtime-config') const { PrismaClient } = require('../generated') +ensureDatabaseUrl() + module.exports = { PrismaClient, + ensureDatabaseUrl, } diff --git a/packages/database/src/index.ts b/packages/database/src/index.ts index de69484..f24d8d8 100644 --- a/packages/database/src/index.ts +++ b/packages/database/src/index.ts @@ -1,4 +1,9 @@ +import { ensureDatabaseUrl } from './runtime-config' + +ensureDatabaseUrl() + export { PrismaClient } from '../generated' +export { ensureDatabaseUrl } from './runtime-config' export type AdminRole = 'SUPER_ADMIN' | 'ADMIN' | 'SUPPORT' | 'FINANCE' | 'VIEWER' export type EmployeeRole = 'OWNER' | 'MANAGER' | 'AGENT' diff --git a/packages/database/src/runtime-config.js b/packages/database/src/runtime-config.js new file mode 100644 index 0000000..18742df --- /dev/null +++ b/packages/database/src/runtime-config.js @@ -0,0 +1,29 @@ +const ENABLED_VALUES = new Set(['1', 'true', 'yes']) + +function ensureDatabaseUrl(env = process.env) { + const shouldBuildFromPostgres = ENABLED_VALUES.has( + env.DATABASE_URL_FROM_POSTGRES?.trim().toLowerCase() ?? '', + ) + + if (!shouldBuildFromPostgres) { + return env.DATABASE_URL + } + + if (!env.POSTGRES_PASSWORD) { + throw new Error('DATABASE_URL_FROM_POSTGRES is enabled but POSTGRES_PASSWORD is not set') + } + + const user = env.POSTGRES_USER ?? 'postgres' + const password = env.POSTGRES_PASSWORD + const host = env.POSTGRES_HOST ?? 'postgres' + const port = env.POSTGRES_PORT ?? '5432' + const database = env.POSTGRES_DB ?? 'rentaldrivego' + + env.DATABASE_URL = `postgresql://${encodeURIComponent(user)}:${encodeURIComponent(password)}@${host}:${port}/${encodeURIComponent(database)}` + + return env.DATABASE_URL +} + +module.exports = { + ensureDatabaseUrl, +} diff --git a/packages/database/src/runtime-config.ts b/packages/database/src/runtime-config.ts new file mode 100644 index 0000000..0e95ca1 --- /dev/null +++ b/packages/database/src/runtime-config.ts @@ -0,0 +1,25 @@ +const ENABLED_VALUES = new Set(['1', 'true', 'yes']) + +export function ensureDatabaseUrl(env: NodeJS.ProcessEnv = process.env) { + const shouldBuildFromPostgres = ENABLED_VALUES.has( + env.DATABASE_URL_FROM_POSTGRES?.trim().toLowerCase() ?? '', + ) + + if (!shouldBuildFromPostgres) { + return env.DATABASE_URL + } + + if (!env.POSTGRES_PASSWORD) { + throw new Error('DATABASE_URL_FROM_POSTGRES is enabled but POSTGRES_PASSWORD is not set') + } + + const user = env.POSTGRES_USER ?? 'postgres' + const password = env.POSTGRES_PASSWORD + const host = env.POSTGRES_HOST ?? 'postgres' + const port = env.POSTGRES_PORT ?? '5432' + const database = env.POSTGRES_DB ?? 'rentaldrivego' + + env.DATABASE_URL = `postgresql://${encodeURIComponent(user)}:${encodeURIComponent(password)}@${host}:${port}/${encodeURIComponent(database)}` + + return env.DATABASE_URL +}