diff --git a/docker-compose.nginx.yml b/docker-compose.nginx.yml index fb84046..f8c7233 100644 --- a/docker-compose.nginx.yml +++ b/docker-compose.nginx.yml @@ -207,7 +207,7 @@ services: PORT: 4000 DB_HOST: db DB_PORT: 5432 - DB_USER: supabase_admin + DB_USER: ${POSTGRES_USER} DB_PASSWORD: ${POSTGRES_PASSWORD} DB_NAME: ${POSTGRES_DB} DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime' @@ -257,7 +257,7 @@ services: db: condition: service_healthy rest: - condition: service_healthy + condition: service_started healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:5000/status"] interval: 10s @@ -310,7 +310,7 @@ services: PG_META_DB_HOST: db PG_META_DB_PORT: 5432 PG_META_DB_NAME: ${POSTGRES_DB} - PG_META_DB_USER: supabase_admin + PG_META_DB_USER: ${POSTGRES_USER} PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD} depends_on: db: diff --git a/docker-compose.yml b/docker-compose.yml index cd12f0f..70adce5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,7 +19,8 @@ services: JWT_EXP: ${JWT_EXPIRY:-3600} volumes: - db-data:/var/lib/postgresql/data - - ./supabase/migrations:/docker-entrypoint-initdb.d + # Migrations mounted separately - DO NOT use /docker-entrypoint-initdb.d (overwrites Supabase init) + - ./supabase/migrations:/migrations:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s @@ -156,7 +157,7 @@ services: networks: - monacousa-network - # ============================================ + # ============================================ # Realtime # ============================================ realtime: @@ -167,7 +168,7 @@ services: PORT: 4000 DB_HOST: db DB_PORT: 5432 - DB_USER: supabase_admin + DB_USER: ${POSTGRES_USER} DB_PASSWORD: ${POSTGRES_PASSWORD:-postgres} DB_NAME: ${POSTGRES_DB:-postgres} DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime' @@ -212,7 +213,7 @@ services: db: condition: service_healthy rest: - condition: service_healthy + condition: service_started healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:5000/status"] interval: 10s @@ -255,7 +256,7 @@ services: PG_META_DB_HOST: db PG_META_DB_PORT: 5432 PG_META_DB_NAME: ${POSTGRES_DB:-postgres} - PG_META_DB_USER: supabase_admin + PG_META_DB_USER: ${POSTGRES_USER} PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD:-postgres} depends_on: db: @@ -273,13 +274,7 @@ services: # Monaco USA Portal (SvelteKit App) # ============================================ portal: - build: - context: . - dockerfile: Dockerfile - args: - PUBLIC_SUPABASE_URL: ${PUBLIC_SUPABASE_URL:-http://localhost:7455} - PUBLIC_SUPABASE_ANON_KEY: ${ANON_KEY} - SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY} + image: code.letsbe.solutions/letsbe/monacousa-portal:latest container_name: monacousa-portal restart: unless-stopped ports: @@ -300,7 +295,6 @@ services: condition: service_healthy networks: - monacousa-network - # ============================================ # Networks # ============================================ @@ -315,4 +309,4 @@ volumes: db-data: driver: local storage-data: - driver: local + driver: local \ No newline at end of file diff --git a/src/app.html b/src/app.html index f273cc5..1f4cb8d 100644 --- a/src/app.html +++ b/src/app.html @@ -3,6 +3,10 @@
+ + + + %sveltekit.head% diff --git a/src/hooks.server.ts b/src/hooks.server.ts index eb422c7..15a341d 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -5,6 +5,11 @@ import { sequence } from '@sveltejs/kit/hooks'; import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'; import { env } from '$env/dynamic/private'; import type { Database } from '$lib/types/database'; +import { supabaseAdmin } from '$lib/server/supabase'; + +// Cache for setup check to avoid hitting DB on every request +let setupCheckCache: { needsSetup: boolean; checkedAt: number } | null = null; +const SETUP_CACHE_TTL = 60000; // 1 minute cache // Use internal URL for server-side operations (Docker network), fallback to public URL const SERVER_SUPABASE_URL = env.SUPABASE_INTERNAL_URL || PUBLIC_SUPABASE_URL; @@ -69,6 +74,56 @@ const supabaseHandle: Handle = async ({ event, resolve }) => { }); }; +/** + * Setup check hook + * Redirects to /setup if no users exist in the system + */ +const setupCheckHandle: Handle = async ({ event, resolve }) => { + const path = event.url.pathname; + + // Always allow access to setup page and static assets + if (path === '/setup' || path.startsWith('/api/') || path.startsWith('/auth/')) { + return resolve(event); + } + + // Check cache first + const now = Date.now(); + if (setupCheckCache && (now - setupCheckCache.checkedAt) < SETUP_CACHE_TTL) { + if (setupCheckCache.needsSetup) { + throw redirect(303, '/setup'); + } + return resolve(event); + } + + // Check if any users exist + try { + const { count, error } = await supabaseAdmin + .from('members') + .select('*', { count: 'exact', head: true }); + + if (error) { + console.error('Error checking for existing users:', error); + // On error, continue without redirect (fail open) + return resolve(event); + } + + const needsSetup = !count || count === 0; + setupCheckCache = { needsSetup, checkedAt: now }; + + if (needsSetup) { + throw redirect(303, '/setup'); + } + } catch (err) { + // If it's a redirect, rethrow it + if (err && typeof err === 'object' && 'status' in err) { + throw err; + } + console.error('Error in setup check:', err); + } + + return resolve(event); +}; + /** * Authorization hook * Protects routes based on authentication and role requirements @@ -77,6 +132,11 @@ const authorizationHandle: Handle = async ({ event, resolve }) => { const { session, member } = await event.locals.safeGetSession(); const path = event.url.pathname; + // Setup page is handled by setupCheckHandle + if (path === '/setup') { + return resolve(event); + } + // API routes handle their own authentication if (path.startsWith('/api/')) { return resolve(event); @@ -131,4 +191,4 @@ const authorizationHandle: Handle = async ({ event, resolve }) => { return resolve(event); }; -export const handle: Handle = sequence(supabaseHandle, authorizationHandle); +export const handle: Handle = sequence(supabaseHandle, setupCheckHandle, authorizationHandle); diff --git a/src/routes/(auth)/login/+page.server.ts b/src/routes/(auth)/login/+page.server.ts index b2cd18c..2609238 100644 --- a/src/routes/(auth)/login/+page.server.ts +++ b/src/routes/(auth)/login/+page.server.ts @@ -21,9 +21,17 @@ export const load: PageServerLoad = async ({ url, locals }) => { errorMessage = decodeURIComponent(errorCode); } + // Handle setup completion message + const setupComplete = url.searchParams.get('setup') === 'complete'; + let successMessage: string | null = null; + if (setupComplete) { + successMessage = 'Admin account created successfully! Please sign in to continue.'; + } + return { redirectTo: url.searchParams.get('redirectTo') || '/dashboard', - urlError: errorMessage + urlError: errorMessage, + urlSuccess: successMessage }; }; diff --git a/src/routes/(auth)/login/+page.svelte b/src/routes/(auth)/login/+page.svelte index 0929262..8472818 100644 --- a/src/routes/(auth)/login/+page.svelte +++ b/src/routes/(auth)/login/+page.svelte @@ -24,6 +24,10 @@
+
+
+ Americans in Monaco
+Create your administrator account to get started
+Initial Setup Required
+This is the first time the portal is being accessed. Create an admin account to manage the portal.
++ © 2026 Monaco USA. All rights reserved. +
+