From 64c0a58f78b48735e4538b36537f9e558a02dcdd Mon Sep 17 00:00:00 2001 From: paul Date: Mon, 14 Jul 2025 09:46:05 +0200 Subject: [PATCH] fix: database migration and routing issues for production MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add migration to fix email_templates column structure after language migration - Add migration to ensure default email templates exist - Create diagnostic script to check database issues - Fix docker-compose configuration for proper routing without path stripping The backend expects routes with /api prefix, so removing the stripprefix middleware allows proper routing to work. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- TROUBLESHOOTING-TRAEFIK.md | 134 ++++++++++++++++++ .../019_fix_email_templates_columns.js | 35 +++++ .../020_ensure_default_email_templates.js | 68 +++++++++ backend/scripts/check-db-issues.js | 60 ++++++++ docker-compose.fixed.yml | 110 ++++++++++++++ docker-compose.production.yml | 131 +++++++++++++++++ 6 files changed, 538 insertions(+) create mode 100644 TROUBLESHOOTING-TRAEFIK.md create mode 100644 backend/migrations/019_fix_email_templates_columns.js create mode 100644 backend/migrations/020_ensure_default_email_templates.js create mode 100644 backend/scripts/check-db-issues.js create mode 100644 docker-compose.fixed.yml create mode 100644 docker-compose.production.yml diff --git a/TROUBLESHOOTING-TRAEFIK.md b/TROUBLESHOOTING-TRAEFIK.md new file mode 100644 index 0000000..f87ccca --- /dev/null +++ b/TROUBLESHOOTING-TRAEFIK.md @@ -0,0 +1,134 @@ +# Traefik Troubleshooting Guide + +## Common Issues and Solutions + +### 1. 404 Errors on API Routes + +**Problem**: Getting 404 errors when accessing `/api/*` routes + +**Causes**: +- Traefik routing rules not properly configured +- Backend container not healthy +- Path stripping not working correctly + +**Solutions**: + +1. **Check container health**: +```bash +docker ps # Check if backend is running +docker logs picpeak-backend # Check for startup errors +``` + +2. **Test backend directly**: +```bash +# Access backend container +docker exec -it picpeak-backend sh + +# Test health endpoint +wget -O- http://localhost:3000/health + +# Test public settings endpoint +wget -O- http://localhost:3000/public/settings +``` + +3. **Check Traefik routing**: +```bash +# Check if routes are registered in Traefik +curl https://traefik.yourdomain.com/api/http/routers | jq '.[] | select(.rule | contains("picpeak"))' +``` + +### 2. Backend Not Accessible Through Traefik + +**Key Configuration Points**: + +1. **Traefik Labels** (in deploy section): + - `traefik.enable=true` - Enable Traefik for this container + - `traefik.docker.network=proxy` - Specify which network Traefik should use + - `traefik.http.routers.picpeak-backend.priority=100` - Higher priority for API routes + +2. **Path Stripping**: + - Frontend expects `/api/*` but backend serves routes without `/api` prefix + - Middleware strips `/api` before forwarding to backend + +3. **Network Configuration**: + - Backend must be in both `picpeak` (internal) and `proxy` (Traefik) networks + +### 3. Environment Variable Issues + +**Critical Variables**: +- `ADMIN_URL` and `FRONTEND_URL` must match your actual domain +- These affect CORS configuration + +**Example .env**: +```env +# URLs +ADMIN_URL=https://picpeak.local.nothaft.cloud +FRONTEND_URL=https://picpeak.local.nothaft.cloud + +# Database +DB_USER=picpeak +DB_PASSWORD=your_secure_password +DB_NAME=picpeak + +# JWT +JWT_SECRET=your_secure_jwt_secret + +# Email (optional) +SMTP_HOST=smtp.example.com +SMTP_PORT=587 +SMTP_SECURE=true +SMTP_USER=noreply@example.com +SMTP_PASS=smtp_password +EMAIL_FROM=noreply@example.com +``` + +### 4. Debugging Steps + +1. **Check if backend is receiving requests**: +```bash +# Watch backend logs +docker logs -f picpeak-backend + +# Look for incoming requests when you try to access the admin page +``` + +2. **Test API routes directly**: +```bash +# From outside +curl -v https://picpeak.local.nothaft.cloud/api/public/settings + +# Should see backend logs if request reaches container +``` + +3. **Verify Traefik middleware**: +```bash +# Check if stripprefix middleware exists +curl https://traefik.yourdomain.com/api/http/middlewares | jq '.[] | select(.name | contains("picpeak"))' +``` + +### 5. Quick Fix Checklist + +- [ ] Backend container is healthy (`docker ps`) +- [ ] Backend is in both networks (`docker inspect picpeak-backend | grep -A 20 Networks`) +- [ ] Traefik labels use correct network (`traefik.docker.network=proxy`) +- [ ] Priority is set correctly (backend: 100, frontend: 10) +- [ ] ADMIN_URL and FRONTEND_URL match your domain +- [ ] Database is accessible from backend +- [ ] Migrations have run successfully + +### 6. Alternative Testing + +If Traefik routing is problematic, test backend directly: + +```bash +# Port forward to test backend directly +docker run --rm -it --network picpeak alpine/curl curl http://backend:3000/health + +# Or expose backend port temporarily +docker run -d --name picpeak-backend-test \ + --network picpeak \ + -p 3001:3000 \ + registry.local.nothaft.cloud/picpeak-backend:latest +``` + +Then access http://localhost:3001/health to verify backend is working. \ No newline at end of file diff --git a/backend/migrations/019_fix_email_templates_columns.js b/backend/migrations/019_fix_email_templates_columns.js new file mode 100644 index 0000000..bee9f9d --- /dev/null +++ b/backend/migrations/019_fix_email_templates_columns.js @@ -0,0 +1,35 @@ +exports.up = async function(knex) { + // Check current column structure + const hasSubjectEn = await knex.schema.hasColumn('email_templates', 'subject_en'); + const hasSubject = await knex.schema.hasColumn('email_templates', 'subject'); + + if (hasSubjectEn && !hasSubject) { + // The language migration was applied, need to add back basic columns + await knex.schema.alterTable('email_templates', function(table) { + table.string('subject'); + table.text('body_html'); + table.text('body_text'); + }); + + // Copy English values to the basic columns + await knex('email_templates').update({ + subject: knex.raw('subject_en'), + body_html: knex.raw('body_html_en'), + body_text: knex.raw('body_text_en') + }); + } +}; + +exports.down = async function(knex) { + // Check if we have the basic columns + const hasSubject = await knex.schema.hasColumn('email_templates', 'subject'); + const hasSubjectEn = await knex.schema.hasColumn('email_templates', 'subject_en'); + + if (hasSubject && hasSubjectEn) { + await knex.schema.alterTable('email_templates', function(table) { + table.dropColumn('subject'); + table.dropColumn('body_html'); + table.dropColumn('body_text'); + }); + } +}; \ No newline at end of file diff --git a/backend/migrations/020_ensure_default_email_templates.js b/backend/migrations/020_ensure_default_email_templates.js new file mode 100644 index 0000000..3e0892e --- /dev/null +++ b/backend/migrations/020_ensure_default_email_templates.js @@ -0,0 +1,68 @@ +exports.up = async function(knex) { + // Check if we have the default email templates + const templates = await knex('email_templates').select('template_key'); + const existingKeys = templates.map(t => t.template_key); + + const defaultTemplates = [ + { + template_key: 'gallery_created', + subject: 'Your Photo Gallery is Ready!', + body_html: `

Gallery Created Successfully

+

Dear {{host_name}},

+

Your photo gallery "{{event_name}}" has been created successfully!

+

Gallery Details:

+ +

Share this link and password with your guests to allow them to view and download photos.

`, + body_text: 'Gallery Created Successfully\n\nDear {{host_name}},\n\nYour photo gallery "{{event_name}}" has been created successfully!', + variables: JSON.stringify(['host_name', 'event_name', 'event_date', 'gallery_link', 'gallery_password', 'expiry_date']) + }, + { + template_key: 'expiration_warning', + subject: 'Your Photo Gallery Expires Soon', + body_html: `

Gallery Expiring Soon

+

Dear {{host_name}},

+

Your photo gallery "{{event_name}}" will expire in {{days_remaining}} days.

+

After expiration, the gallery will be archived and no longer accessible to guests.

+

Visit Gallery

`, + body_text: 'Gallery Expiring Soon\n\nDear {{host_name}},\n\nYour photo gallery "{{event_name}}" will expire in {{days_remaining}} days.', + variables: JSON.stringify(['host_name', 'event_name', 'days_remaining', 'gallery_link']) + }, + { + template_key: 'gallery_expired', + subject: 'Your Photo Gallery Has Expired', + body_html: `

Gallery Expired

+

Dear {{host_name}},

+

Your photo gallery "{{event_name}}" has expired and been archived.

+

The photos are safely stored in our archive system. If you need access to the archived photos, please contact support.

`, + body_text: 'Gallery Expired\n\nDear {{host_name}},\n\nYour photo gallery "{{event_name}}" has expired and been archived.', + variables: JSON.stringify(['host_name', 'event_name']) + }, + { + template_key: 'archive_complete', + subject: 'Gallery Archive Complete', + body_html: `

Archive Complete

+

Dear {{host_name}},

+

Your photo gallery "{{event_name}}" has been successfully archived.

+

Archive size: {{archive_size}}

+

The archive is stored securely and can be retrieved if needed.

`, + body_text: 'Archive Complete\n\nDear {{host_name}},\n\nYour photo gallery "{{event_name}}" has been successfully archived.', + variables: JSON.stringify(['host_name', 'event_name', 'archive_size']) + } + ]; + + // Insert missing templates + for (const template of defaultTemplates) { + if (!existingKeys.includes(template.template_key)) { + await knex('email_templates').insert(template); + } + } +}; + +exports.down = async function(knex) { + // Don't remove templates on rollback as they might have been customized +}; \ No newline at end of file diff --git a/backend/scripts/check-db-issues.js b/backend/scripts/check-db-issues.js new file mode 100644 index 0000000..a2b6fec --- /dev/null +++ b/backend/scripts/check-db-issues.js @@ -0,0 +1,60 @@ +require('dotenv').config(); +const { db } = require('../src/database/db'); + +async function checkDatabaseIssues() { + console.log('Checking database issues...\n'); + + try { + // Check email_templates table structure + console.log('1. Checking email_templates table structure:'); + const emailTemplateColumns = await db('email_templates').columnInfo(); + console.log('Columns:', Object.keys(emailTemplateColumns)); + + // Check if any templates exist + const templateCount = await db('email_templates').count('* as count'); + console.log('Template count:', templateCount[0].count); + + // Check for specific template + const galleryCreatedTemplate = await db('email_templates') + .where('template_key', 'gallery_created') + .first(); + console.log('gallery_created template exists:', !!galleryCreatedTemplate); + + // Check activity_logs table + console.log('\n2. Checking activity_logs table:'); + const activityLogColumns = await db('activity_logs').columnInfo(); + console.log('Columns:', Object.keys(activityLogColumns)); + + // Check migrations table + console.log('\n3. Checking migrations status:'); + const migrations = await db('migrations') + .orderBy('id', 'desc') + .limit(10); + console.log('Latest migrations:'); + migrations.forEach(m => console.log(` - ${m.filename}`)); + + // Test a simple query from notifications route + console.log('\n4. Testing notifications query:'); + try { + const notifications = await db('activity_logs') + .select( + 'activity_logs.*', + 'events.event_name' + ) + .leftJoin('events', 'activity_logs.event_id', 'events.id') + .orderBy('activity_logs.created_at', 'desc') + .limit(5); + console.log(`Found ${notifications.length} notifications`); + } catch (error) { + console.error('Notifications query failed:', error.message); + } + + } catch (error) { + console.error('Error:', error); + } finally { + await db.destroy(); + process.exit(0); + } +} + +checkDatabaseIssues(); \ No newline at end of file diff --git a/docker-compose.fixed.yml b/docker-compose.fixed.yml new file mode 100644 index 0000000..511801e --- /dev/null +++ b/docker-compose.fixed.yml @@ -0,0 +1,110 @@ +version: '3.8' +services: + backend: + image: 'registry.local.nothaft.cloud/picpeak-backend:latest' + restart: unless-stopped + environment: + - NODE_ENV=production + - DATABASE_CLIENT=pg + - DB_HOST=db + - DB_PORT=5432 + - DB_USER=${DB_USER:-picpeak} + - DB_PASSWORD=${DB_PASSWORD} + - DB_NAME=${DB_NAME:-picpeak} + - PORT=3000 + - JWT_SECRET=${JWT_SECRET} + - ADMIN_URL=https://picpeak.local.nothaft.cloud + - FRONTEND_URL=https://picpeak.local.nothaft.cloud + - SMTP_HOST=${SMTP_HOST} + - SMTP_PORT=${SMTP_PORT} + - SMTP_SECURE=${SMTP_SECURE} + - SMTP_USER=${SMTP_USER} + - SMTP_PASS=${SMTP_PASS} + - EMAIL_FROM=${EMAIL_FROM} + - UMAMI_URL=${UMAMI_URL} + - UMAMI_WEBSITE_ID=${UMAMI_WEBSITE_ID} + - STORAGE_PATH=/app/storage + - EVENTS_PATH=/app/storage/events + - ARCHIVE_PATH=/app/storage/events/archived + volumes: + - '/mnt/DockerMount/picpeak/storage:/app/storage' + - '/mnt/DockerMount/picpeak/data:/app/data' + - '/mnt/DockerMount/picpeak/logs:/app/logs' + networks: + - picpeak + - proxy + deploy: + labels: + - traefik.enable=true + - traefik.docker.network=proxy + # Backend API routing WITHOUT path stripping + - 'traefik.http.routers.picpeak-backend.rule=(Host(`picpeak.nothaft.cloud`) || Host(`picpeak.local.nothaft.cloud`)) && PathPrefix(`/api`)' + - traefik.http.routers.picpeak-backend.entrypoints=https + - traefik.http.routers.picpeak-backend.tls.certresolver=dns + - traefik.http.services.picpeak-backend.loadbalancer.server.port=3000 + # Remove the stripprefix middleware - backend expects /api prefix + # - traefik.http.middlewares.picpeak-stripprefix.stripprefix.prefixes=/api + # - traefik.http.routers.picpeak-backend.middlewares=picpeak-stripprefix + - traefik.http.routers.picpeak-backend.priority=100 + - homepage.group=Public Services + - homepage.name=PicPeak Backend + - homepage.icon=mdi-api + - 'homepage.href=https://picpeak.local.nothaft.cloud/api/health' + - homepage.description=PicPeak API Backend + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + depends_on: + - db + + frontend: + image: 'registry.local.nothaft.cloud/picpeak-frontend:latest' + restart: unless-stopped + depends_on: + - backend + networks: + - picpeak + - proxy + deploy: + labels: + - traefik.enable=true + - traefik.docker.network=proxy + - traefik.http.routers.picpeak.rule=Host(`picpeak.nothaft.cloud`) || Host(`picpeak.local.nothaft.cloud`) + - traefik.http.routers.picpeak.entrypoints=https + - traefik.http.routers.picpeak.tls.certresolver=dns + - traefik.http.services.picpeak.loadbalancer.server.port=80 + - traefik.http.routers.picpeak.priority=10 + - homepage.group=Public Services + - homepage.name=PicPeak + - homepage.icon=mdi-photo + - 'homepage.href=https://picpeak.local.nothaft.cloud/' + - homepage.description=Photo Sharing System + + db: + image: 'postgres:14-alpine' + restart: unless-stopped + environment: + - POSTGRES_USER=${DB_USER:-picpeak} + - POSTGRES_PASSWORD=${DB_PASSWORD} + - POSTGRES_DB=${DB_NAME:-picpeak} + - POSTGRES_HOST_AUTH_METHOD=${PG_AUTH_METHOD:-scram-sha-256} + - POSTGRES_INITDB_ARGS=${PG_INIT_ARGS:---auth-host=scram-sha-256 --auth-local=trust} + volumes: + - '/mnt/DockerMount/picpeak/db:/var/lib/postgresql/data' + networks: + - picpeak + command: ${PG_COMMANDS:-postgres -c ssl=off} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-picpeak}"] + interval: 10s + timeout: 5s + retries: 5 + +networks: + proxy: + external: true + picpeak: + driver: bridge \ No newline at end of file diff --git a/docker-compose.production.yml b/docker-compose.production.yml new file mode 100644 index 0000000..6b3ce78 --- /dev/null +++ b/docker-compose.production.yml @@ -0,0 +1,131 @@ +version: '3.8' +services: + backend: + image: 'registry.local.nothaft.cloud/picpeak-backend:latest' + restart: unless-stopped + environment: + - NODE_ENV=production + - DATABASE_CLIENT=pg + - DB_HOST=db + - DB_PORT=5432 + - DB_USER=${DB_USER:-picpeak} + - DB_PASSWORD=${DB_PASSWORD} + - DB_NAME=${DB_NAME:-picpeak} + - PORT=3000 + - JWT_SECRET=${JWT_SECRET} + - ADMIN_URL=https://picpeak.local.nothaft.cloud + - FRONTEND_URL=https://picpeak.local.nothaft.cloud + - SMTP_HOST=${SMTP_HOST} + - SMTP_PORT=${SMTP_PORT} + - SMTP_SECURE=${SMTP_SECURE} + - SMTP_USER=${SMTP_USER} + - SMTP_PASS=${SMTP_PASS} + - EMAIL_FROM=${EMAIL_FROM} + - UMAMI_URL=${UMAMI_URL} + - UMAMI_WEBSITE_ID=${UMAMI_WEBSITE_ID} + - STORAGE_PATH=/app/storage + - EVENTS_PATH=/app/storage/events + - ARCHIVE_PATH=/app/storage/events/archived + volumes: + - '/mnt/DockerMount/picpeak/storage:/app/storage' + - '/mnt/DockerMount/picpeak/data:/app/data' + - '/mnt/DockerMount/picpeak/logs:/app/logs' + networks: + - picpeak + - proxy + deploy: + labels: + - traefik.enable=true + - traefik.docker.network=proxy + # Backend API routing + - 'traefik.http.routers.picpeak-backend.rule=(Host(`picpeak.nothaft.cloud`) || Host(`picpeak.local.nothaft.cloud`)) && PathPrefix(`/api`)' + - traefik.http.routers.picpeak-backend.entrypoints=https + - traefik.http.routers.picpeak-backend.tls=true + - traefik.http.routers.picpeak-backend.tls.certresolver=dns + - traefik.http.services.picpeak-backend.loadbalancer.server.port=3000 + # Strip /api prefix when forwarding to backend + - traefik.http.middlewares.picpeak-stripprefix.stripprefix.prefixes=/api + - traefik.http.routers.picpeak-backend.middlewares=picpeak-stripprefix + # Higher priority for API routes + - traefik.http.routers.picpeak-backend.priority=100 + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + depends_on: + - db + + frontend: + image: 'registry.local.nothaft.cloud/picpeak-frontend:latest' + restart: unless-stopped + depends_on: + - backend + networks: + - picpeak + - proxy + deploy: + labels: + - traefik.enable=true + - traefik.docker.network=proxy + # Frontend routing (catch-all for non-API routes) + - traefik.http.routers.picpeak.rule=Host(`picpeak.nothaft.cloud`) || Host(`picpeak.local.nothaft.cloud`) + - traefik.http.routers.picpeak.entrypoints=https + - traefik.http.routers.picpeak.tls=true + - traefik.http.routers.picpeak.tls.certresolver=dns + - traefik.http.services.picpeak.loadbalancer.server.port=80 + # Lower priority than backend to ensure /api routes go to backend + - traefik.http.routers.picpeak.priority=10 + + db: + image: 'postgres:14-alpine' + restart: unless-stopped + environment: + - POSTGRES_USER=${DB_USER:-picpeak} + - POSTGRES_PASSWORD=${DB_PASSWORD} + - POSTGRES_DB=${DB_NAME:-picpeak} + - POSTGRES_HOST_AUTH_METHOD=scram-sha-256 + - POSTGRES_INITDB_ARGS=--auth-host=scram-sha-256 --auth-local=trust + volumes: + - '/mnt/DockerMount/picpeak/db:/var/lib/postgresql/data' + # Mount init script to create umami database + - ./docker/postgres-init:/docker-entrypoint-initdb.d:ro + networks: + - picpeak + command: postgres -c ssl=off + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-picpeak}"] + interval: 10s + timeout: 5s + retries: 5 + + # Optional: Umami analytics + umami: + image: ghcr.io/umami-software/umami:postgresql-latest + restart: unless-stopped + environment: + DATABASE_URL: postgresql://${DB_USER:-picpeak}:${DB_PASSWORD}@db:5432/umami + DATABASE_TYPE: postgresql + HASH_SALT: ${UMAMI_HASH_SALT} + depends_on: + db: + condition: service_healthy + networks: + - picpeak + - proxy + deploy: + labels: + - traefik.enable=true + - traefik.docker.network=proxy + - traefik.http.routers.picpeak-umami.rule=Host(`analytics.picpeak.local.nothaft.cloud`) + - traefik.http.routers.picpeak-umami.entrypoints=https + - traefik.http.routers.picpeak-umami.tls=true + - traefik.http.routers.picpeak-umami.tls.certresolver=dns + - traefik.http.services.picpeak-umami.loadbalancer.server.port=3000 + +networks: + proxy: + external: true + picpeak: + driver: bridge \ No newline at end of file