refactor(deploy): externalize all secrets to .env, migrate Brevo SMTP → REST API
- docker-compose.yml: replace 43 hardcoded env values with ${VAR} references.
Operators must provide /opt/architools/.env (chmod 600, gitignored) with the
matching keys. Removes the historical leak surface where every edit risked
echoing secrets.
- email-service.ts: drop nodemailer SMTP transport; use Brevo REST API
(POST https://api.brevo.com/v3/smtp/email) with BREVO_API_KEY header.
Brevo SMTP relay credentials have been deleted upstream.
- package.json: remove nodemailer + @types/nodemailer.
NOTE: legacy hardcoded credentials present in git history must still be
rotated separately (DB password, Authentik client secret, ENCRYPTION_SECRET,
ANCPI password, etc.).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+44
-47
@@ -6,33 +6,33 @@ services:
|
|||||||
- NEXT_PUBLIC_STORAGE_ADAPTER=${NEXT_PUBLIC_STORAGE_ADAPTER:-database}
|
- NEXT_PUBLIC_STORAGE_ADAPTER=${NEXT_PUBLIC_STORAGE_ADAPTER:-database}
|
||||||
- NEXT_PUBLIC_APP_NAME=${NEXT_PUBLIC_APP_NAME:-ArchiTools}
|
- NEXT_PUBLIC_APP_NAME=${NEXT_PUBLIC_APP_NAME:-ArchiTools}
|
||||||
- NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL:-https://tools.beletage.ro}
|
- NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL:-https://tools.beletage.ro}
|
||||||
- NEXT_PUBLIC_MARTIN_URL=https://tools.beletage.ro/tiles
|
- NEXT_PUBLIC_MARTIN_URL=${NEXT_PUBLIC_MARTIN_URL}
|
||||||
- NEXT_PUBLIC_PMTILES_URL=/tiles/pmtiles/overview.pmtiles
|
- NEXT_PUBLIC_PMTILES_URL=${NEXT_PUBLIC_PMTILES_URL}
|
||||||
container_name: architools
|
container_name: architools
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=${NODE_ENV}
|
||||||
# Database
|
# Database
|
||||||
- DATABASE_URL=postgresql://architools_user:stictMyFon34!_gonY@10.10.10.166:5432/architools_db?schema=public
|
- DATABASE_URL=${DATABASE_URL}
|
||||||
# MinIO
|
# MinIO
|
||||||
- MINIO_ENDPOINT=10.10.10.166
|
- MINIO_ENDPOINT=${MINIO_ENDPOINT}
|
||||||
- MINIO_PORT=9002
|
- MINIO_PORT=${MINIO_PORT}
|
||||||
- MINIO_USE_SSL=false
|
- MINIO_USE_SSL=${MINIO_USE_SSL}
|
||||||
- MINIO_ACCESS_KEY=admin
|
- MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY}
|
||||||
- MINIO_SECRET_KEY=MinioStrongPass123
|
- MINIO_SECRET_KEY=${MINIO_SECRET_KEY}
|
||||||
- MINIO_BUCKET_NAME=tools
|
- MINIO_BUCKET_NAME=${MINIO_BUCKET_NAME}
|
||||||
# Authentication (Authentik OIDC)
|
# Authentication (Authentik OIDC)
|
||||||
- NEXTAUTH_URL=https://tools.beletage.ro
|
- NEXTAUTH_URL=${NEXTAUTH_URL}
|
||||||
- NEXTAUTH_SECRET=8IL9Kpipj0EZwZPNvekbNRPhV6a2/UY4cGVzE3n0pUY=
|
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
|
||||||
- AUTHENTIK_CLIENT_ID=V59GMiYle87yd9VZOgUmdSmzYQALqNsKVAUR6QMi
|
- AUTHENTIK_CLIENT_ID=${AUTHENTIK_CLIENT_ID}
|
||||||
- AUTHENTIK_CLIENT_SECRET=TMeewkusUro0hQ2DMwS0Z5lNpNMdmziO9WXywNAGlK3Y6Y8HYULZBEtMtm53lioIkszWbpPRQcv1cxHMtwftMvsaSnbliDsL1f707wmUJhMFKjeZ0ypIFKFG4dJkp7Jr
|
- AUTHENTIK_CLIENT_SECRET=${AUTHENTIK_CLIENT_SECRET}
|
||||||
- AUTHENTIK_ISSUER=https://auth.beletage.ro/application/o/architools/
|
- AUTHENTIK_ISSUER=${AUTHENTIK_ISSUER}
|
||||||
# Vault encryption
|
# Vault encryption
|
||||||
- ENCRYPTION_SECRET=ArchiTools-Vault-2025!SecureKey@AES256
|
- ENCRYPTION_SECRET=${ENCRYPTION_SECRET}
|
||||||
# ManicTime Tags.txt sync (SMB mount path)
|
# ManicTime Tags.txt sync (SMB mount path)
|
||||||
- MANICTIME_TAGS_PATH=/mnt/manictime/Tags.txt
|
- MANICTIME_TAGS_PATH=${MANICTIME_TAGS_PATH}
|
||||||
# AI Chat (set AI_PROVIDER to openai/anthropic/ollama; demo if no key)
|
# AI Chat (set AI_PROVIDER to openai/anthropic/ollama; demo if no key)
|
||||||
- AI_PROVIDER=${AI_PROVIDER:-demo}
|
- AI_PROVIDER=${AI_PROVIDER:-demo}
|
||||||
- AI_API_KEY=${AI_API_KEY:-}
|
- AI_API_KEY=${AI_API_KEY:-}
|
||||||
@@ -45,39 +45,36 @@ services:
|
|||||||
- ETERRA_USERNAME=${ETERRA_USERNAME:-}
|
- ETERRA_USERNAME=${ETERRA_USERNAME:-}
|
||||||
- ETERRA_PASSWORD=${ETERRA_PASSWORD:-}
|
- ETERRA_PASSWORD=${ETERRA_PASSWORD:-}
|
||||||
# ANCPI ePay (CF extract ordering)
|
# ANCPI ePay (CF extract ordering)
|
||||||
- ANCPI_USERNAME=m.tarau@beletage.ro
|
- ANCPI_USERNAME=${ANCPI_USERNAME}
|
||||||
- ANCPI_PASSWORD=Beletage@112
|
- ANCPI_PASSWORD=${ANCPI_PASSWORD}
|
||||||
- ANCPI_BASE_URL=https://epay.ancpi.ro/epay
|
- ANCPI_BASE_URL=${ANCPI_BASE_URL}
|
||||||
- ANCPI_LOGIN_URL=https://oassl.ancpi.ro/openam/UI/Login
|
- ANCPI_LOGIN_URL=${ANCPI_LOGIN_URL}
|
||||||
- ANCPI_DEFAULT_SOLICITANT_ID=14452
|
- ANCPI_DEFAULT_SOLICITANT_ID=${ANCPI_DEFAULT_SOLICITANT_ID}
|
||||||
- MINIO_BUCKET_ANCPI=ancpi-documente
|
- MINIO_BUCKET_ANCPI=${MINIO_BUCKET_ANCPI}
|
||||||
# Stirling PDF (local PDF tools)
|
# Stirling PDF (local PDF tools)
|
||||||
- STIRLING_PDF_URL=http://10.10.10.166:8087
|
- STIRLING_PDF_URL=${STIRLING_PDF_URL}
|
||||||
- STIRLING_PDF_API_KEY=cd829f62-6eef-43eb-a64d-c91af727b53a
|
- STIRLING_PDF_API_KEY=${STIRLING_PDF_API_KEY}
|
||||||
# iLovePDF cloud compression (free: 250 files/month)
|
# iLovePDF cloud compression (free: 250 files/month)
|
||||||
- ILOVEPDF_PUBLIC_KEY=${ILOVEPDF_PUBLIC_KEY:-}
|
- ILOVEPDF_PUBLIC_KEY=${ILOVEPDF_PUBLIC_KEY:-}
|
||||||
# Martin vector tile server (geoportal)
|
# Martin vector tile server (geoportal)
|
||||||
- NEXT_PUBLIC_MARTIN_URL=https://tools.beletage.ro/tiles
|
- NEXT_PUBLIC_MARTIN_URL=${NEXT_PUBLIC_MARTIN_URL}
|
||||||
# PMTiles overview tiles — proxied through tile-cache nginx (HTTPS, no mixed-content)
|
# PMTiles overview tiles — proxied through tile-cache nginx (HTTPS, no mixed-content)
|
||||||
- NEXT_PUBLIC_PMTILES_URL=/tiles/pmtiles/overview.pmtiles
|
- NEXT_PUBLIC_PMTILES_URL=${NEXT_PUBLIC_PMTILES_URL}
|
||||||
# DWG-to-DXF sidecar
|
# DWG-to-DXF sidecar
|
||||||
- DWG2DXF_URL=http://dwg2dxf:5001
|
- DWG2DXF_URL=${DWG2DXF_URL}
|
||||||
# Email notifications (Brevo SMTP)
|
# Email notifications (Brevo REST API)
|
||||||
- BREVO_SMTP_HOST=smtp-relay.brevo.com
|
- BREVO_API_KEY=${BREVO_API_KEY}
|
||||||
- BREVO_SMTP_PORT=587
|
- NOTIFICATION_FROM_EMAIL=${NOTIFICATION_FROM_EMAIL}
|
||||||
- BREVO_SMTP_USER=a2d94b001@smtp-brevo.com
|
- NOTIFICATION_FROM_NAME=${NOTIFICATION_FROM_NAME}
|
||||||
- BREVO_SMTP_PASS=xsmtpsib-c2f5dfe1a7809c962d8907afafdc9edc1ff7e74340518539de8f8eccfd1dcc90-ipkNHpvN9RByv1V6
|
- NOTIFICATION_CRON_SECRET=${NOTIFICATION_CRON_SECRET}
|
||||||
- NOTIFICATION_FROM_EMAIL=noreply@beletage.ro
|
|
||||||
- NOTIFICATION_FROM_NAME=Alerte Termene
|
|
||||||
- NOTIFICATION_CRON_SECRET=1547a198feca43af6c05622588c6d3b820bad5163b8c20175b2b5bbf8fc1a987
|
|
||||||
# Weekend Deep Sync email reports (comma-separated for multiple recipients)
|
# Weekend Deep Sync email reports (comma-separated for multiple recipients)
|
||||||
- WEEKEND_SYNC_EMAIL=${WEEKEND_SYNC_EMAIL:-}
|
- WEEKEND_SYNC_EMAIL=${WEEKEND_SYNC_EMAIL:-}
|
||||||
# PMTiles rebuild webhook (pmtiles-webhook systemd service on host)
|
# PMTiles rebuild webhook (pmtiles-webhook systemd service on host)
|
||||||
- N8N_WEBHOOK_URL=${N8N_WEBHOOK_URL:-http://10.10.10.166:9876}
|
- N8N_WEBHOOK_URL=${N8N_WEBHOOK_URL:-http://10.10.10.166:9876}
|
||||||
# Portal-only users (comma-separated, redirected to /portal)
|
# Portal-only users (comma-separated, redirected to /portal)
|
||||||
- PORTAL_ONLY_USERS=dtiurbe,d.tiurbe
|
- PORTAL_ONLY_USERS=${PORTAL_ONLY_USERS}
|
||||||
# Address Book API (inter-service auth for external tools)
|
# Address Book API (inter-service auth for external tools)
|
||||||
- ADDRESSBOOK_API_KEY=abook-7f3e9a2b4c1d8e5f6a0b3c7d9e2f4a1b5c8d0e3f6a9b2c5d8e1f4a7b0c3d6e
|
- ADDRESSBOOK_API_KEY=${ADDRESSBOOK_API_KEY}
|
||||||
depends_on:
|
depends_on:
|
||||||
dwg2dxf:
|
dwg2dxf:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -114,7 +111,7 @@ services:
|
|||||||
# No host port — only accessible via tile-cache nginx proxy
|
# No host port — only accessible via tile-cache nginx proxy
|
||||||
command: ["--config", "/config/martin.yaml"]
|
command: ["--config", "/config/martin.yaml"]
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=postgresql://architools_user:stictMyFon34!_gonY@10.10.10.166:5432/architools_db
|
- DATABASE_URL=${DATABASE_URL}
|
||||||
|
|
||||||
tile-cache:
|
tile-cache:
|
||||||
build:
|
build:
|
||||||
@@ -136,14 +133,14 @@ services:
|
|||||||
container_name: tippecanoe
|
container_name: tippecanoe
|
||||||
profiles: ["tools"]
|
profiles: ["tools"]
|
||||||
environment:
|
environment:
|
||||||
- DB_HOST=10.10.10.166
|
- DB_HOST=${DB_HOST}
|
||||||
- DB_PORT=5432
|
- DB_PORT=${DB_PORT}
|
||||||
- DB_NAME=architools_db
|
- DB_NAME=${DB_NAME}
|
||||||
- DB_USER=architools_user
|
- DB_USER=${DB_USER}
|
||||||
- DB_PASS=stictMyFon34!_gonY
|
- DB_PASS=${DB_PASS}
|
||||||
- MINIO_ENDPOINT=http://10.10.10.166:9002
|
- MINIO_ENDPOINT=${MINIO_ENDPOINT}
|
||||||
- MINIO_ACCESS_KEY=admin
|
- MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY}
|
||||||
- MINIO_SECRET_KEY=MinioStrongPass123
|
- MINIO_SECRET_KEY=${MINIO_SECRET_KEY}
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
tile-cache-data:
|
tile-cache-data:
|
||||||
|
|||||||
Generated
+1
-33
@@ -24,7 +24,6 @@
|
|||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"next-auth": "^4.24.13",
|
"next-auth": "^4.24.13",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"nodemailer": "^7.0.13",
|
|
||||||
"pmtiles": "^4.4.0",
|
"pmtiles": "^4.4.0",
|
||||||
"proj4": "^2.20.3",
|
"proj4": "^2.20.3",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
@@ -42,7 +41,6 @@
|
|||||||
"@types/busboy": "^1.5.4",
|
"@types/busboy": "^1.5.4",
|
||||||
"@types/jszip": "^3.4.0",
|
"@types/jszip": "^3.4.0",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/nodemailer": "^7.0.11",
|
|
||||||
"@types/proj4": "^2.5.6",
|
"@types/proj4": "^2.5.6",
|
||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
@@ -124,7 +122,6 @@
|
|||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
@@ -693,7 +690,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -1994,7 +1990,6 @@
|
|||||||
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
|
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^14.21.3 || >=16"
|
"node": "^14.21.3 || >=16"
|
||||||
},
|
},
|
||||||
@@ -4162,16 +4157,6 @@
|
|||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/nodemailer": {
|
|
||||||
"version": "7.0.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.11.tgz",
|
|
||||||
"integrity": "sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/pako": {
|
"node_modules/@types/pako": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
|
||||||
@@ -4208,7 +4193,6 @@
|
|||||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
@@ -4219,7 +4203,6 @@
|
|||||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
@@ -4313,7 +4296,6 @@
|
|||||||
"integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==",
|
"integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.56.0",
|
"@typescript-eslint/scope-manager": "8.56.0",
|
||||||
"@typescript-eslint/types": "8.56.0",
|
"@typescript-eslint/types": "8.56.0",
|
||||||
@@ -4847,7 +4829,6 @@
|
|||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -5232,7 +5213,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
|
||||||
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
|
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.11",
|
"follow-redirects": "^1.15.11",
|
||||||
"form-data": "^4.0.5",
|
"form-data": "^4.0.5",
|
||||||
@@ -5398,7 +5378,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -6734,7 +6713,6 @@
|
|||||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -6920,7 +6898,6 @@
|
|||||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.9",
|
"array-includes": "^3.1.9",
|
||||||
@@ -7240,7 +7217,6 @@
|
|||||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "^2.0.0",
|
"accepts": "^2.0.0",
|
||||||
"body-parser": "^2.2.1",
|
"body-parser": "^2.2.1",
|
||||||
@@ -8130,7 +8106,6 @@
|
|||||||
"integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==",
|
"integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.9.0"
|
"node": ">=16.9.0"
|
||||||
}
|
}
|
||||||
@@ -10169,6 +10144,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.13.tgz",
|
||||||
"integrity": "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==",
|
"integrity": "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==",
|
||||||
"license": "MIT-0",
|
"license": "MIT-0",
|
||||||
|
"optional": true,
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
@@ -10901,7 +10877,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.28.4.tgz",
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.28.4.tgz",
|
||||||
"integrity": "sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==",
|
"integrity": "sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/preact"
|
"url": "https://opencollective.com/preact"
|
||||||
@@ -10958,7 +10933,6 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/config": "6.19.2",
|
"@prisma/config": "6.19.2",
|
||||||
"@prisma/engines": "6.19.2"
|
"@prisma/engines": "6.19.2"
|
||||||
@@ -11438,7 +11412,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||||
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -11448,7 +11421,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||||
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@@ -12907,7 +12879,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -12967,7 +12938,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
|
||||||
"integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==",
|
"integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tldts": "^7.0.5"
|
"tldts": "^7.0.5"
|
||||||
},
|
},
|
||||||
@@ -13175,7 +13145,6 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -13910,7 +13879,6 @@
|
|||||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@
|
|||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"next-auth": "^4.24.13",
|
"next-auth": "^4.24.13",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"nodemailer": "^7.0.13",
|
|
||||||
"pmtiles": "^4.4.0",
|
"pmtiles": "^4.4.0",
|
||||||
"proj4": "^2.20.3",
|
"proj4": "^2.20.3",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
@@ -43,7 +42,6 @@
|
|||||||
"@types/busboy": "^1.5.4",
|
"@types/busboy": "^1.5.4",
|
||||||
"@types/jszip": "^3.4.0",
|
"@types/jszip": "^3.4.0",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/nodemailer": "^7.0.11",
|
|
||||||
"@types/proj4": "^2.5.6",
|
"@types/proj4": "^2.5.6",
|
||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
|
|||||||
@@ -1,55 +1,47 @@
|
|||||||
import nodemailer from "nodemailer";
|
|
||||||
import type { Transporter } from "nodemailer";
|
|
||||||
import type { EmailPayload } from "./types";
|
import type { EmailPayload } from "./types";
|
||||||
|
|
||||||
// ── Singleton transport (lazy init, same pattern as prisma) ──
|
const BREVO_ENDPOINT = "https://api.brevo.com/v3/smtp/email";
|
||||||
|
|
||||||
const globalForEmail = globalThis as unknown as {
|
|
||||||
emailTransport: Transporter | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
function getTransport(): Transporter {
|
|
||||||
if (globalForEmail.emailTransport) return globalForEmail.emailTransport;
|
|
||||||
|
|
||||||
const host = process.env.BREVO_SMTP_HOST ?? "smtp-relay.brevo.com";
|
|
||||||
const port = parseInt(process.env.BREVO_SMTP_PORT ?? "587", 10);
|
|
||||||
const user = process.env.BREVO_SMTP_USER ?? "";
|
|
||||||
const pass = process.env.BREVO_SMTP_PASS ?? "";
|
|
||||||
|
|
||||||
if (!user || !pass) {
|
|
||||||
throw new Error(
|
|
||||||
"BREVO_SMTP_USER and BREVO_SMTP_PASS must be set for email notifications",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const transport = nodemailer.createTransport({
|
|
||||||
host,
|
|
||||||
port,
|
|
||||||
secure: false, // STARTTLS on port 587
|
|
||||||
auth: { user, pass },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== "production") {
|
|
||||||
globalForEmail.emailTransport = transport;
|
|
||||||
}
|
|
||||||
|
|
||||||
return transport;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a single email via Brevo SMTP relay.
|
* Send a single transactional email via Brevo REST API.
|
||||||
*/
|
*/
|
||||||
export async function sendEmail(payload: EmailPayload): Promise<void> {
|
export async function sendEmail(payload: EmailPayload): Promise<void> {
|
||||||
|
const apiKey = process.env.BREVO_API_KEY;
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new Error("BREVO_API_KEY must be set for email notifications");
|
||||||
|
}
|
||||||
|
|
||||||
const fromEmail =
|
const fromEmail =
|
||||||
process.env.NOTIFICATION_FROM_EMAIL ?? "noreply@beletage.ro";
|
process.env.NOTIFICATION_FROM_EMAIL ?? "noreply@beletage.ro";
|
||||||
const fromName = process.env.NOTIFICATION_FROM_NAME ?? "ArchiTools";
|
const fromName = process.env.NOTIFICATION_FROM_NAME ?? "ArchiTools";
|
||||||
|
|
||||||
const transport = getTransport();
|
const recipients = payload.to
|
||||||
|
.split(",")
|
||||||
|
.map((addr) => addr.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((email) => ({ email }));
|
||||||
|
|
||||||
await transport.sendMail({
|
if (recipients.length === 0) {
|
||||||
from: `"${fromName}" <${fromEmail}>`,
|
throw new Error("sendEmail: no recipients");
|
||||||
to: payload.to,
|
}
|
||||||
|
|
||||||
|
const res = await fetch(BREVO_ENDPOINT, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
accept: "application/json",
|
||||||
|
"content-type": "application/json",
|
||||||
|
"api-key": apiKey,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
sender: { name: fromName, email: fromEmail },
|
||||||
|
to: recipients,
|
||||||
subject: payload.subject,
|
subject: payload.subject,
|
||||||
html: payload.html,
|
htmlContent: payload.html,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const detail = await res.text().catch(() => "");
|
||||||
|
throw new Error(`Brevo API ${res.status}: ${detail.slice(0, 300)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user