|
Build and Push Docker Image / build (push) Successful in 2m8s
Details
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>
|
||
|---|---|---|
| .. | ||
| .env.example | ||
| README.md | ||
| docker-compose.yml | ||
| init.sql | ||
| kong.yml.template | ||
| migrate.sh | ||
| setup.sh | ||
| zz-set-passwords.sh | ||
README.md
Monaco USA Portal - Standalone Production Deployment
This is a standalone deployment package for the Monaco USA Portal. No source code cloning required.
Prerequisites
- Linux server (Ubuntu 22.04+ recommended)
- Docker Engine 24.0+
- Docker Compose v2.20+
- Nginx installed and configured for SSL
- Domain name with DNS pointing to your server
Quick Start
1. Download the deployment files
Create a directory and download the deployment files:
mkdir -p /opt/monacousa
cd /opt/monacousa
# Download files from your deployment source
# Example: copy from Gitea, scp, etc.
You need these files:
docker-compose.yml.env.exampleinit.sqlkong.yml.templatesetup.sh
2. Configure environment
# Copy the example environment file
cp .env.example .env
# Edit with your settings
nano .env
At minimum, configure:
DOMAIN- Your domain name (e.g.,portal.monacousa.org)- SMTP settings (optional but recommended for emails)
3. Run setup script
# Make setup script executable
chmod +x setup.sh
# Run setup - this generates secrets and kong.yml
./setup.sh
The setup script will:
- Generate secure random passwords and JWT tokens
- Create
kong.ymlfrom the template with your API keys - Validate your configuration
4. Start the services
docker compose up -d
5. Configure Nginx
Add proxy configuration for your domain. Example:
# Portal - main site
server {
listen 443 ssl http2;
server_name portal.monacousa.org;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
# API - Supabase services
server {
listen 443 ssl http2;
server_name api.portal.monacousa.org;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
# Studio - Supabase dashboard (optional)
server {
listen 443 ssl http2;
server_name studio.portal.monacousa.org;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# Add basic auth for security
auth_basic "Supabase Studio";
auth_basic_user_file /etc/nginx/.htpasswd;
location / {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
Reload nginx:
nginx -t && systemctl reload nginx
6. Verify deployment
# Check all containers are running
docker compose ps
# Check database initialization
docker compose logs db
# Check for any errors
docker compose logs -f
7. Access the portal
Open https://portal.monacousa.org in your browser. On first visit, you'll be redirected to /setup to create the initial admin account.
Architecture
Internet
│
└─► Nginx (SSL/Reverse Proxy)
│
├─► portal.domain.com:443 ──► localhost:3000 ──► Portal (SvelteKit)
├─► api.domain.com:443 ──► localhost:8000 ──► Kong ──► Auth/REST/Storage
└─► studio.domain.com:443 ──► localhost:3001 ──► Studio (Dashboard)
Internal Docker Network
│
├─► Kong API Gateway (port 8000)
│ ├─► Auth (GoTrue)
│ ├─► REST (PostgREST)
│ ├─► Storage API
│ └─► Realtime
│
└─► PostgreSQL Database
Exposed Ports
| Service | Port | Purpose |
|---|---|---|
| Portal | 127.0.0.1:3000 | Main web application |
| Kong | 127.0.0.1:8000 | Supabase API gateway |
| Studio | 127.0.0.1:3001 | Supabase dashboard (optional) |
All ports are bound to localhost only for security. Use nginx to expose them.
Files Description
| File | Purpose |
|---|---|
docker-compose.yml |
All service definitions |
.env |
Your configuration (from .env.example) |
init.sql |
Database schema and migrations |
kong.yml.template |
API gateway config template |
kong.yml |
Generated API gateway config (created by setup.sh) |
setup.sh |
Setup script for secrets and validation |
Management Commands
# Start all services
docker compose up -d
# Stop all services
docker compose down
# View logs
docker compose logs -f
# View specific service logs
docker compose logs -f portal
docker compose logs -f db
# Restart a specific service
docker compose restart portal
# Check resource usage
docker stats
# Enter database shell
docker compose exec db psql -U postgres
Updating
To update the portal to a new version:
# Pull the latest image
docker compose pull portal
# Restart the portal service
docker compose up -d portal
Backup
Database backup
# Create backup
docker compose exec db pg_dump -U postgres postgres > backup_$(date +%Y%m%d).sql
# Restore backup
docker compose exec -T db psql -U postgres postgres < backup_YYYYMMDD.sql
Full backup (including storage)
# Stop services first for consistent backup
docker compose stop
# Backup volumes
docker run --rm -v monacousa_db-data:/data -v $(pwd):/backup alpine \
tar czf /backup/db-data-backup.tar.gz -C /data .
docker run --rm -v monacousa_storage-data:/data -v $(pwd):/backup alpine \
tar czf /backup/storage-data-backup.tar.gz -C /data .
# Start services
docker compose up -d
Important Notes
Environment Variables
The portal uses dynamic environment variables which are read at runtime. This means:
- You can change
.envvalues and restart containers without rebuilding - The pre-built Docker image works with any configuration
- JWT tokens and API keys must be generated using
./setup.sh
First-Time Setup
On first access to the portal, you'll be redirected to /setup where you create the initial admin account. This only happens when the members table is empty.
Database Initialization
The init.sql file:
- Creates all database schemas, tables, views, and functions
- Sets up Row Level Security (RLS) policies
- Grants appropriate permissions to database roles
- Runs automatically on first container start
If you need to reset the database:
docker compose down -v # WARNING: Deletes all data!
docker compose up -d
Rebuilding the Portal (If Needed)
If you're building from source instead of using the pre-built image, ensure environment variables are passed during build for static features. For most deployments, the pre-built image with runtime env vars is recommended.
Troubleshooting
Containers not starting
# Check logs for errors
docker compose logs
# Check if ports are in use
netstat -tlnp | grep -E ':(3000|3001|8000)'
Database connection errors
# Check database is healthy
docker compose ps db
# Check database logs
docker compose logs db
# Verify database is accepting connections
docker compose exec db pg_isready -U postgres
API 401 Unauthorized errors
This usually means the API keys don't match. Run setup again:
./setup.sh
docker compose restart kong
API 403 Forbidden errors
This means the database permissions (GRANTs) are missing. This is fixed in the init.sql, but if you see this on an existing deployment, run:
docker compose exec db psql -U postgres -c "
GRANT SELECT, INSERT, UPDATE ON public.members TO authenticated;
GRANT SELECT ON public.membership_statuses TO authenticated;
GRANT SELECT ON public.membership_types TO authenticated;
GRANT SELECT ON public.members_with_dues TO authenticated;
GRANT SELECT, INSERT ON public.dues_payments TO authenticated;
GRANT SELECT, INSERT, UPDATE, DELETE ON public.events TO authenticated;
GRANT SELECT, INSERT, UPDATE, DELETE ON public.event_rsvps TO authenticated;
GRANT SELECT ON public.events_with_counts TO authenticated;
"
"Your account is not properly configured" error
This occurs when a user can authenticate but can't query their member profile. Usually a database permission issue - see the 403 fix above.
Portal not loading
# Check portal logs
docker compose logs portal
# Verify kong is routing correctly
docker compose exec portal wget -qO- http://kong:8000/rest/v1/ || echo "Kong not reachable"
Nginx 502 Bad Gateway
Check if the Docker containers are running:
docker compose ps
curl http://127.0.0.1:3000 # Should return HTML
curl http://127.0.0.1:8000 # Should return JSON
Security Recommendations
-
Secure your .env file
chmod 600 .env -
Protect Supabase Studio with auth
# Generate password file for nginx htpasswd -c /etc/nginx/.htpasswd admin -
Regular updates
- Keep Docker and host OS updated
- Regularly pull latest portal images
-
Monitor logs
- Set up log rotation (configured in docker-compose.yml)
- Consider centralized logging
Support
For issues and questions:
- Check logs:
docker compose logs -f - GitHub/Gitea issues
- Email: support@monacousa.org