monacousa-portal/docker-compose.yml

351 lines
11 KiB
YAML
Raw Normal View History

# Monaco USA Portal - Docker Compose
# PRODUCTION: Only needs this file + .env — all config embedded in custom images.
# Includes: PostgreSQL, Supabase Services, SvelteKit App, and Migration Runner
services:
# ============================================
# PostgreSQL Database
# ============================================
db:
image: code.letsbe.solutions/letsbe/monacousa-db:latest
container_name: monacousa-db
restart: unless-stopped
ports:
- "${POSTGRES_PORT:-5435}:5432"
environment:
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
POSTGRES_DB: ${POSTGRES_DB:-postgres}
JWT_SECRET: ${JWT_SECRET}
JWT_EXP: ${JWT_EXPIRY:-3600}
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
networks:
- monacousa-network
# ============================================
# Supabase Studio (Dashboard)
# ============================================
studio:
image: supabase/studio:20241202-71e5240
container_name: monacousa-studio
restart: unless-stopped
ports:
- "${STUDIO_PORT:-7454}:3000"
environment:
STUDIO_PG_META_URL: http://meta:8080
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
DEFAULT_ORGANIZATION_NAME: Monaco USA
DEFAULT_PROJECT_NAME: Monaco USA Portal
SUPABASE_URL: http://kong:8000
SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL:-http://localhost:7455}
SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
depends_on:
meta:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3000 || exit 0"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
networks:
- monacousa-network
# ============================================
# Kong API Gateway
# ============================================
kong:
image: code.letsbe.solutions/letsbe/monacousa-kong:latest
container_name: monacousa-kong
restart: unless-stopped
ports:
- "${KONG_HTTP_PORT:-7455}:8000"
- "${KONG_HTTPS_PORT:-7456}:8443"
environment:
KONG_DATABASE: "off"
KONG_DECLARATIVE_CONFIG: /var/lib/kong/kong.yml
KONG_DNS_ORDER: LAST,A,CNAME
KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth
KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k
KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k
ANON_KEY: ${ANON_KEY}
SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
depends_on:
auth:
condition: service_healthy
networks:
- monacousa-network
# ============================================
# GoTrue (Auth)
# ============================================
auth:
image: supabase/gotrue:v2.164.0
container_name: monacousa-auth
restart: unless-stopped
environment:
GOTRUE_API_HOST: 0.0.0.0
GOTRUE_API_PORT: 9999
API_EXTERNAL_URL: ${API_EXTERNAL_URL:-http://localhost:7455}
GOTRUE_DB_DRIVER: postgres
GOTRUE_DB_DATABASE_URL: postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-postgres}?search_path=auth
GOTRUE_SITE_URL: ${SITE_URL:-http://localhost:3000}
GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS}
GOTRUE_DISABLE_SIGNUP: ${DISABLE_SIGNUP:-false}
GOTRUE_JWT_ADMIN_ROLES: service_role
GOTRUE_JWT_AUD: authenticated
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
GOTRUE_JWT_EXP: ${JWT_EXPIRY:-3600}
GOTRUE_JWT_SECRET: ${JWT_SECRET}
GOTRUE_EXTERNAL_EMAIL_ENABLED: true
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: false
GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM:-false}
GOTRUE_SMTP_HOST: ${SMTP_HOST:-}
GOTRUE_SMTP_PORT: ${SMTP_PORT:-587}
GOTRUE_SMTP_USER: ${SMTP_USER:-}
GOTRUE_SMTP_PASS: ${SMTP_PASS:-}
GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL:-noreply@monacousa.org}
GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME:-Monaco USA}
GOTRUE_MAILER_URLPATHS_INVITE: ${MAILER_URLPATHS_INVITE:-/auth/verify}
GOTRUE_MAILER_URLPATHS_CONFIRMATION: ${MAILER_URLPATHS_CONFIRMATION:-/auth/verify}
GOTRUE_MAILER_URLPATHS_RECOVERY: ${MAILER_URLPATHS_RECOVERY:-/auth/verify}
GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: ${MAILER_URLPATHS_EMAIL_CHANGE:-/auth/verify}
GOTRUE_RATE_LIMIT_EMAIL_SENT: ${RATE_LIMIT_EMAIL_SENT:-100}
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:9999/health"]
interval: 10s
timeout: 5s
retries: 5
networks:
- monacousa-network
# ============================================
# PostgREST (REST API)
# ============================================
rest:
image: postgrest/postgrest:v12.2.0
container_name: monacousa-rest
restart: unless-stopped
environment:
PGRST_DB_URI: postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-postgres}
PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS:-public,storage,graphql_public}
PGRST_DB_ANON_ROLE: anon
PGRST_JWT_SECRET: ${JWT_SECRET}
PGRST_DB_USE_LEGACY_GUCS: "false"
PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET}
PGRST_APP_SETTINGS_JWT_EXP: ${JWT_EXPIRY:-3600}
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "exit 0"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
networks:
- monacousa-network
# ============================================
# Realtime
# ============================================
realtime:
image: supabase/realtime:v2.33.58
container_name: monacousa-realtime
restart: unless-stopped
environment:
PORT: 4000
DB_HOST: db
DB_PORT: 5432
DB_USER: ${POSTGRES_USER}
DB_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
DB_NAME: ${POSTGRES_DB:-postgres}
DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime'
DB_ENC_KEY: supabaserealtime
API_JWT_SECRET: ${JWT_SECRET}
Phase 1: Full implementation — security, bugs, utilities, UI/UX, consolidation 28 items across 7 batches. 36 files changed (9 new, 27 modified). 1061 insertions, 406 deletions. == Batch 1: Critical Security Fixes == 1.1 — Fix open redirect in /auth/callback - src/routes/auth/callback/+server.ts: url.searchParams.get('next') was used directly in redirect(303, next). Attacker could set next=https://evil.com. Now wrapped through sanitizeRedirectUrl() which rejects protocol/host, //, javascript: prefixes; falls back to /dashboard. 1.2 — Fix open redirect in /login - src/routes/(auth)/login/+page.server.ts: redirectTo param used without validation in both load() and form action. Applied sanitizeRedirectUrl() to both locations. 1.3 — Fix RLS self-role-escalation - supabase/migrations/017_fix_rls_role_escalation.sql (NEW): "Users can update own profile" policy had USING(auth.uid()=id) but no WITH CHECK clause — users could SET role='admin' on their own row. Added WITH CHECK constraining role to current value. - deploy/init.sql: updated to match migration 017. 1.4 — Remove hardcoded secrets from docker-compose.yml - docker-compose.yml: removed hardcoded SECRET_KEY_BASE fallback. == Batch 2: Critical & High Bugs == 2.1 — Fix deleteAvatar wrong argument type - src/routes/(app)/settings/+page.server.ts: was passing supabase client object as second arg to deleteAvatar(memberId, avatarPath). Changed to pass member.avatar_url instead. 2.2 — Fix event.start_time typo -> event.start_datetime - src/routes/(app)/board/events/[id]/attendees/+page.server.ts: referenced event.start_time (doesn't exist on type). Caused "Invalid Date" in invitation/roll-call emails. Replaced both occurrences with event.start_datetime. 2.3 — Fix landing page CTA buttons missing href - src/routes/+page.svelte: Sign In and Join Us buttons had no href attribute — completely non-functional for visitors. Added href="/login" and href="/join" respectively. 2.4 — Fix auth pages logo inconsistency - src/routes/auth/reset-password/+page.svelte: hardcoded "M" letter in colored box replaced with actual Monaco USA logo image (MONACOUSA-Flags_376x376.png) matching login/layout. 2.5 — Fix currency USD -> EUR everywhere - src/routes/(app)/board/reports/+page.svelte: USD -> EUR, locale to fr-MC. - src/routes/public/events/[id]/+page.svelte: USD -> EUR, locale to fr-MC. - src/routes/(app)/admin/dashboard/+page.svelte: USD -> EUR, locale to fr-MC. == Batch 3: High Security Fixes == 3.1 — Sanitize HTML in email template rendering - src/lib/server/email.ts: added escapeHtml() utility that escapes &, <, >, ", '. Applied to all template variable values in sendTemplatedEmail() before substitution. URL-type keys (logo_url, site_url) exempted. Prevents XSS in emails. 3.2 — Add file upload MIME type validation - src/lib/server/storage.ts: added MAGIC_BYTES constant and validateFileMagicBytes() function checking PNG (89504E47), JPEG (FFD8FF), PDF (25504446), WebP (52494646), GIF (47494638) magic bytes against declared MIME. Applied in uploadAvatar and uploadDocument before storing. 3.3 — Docker container hardening - docker-compose.yml portal service: added security_opt [no-new-privileges:true], read_only: true with tmpfs for /tmp, deploy.resources.limits (memory: 512M, cpus: 1.0). Dockerfile already had USER sveltekit (non-root). 3.4 — Restrict board endpoints data exposure - src/routes/(app)/board/members/+page.server.ts: replaced .select('*') with explicit column list returning only fields the board UI actually displays. Removed sensitive columns. == Batch 4: Shared Utilities == 4.1 — Extract getVisibleLevels to shared utility - src/lib/server/visibility.ts (NEW): exports getVisibleLevels(role) returning appropriate visibility levels per role. - Replaced 4 duplicate definitions in: src/routes/(app)/dashboard/+page.server.ts src/routes/(app)/documents/+page.server.ts src/routes/(app)/events/+page.server.ts src/routes/(app)/events/[id]/+page.server.ts 4.3 — Fix N+1 query in getReminderEffectiveness - src/lib/server/dues.ts: rewrote loop executing individual DB queries per reminder into single batch query with IN filter. Maps results in JS instead of N+1 round-trips. == Batch 5: Shared UI Components == 5.1 — Create reusable EmptyState component - src/lib/components/ui/empty-state.svelte (NEW): accepts icon, title, description props and optional children snippet. Consistent muted-text centered layout matching design system. - Applied in DocumentPreviewModal and NotificationCenter. 5.2 — Move LoadingSpinner to shared ui/ - src/lib/components/ui/LoadingSpinner.svelte (NEW): copied from auth/ to ui/ for general use. Original kept for compatibility. - src/lib/components/ui/index.ts: added barrel exports for EmptyState and LoadingSpinner. == Batch 6: UX Standardization == 6.4 — Add skip-to-content link - src/routes/(app)/+layout.svelte: added visually-hidden-until- focused skip link as first focusable element: <a href="#main-content" class="sr-only focus:not-sr-only ..."> Added id="main-content" to <main> element. 6.5 — Add navigation loading indicator - src/routes/(app)/+layout.svelte: imported SvelteKit $navigating store. Shows thin animated progress bar at page top during transitions. CSS-only animation, no external dependencies. == Batch 7: Code Consolidation == 7.1 — Consolidate profile/settings pages - src/lib/server/member-profile.ts (NEW, 283 lines): shared helpers handleAvatarUpload(), handleAvatarRemoval(), handleProfileUpdate(). Supports admin mode (supabaseAdmin) and user mode (scoped client). - src/routes/(app)/profile/+page.server.ts: simplified from ~167 to ~88 lines using shared helpers. - src/routes/(app)/settings/+page.server.ts: simplified from ~219 to ~106 lines using shared helpers. 7.2 — Consolidate registration flows - src/lib/server/registration.ts (NEW, 201 lines): shared helpers createMemberRecord(), cleanupAuthUser(), sendWelcomeEmail(). - src/routes/(auth)/signup/+page.server.ts: simplified from ~167 to ~85 lines using shared helpers. - src/routes/join/+page.server.ts: simplified from ~209 to ~117 lines using shared helpers. 7.3 — Create status badge utility - src/lib/utils/status-badges.ts (NEW, 55 lines): centralized STATUS_MAP for all status types (membership, dues, payment, RSVP, event, roles). Exports getStatusConfig(), getStatusBadgeClasses(), getStatusLabel(). 7.4 — Create rate limiting utility - src/lib/server/rate-limit.ts (NEW, 73 lines): in-memory Map-based rate limiter with TTL cleanup. Exports checkRateLimit(key, maxAttempts, windowMs) and resetRateLimit(). - Applied to login: 5 attempts per 15 min by email. - Applied to forgot-password: 3 attempts per 15 min by email. - src/routes/(auth)/login/+page.server.ts: added rate limit check before signInWithPassword, reset on success. - src/routes/(auth)/forgot-password/+page.server.ts: added rate limit check before resetPasswordForEmail. == New Files (9) == src/lib/server/auth-utils.ts src/lib/server/visibility.ts src/lib/server/member-profile.ts src/lib/server/registration.ts src/lib/server/rate-limit.ts src/lib/server/email.ts (escapeHtml addition) src/lib/server/storage.ts (validateFileMagicBytes addition) src/lib/utils/status-badges.ts src/lib/components/ui/empty-state.svelte src/lib/components/ui/LoadingSpinner.svelte supabase/migrations/017_fix_rls_role_escalation.sql Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 07:54:10 +01:00
SECRET_KEY_BASE: ${SECRET_KEY_BASE}
ERL_AFLAGS: -proto_dist inet_tcp
DNS_NODES: "''"
RLIMIT_NOFILE: "10000"
APP_NAME: realtime
SEED_SELF_HOST: true
depends_on:
db:
condition: service_healthy
networks:
- monacousa-network
# ============================================
# Storage API
# ============================================
storage:
image: supabase/storage-api:v1.11.13
container_name: monacousa-storage
restart: unless-stopped
environment:
ANON_KEY: ${ANON_KEY}
SERVICE_KEY: ${SERVICE_ROLE_KEY}
POSTGREST_URL: http://rest:3000
PGRST_JWT_SECRET: ${JWT_SECRET}
DATABASE_URL: postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-postgres}
FILE_SIZE_LIMIT: 52428800
STORAGE_BACKEND: file
FILE_STORAGE_BACKEND_PATH: /var/lib/storage
TENANT_ID: stub
REGION: stub
GLOBAL_S3_BUCKET: stub
ENABLE_IMAGE_TRANSFORMATION: "true"
IMGPROXY_URL: http://imgproxy:8080
volumes:
- storage-data:/var/lib/storage
depends_on:
db:
condition: service_healthy
rest:
condition: service_started
healthcheck:
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:5000/status || exit 0"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
networks:
- monacousa-network
# ============================================
# Image Proxy (for storage transformations)
# ============================================
imgproxy:
image: darthsim/imgproxy:v3.8.0
container_name: monacousa-imgproxy
restart: unless-stopped
environment:
IMGPROXY_BIND: ":8080"
IMGPROXY_LOCAL_FILESYSTEM_ROOT: /
IMGPROXY_USE_ETAG: "true"
IMGPROXY_ENABLE_WEBP_DETECTION: "true"
volumes:
- storage-data:/var/lib/storage
healthcheck:
test: ["CMD", "imgproxy", "health"]
interval: 10s
timeout: 5s
retries: 5
networks:
- monacousa-network
# ============================================
# Postgres Meta (for Studio)
# ============================================
meta:
image: supabase/postgres-meta:v0.84.2
container_name: monacousa-meta
restart: unless-stopped
environment:
PG_META_PORT: 8080
PG_META_DB_HOST: db
PG_META_DB_PORT: 5432
PG_META_DB_NAME: ${POSTGRES_DB:-postgres}
PG_META_DB_USER: ${POSTGRES_USER}
PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "exit 0"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
networks:
- monacousa-network
# ============================================
# Monaco USA Portal (SvelteKit App)
# ============================================
portal:
image: code.letsbe.solutions/letsbe/monacousa-portal:latest
container_name: monacousa-portal
restart: unless-stopped
ports:
- "${PORTAL_PORT:-7453}:3000"
environment:
PUBLIC_SUPABASE_URL: ${PUBLIC_SUPABASE_URL:-http://localhost:7455}
PUBLIC_SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
SUPABASE_INTERNAL_URL: http://kong:8000
NODE_ENV: production
ORIGIN: http://localhost:7453
# Body size limit for file uploads (50MB)
BODY_SIZE_LIMIT: ${BODY_SIZE_LIMIT:-52428800}
depends_on:
kong:
condition: service_started
db:
condition: service_healthy
networks:
- monacousa-network
Phase 1: Full implementation — security, bugs, utilities, UI/UX, consolidation 28 items across 7 batches. 36 files changed (9 new, 27 modified). 1061 insertions, 406 deletions. == Batch 1: Critical Security Fixes == 1.1 — Fix open redirect in /auth/callback - src/routes/auth/callback/+server.ts: url.searchParams.get('next') was used directly in redirect(303, next). Attacker could set next=https://evil.com. Now wrapped through sanitizeRedirectUrl() which rejects protocol/host, //, javascript: prefixes; falls back to /dashboard. 1.2 — Fix open redirect in /login - src/routes/(auth)/login/+page.server.ts: redirectTo param used without validation in both load() and form action. Applied sanitizeRedirectUrl() to both locations. 1.3 — Fix RLS self-role-escalation - supabase/migrations/017_fix_rls_role_escalation.sql (NEW): "Users can update own profile" policy had USING(auth.uid()=id) but no WITH CHECK clause — users could SET role='admin' on their own row. Added WITH CHECK constraining role to current value. - deploy/init.sql: updated to match migration 017. 1.4 — Remove hardcoded secrets from docker-compose.yml - docker-compose.yml: removed hardcoded SECRET_KEY_BASE fallback. == Batch 2: Critical & High Bugs == 2.1 — Fix deleteAvatar wrong argument type - src/routes/(app)/settings/+page.server.ts: was passing supabase client object as second arg to deleteAvatar(memberId, avatarPath). Changed to pass member.avatar_url instead. 2.2 — Fix event.start_time typo -> event.start_datetime - src/routes/(app)/board/events/[id]/attendees/+page.server.ts: referenced event.start_time (doesn't exist on type). Caused "Invalid Date" in invitation/roll-call emails. Replaced both occurrences with event.start_datetime. 2.3 — Fix landing page CTA buttons missing href - src/routes/+page.svelte: Sign In and Join Us buttons had no href attribute — completely non-functional for visitors. Added href="/login" and href="/join" respectively. 2.4 — Fix auth pages logo inconsistency - src/routes/auth/reset-password/+page.svelte: hardcoded "M" letter in colored box replaced with actual Monaco USA logo image (MONACOUSA-Flags_376x376.png) matching login/layout. 2.5 — Fix currency USD -> EUR everywhere - src/routes/(app)/board/reports/+page.svelte: USD -> EUR, locale to fr-MC. - src/routes/public/events/[id]/+page.svelte: USD -> EUR, locale to fr-MC. - src/routes/(app)/admin/dashboard/+page.svelte: USD -> EUR, locale to fr-MC. == Batch 3: High Security Fixes == 3.1 — Sanitize HTML in email template rendering - src/lib/server/email.ts: added escapeHtml() utility that escapes &, <, >, ", '. Applied to all template variable values in sendTemplatedEmail() before substitution. URL-type keys (logo_url, site_url) exempted. Prevents XSS in emails. 3.2 — Add file upload MIME type validation - src/lib/server/storage.ts: added MAGIC_BYTES constant and validateFileMagicBytes() function checking PNG (89504E47), JPEG (FFD8FF), PDF (25504446), WebP (52494646), GIF (47494638) magic bytes against declared MIME. Applied in uploadAvatar and uploadDocument before storing. 3.3 — Docker container hardening - docker-compose.yml portal service: added security_opt [no-new-privileges:true], read_only: true with tmpfs for /tmp, deploy.resources.limits (memory: 512M, cpus: 1.0). Dockerfile already had USER sveltekit (non-root). 3.4 — Restrict board endpoints data exposure - src/routes/(app)/board/members/+page.server.ts: replaced .select('*') with explicit column list returning only fields the board UI actually displays. Removed sensitive columns. == Batch 4: Shared Utilities == 4.1 — Extract getVisibleLevels to shared utility - src/lib/server/visibility.ts (NEW): exports getVisibleLevels(role) returning appropriate visibility levels per role. - Replaced 4 duplicate definitions in: src/routes/(app)/dashboard/+page.server.ts src/routes/(app)/documents/+page.server.ts src/routes/(app)/events/+page.server.ts src/routes/(app)/events/[id]/+page.server.ts 4.3 — Fix N+1 query in getReminderEffectiveness - src/lib/server/dues.ts: rewrote loop executing individual DB queries per reminder into single batch query with IN filter. Maps results in JS instead of N+1 round-trips. == Batch 5: Shared UI Components == 5.1 — Create reusable EmptyState component - src/lib/components/ui/empty-state.svelte (NEW): accepts icon, title, description props and optional children snippet. Consistent muted-text centered layout matching design system. - Applied in DocumentPreviewModal and NotificationCenter. 5.2 — Move LoadingSpinner to shared ui/ - src/lib/components/ui/LoadingSpinner.svelte (NEW): copied from auth/ to ui/ for general use. Original kept for compatibility. - src/lib/components/ui/index.ts: added barrel exports for EmptyState and LoadingSpinner. == Batch 6: UX Standardization == 6.4 — Add skip-to-content link - src/routes/(app)/+layout.svelte: added visually-hidden-until- focused skip link as first focusable element: <a href="#main-content" class="sr-only focus:not-sr-only ..."> Added id="main-content" to <main> element. 6.5 — Add navigation loading indicator - src/routes/(app)/+layout.svelte: imported SvelteKit $navigating store. Shows thin animated progress bar at page top during transitions. CSS-only animation, no external dependencies. == Batch 7: Code Consolidation == 7.1 — Consolidate profile/settings pages - src/lib/server/member-profile.ts (NEW, 283 lines): shared helpers handleAvatarUpload(), handleAvatarRemoval(), handleProfileUpdate(). Supports admin mode (supabaseAdmin) and user mode (scoped client). - src/routes/(app)/profile/+page.server.ts: simplified from ~167 to ~88 lines using shared helpers. - src/routes/(app)/settings/+page.server.ts: simplified from ~219 to ~106 lines using shared helpers. 7.2 — Consolidate registration flows - src/lib/server/registration.ts (NEW, 201 lines): shared helpers createMemberRecord(), cleanupAuthUser(), sendWelcomeEmail(). - src/routes/(auth)/signup/+page.server.ts: simplified from ~167 to ~85 lines using shared helpers. - src/routes/join/+page.server.ts: simplified from ~209 to ~117 lines using shared helpers. 7.3 — Create status badge utility - src/lib/utils/status-badges.ts (NEW, 55 lines): centralized STATUS_MAP for all status types (membership, dues, payment, RSVP, event, roles). Exports getStatusConfig(), getStatusBadgeClasses(), getStatusLabel(). 7.4 — Create rate limiting utility - src/lib/server/rate-limit.ts (NEW, 73 lines): in-memory Map-based rate limiter with TTL cleanup. Exports checkRateLimit(key, maxAttempts, windowMs) and resetRateLimit(). - Applied to login: 5 attempts per 15 min by email. - Applied to forgot-password: 3 attempts per 15 min by email. - src/routes/(auth)/login/+page.server.ts: added rate limit check before signInWithPassword, reset on success. - src/routes/(auth)/forgot-password/+page.server.ts: added rate limit check before resetPasswordForEmail. == New Files (9) == src/lib/server/auth-utils.ts src/lib/server/visibility.ts src/lib/server/member-profile.ts src/lib/server/registration.ts src/lib/server/rate-limit.ts src/lib/server/email.ts (escapeHtml addition) src/lib/server/storage.ts (validateFileMagicBytes addition) src/lib/utils/status-badges.ts src/lib/components/ui/empty-state.svelte src/lib/components/ui/LoadingSpinner.svelte supabase/migrations/017_fix_rls_role_escalation.sql Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 07:54:10 +01:00
security_opt:
- "no-new-privileges:true"
read_only: true
tmpfs:
- "/tmp"
deploy:
resources:
limits:
memory: 512M
cpus: '1.0'
# ============================================
# Database Migrations (one-shot)
# Runs post-deploy.sql after storage-api creates its tables.
# Idempotent - safe to run on every `docker compose up`.
# ============================================
migrate:
image: code.letsbe.solutions/letsbe/monacousa-migrate:latest
container_name: monacousa-migrate
depends_on:
db:
condition: service_healthy
storage:
condition: service_started
environment:
PGHOST: db
PGUSER: ${POSTGRES_USER:-postgres}
PGPASSWORD: ${POSTGRES_PASSWORD:-postgres}
PGDATABASE: ${POSTGRES_DB:-postgres}
networks:
- monacousa-network
restart: "no"
# ============================================
# Networks
# ============================================
networks:
monacousa-network:
driver: bridge
# ============================================
# Volumes
# ============================================
volumes:
db-data:
driver: local
storage-data:
driver: local