feat(security/devops): disable swagger in prod, secure staging with basic auth & noindex, add DB backup script and uploads volume persistence
Some checks failed
Deploy Canina / deploy (push) Failing after 29s

This commit is contained in:
parsa aghaei 2026-08-08 17:46:42 +03:30
parent 9f11d8fb2d
commit e281ef75da
5 changed files with 91 additions and 10 deletions

View File

@ -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);
}

View File

@ -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;
}

36
scripts/backup_db.sh Executable file
View File

@ -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."

View File

@ -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:

View File

@ -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: