From e281ef75da2470ea38beb91b4878a616f28418a5 Mon Sep 17 00:00:00 2001
From: parsa aghaei
Date: Sat, 8 Aug 2026 17:46:42 +0330
Subject: [PATCH] feat(security/devops): disable swagger in prod, secure
staging with basic auth & noindex, add DB backup script and uploads volume
persistence
---
backend/src/main.ts | 28 ++++++++++++++++++----------
nginx.conf | 29 +++++++++++++++++++++++++++++
scripts/backup_db.sh | 36 ++++++++++++++++++++++++++++++++++++
scripts/compose.prod.yml | 3 +++
scripts/compose.stage.yml | 5 +++++
5 files changed, 91 insertions(+), 10 deletions(-)
create mode 100755 scripts/backup_db.sh
diff --git a/backend/src/main.ts b/backend/src/main.ts
index 160e527..8e8c73a 100644
--- a/backend/src/main.ts
+++ b/backend/src/main.ts
@@ -83,17 +83,25 @@ async function bootstrap() {
app.useGlobalInterceptors(new DecimalInterceptor());
- const config = new DocumentBuilder()
- .setTitle('Canina Iran API')
- .setDescription(
- 'API Documentation for Canina Iran Pet Health & Supplement Platform',
- )
- .setVersion('1.0.0')
- .addBearerAuth()
- .build();
+ const isProduction = process.env.NODE_ENV === 'production';
+ const enableSwagger = process.env.ENABLE_SWAGGER === 'true';
- const document = SwaggerModule.createDocument(app, config);
- SwaggerModule.setup('api/docs', app, document);
+ if (!isProduction || enableSwagger) {
+ const config = new DocumentBuilder()
+ .setTitle('Canina Iran API')
+ .setDescription(
+ 'API Documentation for Canina Iran Pet Health & Supplement Platform',
+ )
+ .setVersion('1.0.0')
+ .addBearerAuth()
+ .build();
+
+ const document = SwaggerModule.createDocument(app, config);
+ SwaggerModule.setup('api/docs', app, document);
+ console.log('Swagger UI is ACTIVE on /api/docs');
+ } else {
+ console.log('Swagger UI is DISABLED for security (production environment)');
+ }
await app.listen(process.env.PORT ?? 4001);
}
diff --git a/nginx.conf b/nginx.conf
index 059625f..e2368d0 100644
--- a/nginx.conf
+++ b/nginx.conf
@@ -9,8 +9,23 @@ server {
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
+ # Block SEO Indexing for Staging Environment
+ if ($host = "stage.canina.ir") {
+ add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive" always;
+ }
+
# Main Application (Next.js)
location / {
+ # Basic Authentication for Staging Environment
+ if ($host = "stage.canina.ir") {
+ set $auth_type "Staging Restricted Area";
+ }
+ if ($host != "stage.canina.ir") {
+ set $auth_type "off";
+ }
+ auth_basic $auth_type;
+ auth_basic_user_file /etc/nginx/.htpasswd;
+
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
@@ -45,8 +60,22 @@ server {
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
+ # Block SEO Indexing for Staging Admin
+ if ($host = "stageadmin.canina.ir") {
+ add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive" always;
+ }
+
# SPA routing for Admin Panel
location / {
+ if ($host = "stageadmin.canina.ir") {
+ set $auth_type "Staging Admin Restricted Area";
+ }
+ if ($host != "stageadmin.canina.ir") {
+ set $auth_type "off";
+ }
+ auth_basic $auth_type;
+ auth_basic_user_file /etc/nginx/.htpasswd;
+
try_files $uri $uri/ /index.html;
}
diff --git a/scripts/backup_db.sh b/scripts/backup_db.sh
new file mode 100755
index 0000000..b39476c
--- /dev/null
+++ b/scripts/backup_db.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+set -eo pipefail
+
+# Configuration
+CONTAINER_NAME="${DB_CONTAINER_NAME:-canino_db_prod}"
+DB_USER="${POSTGRES_USER:-canino_prod}"
+DB_NAME="${POSTGRES_DB:-caninodb_prod}"
+BACKUP_DIR="${BACKUP_DIR:-/var/backups/canina_db}"
+RETENTION_DAYS=7
+
+TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
+BACKUP_FILE="${BACKUP_DIR}/canina_db_${TIMESTAMP}.sql.gz"
+
+echo "[$(date -u +'%Y-%m-%dT%H:%M:%SZ')] Starting automated PostgreSQL backup..."
+echo "Target Container: ${CONTAINER_NAME}"
+echo "Database: ${DB_NAME}"
+echo "Backup Output: ${BACKUP_FILE}"
+
+# Ensure backup directory exists
+mkdir -p "${BACKUP_DIR}"
+
+# Execute compressed database dump
+if docker exec "${CONTAINER_NAME}" pg_dump -U "${DB_USER}" -d "${DB_NAME}" | gzip > "${BACKUP_FILE}"; then
+ BACKUP_SIZE=$(du -h "${BACKUP_FILE}" | cut -f1)
+ echo "[$(date -u +'%Y-%m-%dT%H:%M:%SZ')] Backup completed successfully! File size: ${BACKUP_SIZE}"
+else
+ echo "[$(date -u +'%Y-%m-%dT%H:%M:%SZ')] ERROR: Backup failed!" >&2
+ rm -f "${BACKUP_FILE}"
+ exit 1
+fi
+
+# Purge backups older than RETENTION_DAYS (7 days)
+echo "[$(date -u +'%Y-%m-%dT%H:%M:%SZ')] Purging backups older than ${RETENTION_DAYS} days from ${BACKUP_DIR}..."
+find "${BACKUP_DIR}" -type f -name "canina_db_*.sql.gz" -mtime +"${RETENTION_DAYS}" -exec rm -f {} \; -verbose || true
+
+echo "[$(date -u +'%Y-%m-%dT%H:%M:%SZ')] Backup process completed."
diff --git a/scripts/compose.prod.yml b/scripts/compose.prod.yml
index 05ac34f..cc1e864 100644
--- a/scripts/compose.prod.yml
+++ b/scripts/compose.prod.yml
@@ -34,6 +34,8 @@ services:
- PORT=3000
- PRISMA_SCHEMA_ENGINE_BINARY=/app/node_modules/@prisma/engines/schema-engine-linux-musl-openssl-3.0.x
- PRISMA_QUERY_ENGINE_LIBRARY=/app/node_modules/@prisma/engines/libquery_engine-linux-musl-openssl-3.0.x.so.node
+ volumes:
+ - canina_prod_uploads:/app/uploads
labels:
- "traefik.enable=true"
- "traefik.http.routers.canina-api-prod.rule=Host(`api.canina.ir`)"
@@ -72,3 +74,4 @@ networks:
volumes:
canina_prod_db:
+ canina_prod_uploads:
diff --git a/scripts/compose.stage.yml b/scripts/compose.stage.yml
index 287168a..3bc37bb 100644
--- a/scripts/compose.stage.yml
+++ b/scripts/compose.stage.yml
@@ -34,6 +34,8 @@ services:
- PORT=3000
- PRISMA_SCHEMA_ENGINE_BINARY=/app/node_modules/@prisma/engines/schema-engine-linux-musl-openssl-3.0.x
- PRISMA_QUERY_ENGINE_LIBRARY=/app/node_modules/@prisma/engines/libquery_engine-linux-musl-openssl-3.0.x.so.node
+ volumes:
+ - canina_stage_uploads:/app/uploads
labels:
- "traefik.enable=true"
- "traefik.http.routers.canina-api-stage.rule=Host(`stageapi.canina.ir`)"
@@ -60,6 +62,8 @@ services:
- "traefik.http.routers.canina-stage.rule=Host(`stage.canina.ir`) || Host(`stageadmin.canina.ir`)"
- "traefik.http.routers.canina-stage.entrypoints=websecure"
- "traefik.http.routers.canina-stage.tls=true"
+ - "traefik.http.routers.canina-stage.middlewares=staging-robots-headers"
+ - "traefik.http.middlewares.staging-robots-headers.headers.customresponseheaders.X-Robots-Tag=noindex, nofollow, nosnippet, noarchive"
- "traefik.http.services.canina-stage.loadbalancer.server.port=8080"
depends_on:
- backend_stage
@@ -72,3 +76,4 @@ networks:
volumes:
canina_stage_db:
+ canina_stage_uploads: