monacousa-portal/src/lib/server/registration.ts

194 lines
6.3 KiB
TypeScript
Raw Normal View History

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
* - 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;
/** @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.' };
}
// 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
};
// 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
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.' };
}
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' };
}
}