Commit Graph

12 Commits

Author SHA1 Message Date
Matt 5ff9f950a1 Implement complete feature & security overhaul (21 items, 3 phases)
Build and Push Docker Images / build-portal (push) Successful in 2m1s Details
Build and Push Docker Images / build-infra (docker/db, monacousa-db) (push) Successful in 1m17s Details
Build and Push Docker Images / build-infra (docker/kong, monacousa-kong) (push) Successful in 24s Details
Build and Push Docker Images / build-infra (docker/migrate, monacousa-migrate) (push) Successful in 1m0s Details
Phase 1 - Security & Data Integrity:
- Atomic member ID generation via PostgreSQL sequence (018)
- Rate limiting on signup, input sanitization (XSS prevention)
- Onboarding photo upload, document upload validation (magic bytes, MIME, size)
- RLS fix for admin role assignment without self-escalation (019)
- Email notification preferences enforcement
- Audit logging across all admin/board mutation actions
- CSV export for membership, payments, and events reports
- Member approval workflow with email notifications (020)

Phase 2 - Functionality & Monitoring:
- Directory privacy settings (022) with board-level filtering
- Document full-text search with PostgreSQL tsvector/GIN index (023)
- Cron job monitoring dashboard with manual trigger (024)
- Settings audit log tab
- Bulk email broadcast with recipient filtering and personalization (025)

Phase 3 - Feature Completeness:
- Event type filtering on events page
- RSVP deadline control for event organizers (021)

Also includes Kong CORS configuration fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 18:03:46 +01:00
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 9b119302d3 Fix admin settings 502 and improve notifications UX
Build and Push Docker Image / build (push) Successful in 1m47s Details
- Use supabaseAdmin for admin settings operations (bypasses RLS completely)
- Add proper error handling to updateSettings action
- Update notifications to be expandable with full message display
- Clicking notifications in dropdown now goes to /notifications?id=X
- Auto-scroll and expand notification when opening from dropdown link

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 17:26:17 +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 6be67e2329 Fix redirect loop after admin setup by invalidating setup cache
Build and Push Docker Image / build (push) Successful in 2m9s Details
The setupCheckHandle hook caches whether setup is needed for 1 minute.
After creating the admin, this cache wasn't cleared, causing a redirect
loop between /login and /setup.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 16:23:34 +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 fdd0bb1f7e Use dynamic env vars for Supabase config
Build and Push Docker Image / build (push) Successful in 2m30s Details
Changes $env/static/public to $env/dynamic/public for all Supabase
URL and API key configuration. This allows the app to read environment
variables at runtime instead of build time, enabling deployment with
different configurations without rebuilding the Docker image.

Files updated:
- hooks.server.ts: Use dynamic env for PUBLIC_SUPABASE_URL/KEY
- lib/server/supabase.ts: Lazy-init admin client with dynamic env
- lib/server/storage.ts: Use dynamic env for browser-accessible URLs
- lib/supabase.ts: Use dynamic env for browser client

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 15:25:41 +01:00
Matt 7a184e8a5f Fix missing display_name in membership status creation
Build and Push Docker Image / build (push) Successful in 2m18s Details
The membership_statuses table requires display_name but the setup page
was not providing it when creating the initial Active status.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 14:00:53 +01:00
Matt d4f47c5b20 Fix: Use dynamic env for SERVICE_ROLE_KEY
Build and Push Docker Image / build (push) Successful in 1m49s Details
$env/static/private reads at build time, not runtime.
Changed to $env/dynamic/private so the key is read at runtime
from the container environment.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 12:05:58 +01:00
Matt e4a40e1e40 Fix setup check to redirect when members table doesn't exist
Build and Push Docker Image / build (push) Successful in 1m49s Details
- Treat "table does not exist" errors as needing setup
- Redirect to /setup on unexpected errors (safer default)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 09:55:37 +01:00
Matt 5bbf26e7a1 Add initial admin setup page and favicon support
Build and Push Docker Image / build (push) Successful in 2m2s Details
- Add /setup route for first-run admin user creation
- Add setup check hook to redirect to /setup when no users exist
- Fix storage container dependency (service_started vs service_healthy)
- Fix migrations mount path (don't overwrite Supabase init scripts)
- Add favicon and apple touch icon links to app.html
- Show success message on login after setup completion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 09:36:25 +01:00
Matt e7338d1a70 Initial production deployment setup
- Production docker-compose with nginx support
- Nginx configuration for portal.monacousa.org
- Deployment script with backup/restore
- Gitea CI/CD workflow
- Fix CountryFlag reactivity for dropdown flags

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 02:19:49 +01:00