Commit Graph

21 Commits

Author SHA1 Message Date
Matt 19eb2be85f Phase 1: Full implementation — security, bugs, utilities, UI/UX, consolidation
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>
2026-02-06 07:55:19 +01:00
Matt 4e3cf89f62 Add notifications pages and fix RLS/email issues
Build and Push Docker Image / build (push) Successful in 2m7s Details
- Fix RLS policies: Add WITH CHECK clause to all FOR ALL policies
  (fixes 502 errors on admin settings and other updates)
- Add /notifications page for users to view all notifications
- Add /admin/notifications page for admins to create/manage notifications
- Add notifications link to admin sidebar
- Fix NotificationCenter to use goto() for internal navigation
- Fix email.ts to fall back to environment variables for SMTP
  (allows welcome emails to work when app_settings SMTP not configured)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 17:19:06 +01:00
Matt 0053fa2b5e Fix migrate.sh auth failure by overriding built-in script
Build and Push Docker Image / build (push) Successful in 2m9s Details
The Supabase postgres image includes a migrate.sh that tries to connect
as supabase_admin without proper credentials. Override it with an empty
script since migrations are handled by init.sql.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 17:02:07 +01:00
Matt f81da356cc Fix init.sql table grant ordering
Build and Push Docker Image / build (push) Successful in 2m49s Details
Move GRANT statements for document_folders and user_notification_preferences
to after their respective CREATE TABLE statements. The grants were failing
because they referenced tables that hadn't been created yet.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 16:52:07 +01:00
Matt 274b13fe1e Add notifications system, fix button href, admin settings and welcome email
Build and Push Docker Image / build (push) Successful in 1m55s Details
- Fix admin settings 502 error by adding INSERT/UPDATE/DELETE grants
- Fix Button component to render <a> when href prop is provided
- Add welcome email for admin created during initial setup
- Add in-app notifications system with NotificationCenter component
- Add notifications table with RLS policies and welcome trigger
- Add API endpoints for fetching and marking notifications as read

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 16:04:27 +01:00
Matt 2451582dc6 Complete database grants and update README
Build and Push Docker Image / build (push) Successful in 1m52s Details
init.sql changes:
- Add INSERT grant for members table (for /join signup)
- Add INSERT grant for dues_payments (for board recording payments)
- Add full CRUD grants for events, documents, document_folders
- Add UPDATE grant for email_templates (admin management)
- Add anon role grants for public event viewing and RSVP creation

README changes:
- Add "Important Notes" section explaining dynamic env vars
- Add first-time setup and database initialization docs
- Add troubleshooting for 403 errors with grant fix commands
- Add troubleshooting for "account not configured" errors

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 15:44:25 +01:00
Matt dc0198dcad Add comprehensive table grants for authenticated role
Build and Push Docker Image / build (push) Successful in 1m54s Details
RLS policies define WHAT rows can be accessed, but GRANT statements
control WHETHER a table can be accessed at all. This was causing 403
errors when authenticated users tried to access tables.

Added grants for:
- Core tables: members, membership_statuses, membership_types
- Dues: dues_payments (SELECT)
- Events: events, event_types, event_rsvps (full CRUD), event_rsvps_public
- Documents: documents, document_categories, document_folders
- Settings: app_settings (SELECT for public settings)
- Email: email_logs (SELECT for own logs)
- Preferences: user_notification_preferences (SELECT, INSERT, UPDATE)
- Views: members_with_dues, events_with_counts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 15:40:47 +01:00
Matt c74525e113 Make phone, date_of_birth, address nullable in members table
Build and Push Docker Image / build (push) Successful in 1m53s Details
These fields can be filled in later by the user. The admin setup page
only collects essential fields (name, email, password).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 14:06:10 +01:00
Matt cfbf7639c2 Add password setup script for Supabase roles
Build and Push Docker Image / build (push) Successful in 1m51s Details
The Supabase postgres image's internal migrate.sh requires supabase_admin
to have a password matching POSTGRES_PASSWORD. Added zz-set-passwords.sh
to run after init.sql and set passwords dynamically using the environment
variable.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 13:57:35 +01:00
Matt 0e93961bb9 Remove hardcoded supabase_admin password from init.sql
Build and Push Docker Image / build (push) Successful in 1m48s Details
The Supabase postgres image sets these passwords based on POSTGRES_PASSWORD.
Hardcoding 'postgres' caused the image's migrate.sh to fail.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 12:20:46 +01:00
Matt c8efc3859c Wrap storage operations in conditionals for fresh db init
Build and Push Docker Image / build (push) Successful in 1m46s Details
- storage.objects and storage.buckets are created by storage-api service
- Wrapped all storage bucket inserts and policy operations in DO blocks
- Check if table exists before running storage operations
- Prevents errors during initial database setup

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 12:14:32 +01:00
Matt ce3239598d Add auth helper functions (uid, role, jwt) to init.sql
Build and Push Docker Image / build (push) Successful in 1m45s Details
These functions are normally created by GoTrue but our init.sql
runs first. Needed for RLS policies that use auth.uid().

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 12:12:33 +01:00
Matt 679f278075 Grant service_role full access to all public tables
Build and Push Docker Image / build (push) Successful in 1m46s Details
Added GRANT ALL for service_role on:
- membership_statuses, membership_types, members tables
- All tables and sequences in public schema
- Default privileges for future tables

Fixes 'permission denied' errors during admin setup.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 12:10:21 +01:00
Matt 4039ec8187 Remove FK references to auth.users from init.sql
Build and Push Docker Image / build (push) Successful in 1m55s Details
The auth.users table is created by GoTrue, not the database init.
FK constraints to auth.users fail because init.sql runs before auth starts.
Removed FK from members and audit_logs tables.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 11:58:31 +01:00
Matt f599a37964 Remove shell-based healthchecks for minimal images
Build and Push Docker Image / build (push) Successful in 1m47s Details
- PostgREST and postgres-meta images don't have /bin/sh
- Removed CMD-SHELL healthchecks that were causing unhealthy status
- Changed dependent services from service_healthy to service_started

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 11:47:24 +01:00
Matt dec5e1950e Fix domain configuration for separate portal and API domains
Build and Push Docker Image / build (push) Successful in 1m59s Details
- Changed from single DOMAIN variable to PORTAL_DOMAIN and API_DOMAIN
- Matches nginx config: portal.monacousa.org, api.monacousa.org, studio.monacousa.org
- Updated docker-compose.yml to use correct domain variables with defaults
- Updated setup.sh to validate both domain variables
- Updated .env.example with separate domain configuration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 11:34:25 +01:00
Matt c92a7ee9b8 Update port mappings to match nginx config
Build and Push Docker Image / build (push) Successful in 1m55s Details
- Portal: 7453
- Studio: 7454
- Kong API: 7455

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 11:31:55 +01:00
Matt 35f9beabc6 Fix setup.sh: use awk instead of sed for robustness
Build and Push Docker Image / build (push) Successful in 1m53s Details
- Use openssl rand -hex for secrets (no special chars)
- Use awk instead of sed for .env updates (handles any chars)
- Use awk for kong.yml generation (handles JWT tokens)
- Suppress source errors for malformed .env

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 11:25:47 +01:00
Matt 4f4d0dd42e Fix .env.example: quote SMTP_SENDER_NAME value
Build and Push Docker Image / build (push) Has been cancelled Details
Values with spaces must be quoted for bash source command

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 11:24:20 +01:00
Matt 8cabe7d362 Remove Traefik from deploy package (use existing nginx)
Build and Push Docker Image / build (push) Successful in 1m52s Details
- Remove Traefik service and related labels
- Expose ports to localhost only (3000, 8000, 3001)
- Update README with nginx proxy configuration examples
- Remove ACME_EMAIL and Traefik auth from .env.example

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 11:18:55 +01:00
Matt 3187f5babb Add standalone production deployment package
Build and Push Docker Image / build (push) Successful in 1m46s Details
- docker-compose.yml: Standalone compose with Traefik, Supabase, portal
- init.sql: Combined database schema + all 16 migrations
- kong.yml.template: Kong config with API key placeholders
- setup.sh: Auto-generates secrets (JWT, passwords, API keys)
- .env.example: Comprehensive environment template
- README.md: Complete deployment guide

No source code cloning required - just copy files and run setup.sh

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 11:15:56 +01:00