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
|
|
|
/**
|
|
|
|
|
* Shared registration helpers used by both the signup and join pages.
|
|
|
|
|
* Consolidates member creation and welcome email logic.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { supabaseAdmin } from '$lib/server/supabase';
|
|
|
|
|
import { sendTemplatedEmail } from '$lib/server/email';
|
|
|
|
|
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
|
|
|
|
|
|
|
|
// ────────────────────────────────────────────────────────────────
|
|
|
|
|
// Types
|
|
|
|
|
// ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export interface RegistrationData {
|
|
|
|
|
userId: string;
|
|
|
|
|
email: string;
|
|
|
|
|
firstName: string;
|
|
|
|
|
lastName: string;
|
|
|
|
|
phone?: string;
|
|
|
|
|
dateOfBirth?: string;
|
|
|
|
|
address?: string;
|
|
|
|
|
nationality?: string[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface CreateMemberResult {
|
|
|
|
|
success: boolean;
|
|
|
|
|
error?: string;
|
|
|
|
|
memberId?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ────────────────────────────────────────────────────────────────
|
|
|
|
|
// Member Creation
|
|
|
|
|
// ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create a member record in the database after auth user creation.
|
|
|
|
|
*
|
|
|
|
|
* Handles:
|
|
|
|
|
* - Looking up the default/pending membership status and type
|
2026-02-10 18:03:46 +01:00
|
|
|
* - Member ID auto-generated by database trigger (atomic sequence)
|
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
|
|
|
* - Inserting the member record
|
|
|
|
|
*
|
|
|
|
|
* @param data Core registration data.
|
|
|
|
|
* @param supabase The Supabase client to use for DB operations.
|
|
|
|
|
* @param options Additional options for how the member is created.
|
|
|
|
|
*/
|
|
|
|
|
export async function createMemberRecord(
|
|
|
|
|
data: RegistrationData,
|
|
|
|
|
supabase: SupabaseClient,
|
|
|
|
|
options?: {
|
|
|
|
|
/** Look up status by name instead of is_default. Defaults to undefined (uses is_default). */
|
|
|
|
|
statusName?: string;
|
2026-02-10 18:03:46 +01:00
|
|
|
/** @deprecated Member ID now auto-generated by database trigger. */
|
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
|
|
|
generateMemberId?: boolean;
|
|
|
|
|
}
|
|
|
|
|
): Promise<CreateMemberResult> {
|
|
|
|
|
const statusName = options?.statusName;
|
|
|
|
|
|
|
|
|
|
// Look up the membership status
|
|
|
|
|
let statusQuery;
|
|
|
|
|
if (statusName) {
|
|
|
|
|
statusQuery = supabase
|
|
|
|
|
.from('membership_statuses')
|
|
|
|
|
.select('id')
|
|
|
|
|
.eq('name', statusName)
|
|
|
|
|
.single();
|
|
|
|
|
} else {
|
|
|
|
|
statusQuery = supabase
|
|
|
|
|
.from('membership_statuses')
|
|
|
|
|
.select('id')
|
|
|
|
|
.eq('is_default', true)
|
|
|
|
|
.single();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { data: statusData, error: statusError } = await statusQuery;
|
|
|
|
|
|
|
|
|
|
if (statusError || !statusData?.id) {
|
|
|
|
|
console.error('No membership status found:', statusError);
|
|
|
|
|
return { success: false, error: 'System configuration error. Please contact support.' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Look up the default membership type
|
|
|
|
|
const { data: typeData, error: typeError } = await supabase
|
|
|
|
|
.from('membership_types')
|
|
|
|
|
.select('id')
|
|
|
|
|
.eq('is_default', true)
|
|
|
|
|
.single();
|
|
|
|
|
|
|
|
|
|
if (typeError || !typeData?.id) {
|
|
|
|
|
console.error('No default membership type found:', typeError);
|
|
|
|
|
return { success: false, error: 'System configuration error. Please contact support.' };
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 18:03:46 +01:00
|
|
|
// Member ID will be auto-generated by database trigger (generate_member_id)
|
|
|
|
|
// See migration 018_atomic_member_id_generation.sql
|
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
|
|
|
|
|
|
|
|
// Create the member profile
|
|
|
|
|
const insertPayload: Record<string, unknown> = {
|
|
|
|
|
id: data.userId,
|
|
|
|
|
first_name: data.firstName,
|
|
|
|
|
last_name: data.lastName,
|
|
|
|
|
email: data.email,
|
|
|
|
|
phone: data.phone || null,
|
|
|
|
|
date_of_birth: data.dateOfBirth || null,
|
|
|
|
|
address: data.address || null,
|
|
|
|
|
nationality: data.nationality || [],
|
|
|
|
|
role: 'member',
|
|
|
|
|
membership_status_id: statusData.id,
|
|
|
|
|
membership_type_id: typeData.id
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-10 18:03:46 +01:00
|
|
|
// member_id is auto-generated by trigger, no need to set it
|
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
|
|
|
|
2026-02-10 18:03:46 +01:00
|
|
|
const { error: memberError, data: insertedMember } = await supabase
|
|
|
|
|
.from('members')
|
|
|
|
|
.insert(insertPayload)
|
|
|
|
|
.select('member_id')
|
|
|
|
|
.single();
|
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
|
|
|
|
|
|
|
|
if (memberError) {
|
|
|
|
|
console.error('Failed to create member profile:', memberError);
|
|
|
|
|
return { success: false, error: 'Failed to create member profile. Please try again or contact support.' };
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 18:03:46 +01:00
|
|
|
return { success: true, memberId: insertedMember?.member_id };
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Clean up auth user on registration failure.
|
|
|
|
|
* Uses supabaseAdmin to ensure we can always delete the user.
|
|
|
|
|
*/
|
|
|
|
|
export async function cleanupAuthUser(userId: string): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
await supabaseAdmin.auth.admin.deleteUser(userId);
|
|
|
|
|
} catch (deleteError) {
|
|
|
|
|
console.error('Failed to clean up auth user:', deleteError);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ────────────────────────────────────────────────────────────────
|
|
|
|
|
// Welcome Email
|
|
|
|
|
// ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Send the onboarding welcome email with payment instructions.
|
|
|
|
|
*
|
|
|
|
|
* @param member Basic member info for the email template.
|
|
|
|
|
* @param paymentSettings Payment account details from app_settings.
|
|
|
|
|
* @param duesAmount The annual dues amount.
|
|
|
|
|
* @param paymentDeadline The payment deadline date.
|
|
|
|
|
*/
|
|
|
|
|
export async function sendWelcomeEmail(
|
|
|
|
|
member: {
|
|
|
|
|
id: string;
|
|
|
|
|
first_name: string;
|
|
|
|
|
email: string;
|
|
|
|
|
member_id?: string;
|
|
|
|
|
},
|
|
|
|
|
paymentSettings: Record<string, string>,
|
|
|
|
|
duesAmount: number,
|
|
|
|
|
paymentDeadline: Date
|
|
|
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
|
|
|
try {
|
|
|
|
|
const result = await sendTemplatedEmail(
|
|
|
|
|
'onboarding_welcome',
|
|
|
|
|
member.email,
|
|
|
|
|
{
|
|
|
|
|
first_name: member.first_name,
|
|
|
|
|
member_id: member.member_id || 'N/A',
|
|
|
|
|
amount: `\u20AC${duesAmount}`,
|
|
|
|
|
payment_deadline: paymentDeadline.toLocaleDateString('en-US', {
|
|
|
|
|
weekday: 'long',
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
month: 'long',
|
|
|
|
|
day: 'numeric'
|
|
|
|
|
}),
|
|
|
|
|
account_holder: paymentSettings.account_holder || 'Monaco USA',
|
|
|
|
|
bank_name: paymentSettings.bank_name || 'Credit Foncier de Monaco',
|
|
|
|
|
iban: paymentSettings.iban || 'Contact for details'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
recipientId: member.id,
|
|
|
|
|
recipientName: member.first_name,
|
|
|
|
|
sentBy: 'system'
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
} catch (emailError) {
|
|
|
|
|
console.error('Failed to send welcome email:', emailError);
|
|
|
|
|
return { success: false, error: 'Failed to send welcome email' };
|
|
|
|
|
}
|
|
|
|
|
}
|