Add initial admin setup page and favicon support
Build and Push Docker Image / build (push) Successful in 2m2s
Details
Build and Push Docker Image / build (push) Successful in 2m2s
Details
- Add /setup route for first-run admin user creation - Add setup check hook to redirect to /setup when no users exist - Fix storage container dependency (service_started vs service_healthy) - Fix migrations mount path (don't overwrite Supabase init scripts) - Add favicon and apple touch icon links to app.html - Show success message on login after setup completion Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a450e1afd9
commit
5bbf26e7a1
|
|
@ -207,7 +207,7 @@ services:
|
||||||
PORT: 4000
|
PORT: 4000
|
||||||
DB_HOST: db
|
DB_HOST: db
|
||||||
DB_PORT: 5432
|
DB_PORT: 5432
|
||||||
DB_USER: supabase_admin
|
DB_USER: ${POSTGRES_USER}
|
||||||
DB_PASSWORD: ${POSTGRES_PASSWORD}
|
DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
DB_NAME: ${POSTGRES_DB}
|
DB_NAME: ${POSTGRES_DB}
|
||||||
DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime'
|
DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime'
|
||||||
|
|
@ -257,7 +257,7 @@ services:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
rest:
|
rest:
|
||||||
condition: service_healthy
|
condition: service_started
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:5000/status"]
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:5000/status"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
|
|
@ -310,7 +310,7 @@ services:
|
||||||
PG_META_DB_HOST: db
|
PG_META_DB_HOST: db
|
||||||
PG_META_DB_PORT: 5432
|
PG_META_DB_PORT: 5432
|
||||||
PG_META_DB_NAME: ${POSTGRES_DB}
|
PG_META_DB_NAME: ${POSTGRES_DB}
|
||||||
PG_META_DB_USER: supabase_admin
|
PG_META_DB_USER: ${POSTGRES_USER}
|
||||||
PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD}
|
PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ services:
|
||||||
JWT_EXP: ${JWT_EXPIRY:-3600}
|
JWT_EXP: ${JWT_EXPIRY:-3600}
|
||||||
volumes:
|
volumes:
|
||||||
- db-data:/var/lib/postgresql/data
|
- 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:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
|
|
@ -156,7 +157,7 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- monacousa-network
|
- monacousa-network
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Realtime
|
# Realtime
|
||||||
# ============================================
|
# ============================================
|
||||||
realtime:
|
realtime:
|
||||||
|
|
@ -167,7 +168,7 @@ services:
|
||||||
PORT: 4000
|
PORT: 4000
|
||||||
DB_HOST: db
|
DB_HOST: db
|
||||||
DB_PORT: 5432
|
DB_PORT: 5432
|
||||||
DB_USER: supabase_admin
|
DB_USER: ${POSTGRES_USER}
|
||||||
DB_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
DB_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
||||||
DB_NAME: ${POSTGRES_DB:-postgres}
|
DB_NAME: ${POSTGRES_DB:-postgres}
|
||||||
DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime'
|
DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime'
|
||||||
|
|
@ -212,7 +213,7 @@ services:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
rest:
|
rest:
|
||||||
condition: service_healthy
|
condition: service_started
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:5000/status"]
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:5000/status"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
|
|
@ -255,7 +256,7 @@ services:
|
||||||
PG_META_DB_HOST: db
|
PG_META_DB_HOST: db
|
||||||
PG_META_DB_PORT: 5432
|
PG_META_DB_PORT: 5432
|
||||||
PG_META_DB_NAME: ${POSTGRES_DB:-postgres}
|
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}
|
PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
|
|
@ -273,13 +274,7 @@ services:
|
||||||
# Monaco USA Portal (SvelteKit App)
|
# Monaco USA Portal (SvelteKit App)
|
||||||
# ============================================
|
# ============================================
|
||||||
portal:
|
portal:
|
||||||
build:
|
image: code.letsbe.solutions/letsbe/monacousa-portal:latest
|
||||||
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}
|
|
||||||
container_name: monacousa-portal
|
container_name: monacousa-portal
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
|
|
@ -300,7 +295,6 @@ services:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
networks:
|
networks:
|
||||||
- monacousa-network
|
- monacousa-network
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Networks
|
# Networks
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -315,4 +309,4 @@ volumes:
|
||||||
db-data:
|
db-data:
|
||||||
driver: local
|
driver: local
|
||||||
storage-data:
|
storage-data:
|
||||||
driver: local
|
driver: local
|
||||||
|
|
@ -3,6 +3,10 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="192x192" href="/icon-192x192.png" />
|
||||||
|
<meta name="theme-color" content="#dc2626" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,11 @@ import { sequence } from '@sveltejs/kit/hooks';
|
||||||
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
|
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
|
||||||
import { env } from '$env/dynamic/private';
|
import { env } from '$env/dynamic/private';
|
||||||
import type { Database } from '$lib/types/database';
|
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
|
// Use internal URL for server-side operations (Docker network), fallback to public URL
|
||||||
const SERVER_SUPABASE_URL = env.SUPABASE_INTERNAL_URL || PUBLIC_SUPABASE_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
|
* Authorization hook
|
||||||
* Protects routes based on authentication and role requirements
|
* 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 { session, member } = await event.locals.safeGetSession();
|
||||||
const path = event.url.pathname;
|
const path = event.url.pathname;
|
||||||
|
|
||||||
|
// Setup page is handled by setupCheckHandle
|
||||||
|
if (path === '/setup') {
|
||||||
|
return resolve(event);
|
||||||
|
}
|
||||||
|
|
||||||
// API routes handle their own authentication
|
// API routes handle their own authentication
|
||||||
if (path.startsWith('/api/')) {
|
if (path.startsWith('/api/')) {
|
||||||
return resolve(event);
|
return resolve(event);
|
||||||
|
|
@ -131,4 +191,4 @@ const authorizationHandle: Handle = async ({ event, resolve }) => {
|
||||||
return resolve(event);
|
return resolve(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handle: Handle = sequence(supabaseHandle, authorizationHandle);
|
export const handle: Handle = sequence(supabaseHandle, setupCheckHandle, authorizationHandle);
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,17 @@ export const load: PageServerLoad = async ({ url, locals }) => {
|
||||||
errorMessage = decodeURIComponent(errorCode);
|
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 {
|
return {
|
||||||
redirectTo: url.searchParams.get('redirectTo') || '/dashboard',
|
redirectTo: url.searchParams.get('redirectTo') || '/dashboard',
|
||||||
urlError: errorMessage
|
urlError: errorMessage,
|
||||||
|
urlSuccess: successMessage
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,10 @@
|
||||||
<FormMessage type="error" message={data.urlError} />
|
<FormMessage type="error" message={data.urlError} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if data.urlSuccess}
|
||||||
|
<FormMessage type="success" message={data.urlSuccess} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if form?.error}
|
{#if form?.error}
|
||||||
<FormMessage type="error" message={form.error} />
|
<FormMessage type="error" message={form.error} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,208 @@
|
||||||
|
import { fail, redirect } from '@sveltejs/kit';
|
||||||
|
import type { Actions, PageServerLoad } from './$types';
|
||||||
|
import { supabaseAdmin } from '$lib/server/supabase';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async () => {
|
||||||
|
// Check if any users exist in the system
|
||||||
|
const { count, error } = await supabaseAdmin
|
||||||
|
.from('members')
|
||||||
|
.select('*', { count: 'exact', head: true });
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error checking for existing users:', error);
|
||||||
|
// If we can't check, assume setup is needed (will fail gracefully later)
|
||||||
|
return { needsSetup: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// If users already exist, redirect to login
|
||||||
|
if (count && count > 0) {
|
||||||
|
throw redirect(303, '/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { needsSetup: true };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actions: Actions = {
|
||||||
|
default: async ({ request, url }) => {
|
||||||
|
// Double-check that no users exist (prevent race conditions)
|
||||||
|
const { count: existingCount } = await supabaseAdmin
|
||||||
|
.from('members')
|
||||||
|
.select('*', { count: 'exact', head: true });
|
||||||
|
|
||||||
|
if (existingCount && existingCount > 0) {
|
||||||
|
return fail(400, {
|
||||||
|
error: 'Setup has already been completed. Please sign in instead.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = await request.formData();
|
||||||
|
|
||||||
|
// Extract form fields
|
||||||
|
const firstName = formData.get('first_name') as string;
|
||||||
|
const lastName = formData.get('last_name') as string;
|
||||||
|
const email = formData.get('email') as string;
|
||||||
|
const password = formData.get('password') as string;
|
||||||
|
const confirmPassword = formData.get('confirm_password') as string;
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
const errors: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (!firstName || firstName.length < 2) {
|
||||||
|
errors.first_name = 'First name must be at least 2 characters';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lastName || lastName.length < 2) {
|
||||||
|
errors.last_name = 'Last name must be at least 2 characters';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email || !email.includes('@')) {
|
||||||
|
errors.email = 'Please enter a valid email address';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!password || password.length < 8) {
|
||||||
|
errors.password = 'Password must be at least 8 characters';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
errors.confirm_password = 'Passwords do not match';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return validation errors
|
||||||
|
if (Object.keys(errors).length > 0) {
|
||||||
|
return fail(400, {
|
||||||
|
error: Object.values(errors)[0],
|
||||||
|
first_name: firstName,
|
||||||
|
last_name: lastName,
|
||||||
|
email
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user with admin service role (auto-confirms email)
|
||||||
|
const { data: authData, error: authError } = await supabaseAdmin.auth.admin.createUser({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
email_confirm: true, // Auto-confirm email for initial admin
|
||||||
|
user_metadata: {
|
||||||
|
first_name: firstName,
|
||||||
|
last_name: lastName
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (authError) {
|
||||||
|
console.error('Failed to create admin user:', authError);
|
||||||
|
return fail(400, {
|
||||||
|
error: authError.message,
|
||||||
|
first_name: firstName,
|
||||||
|
last_name: lastName,
|
||||||
|
email
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authData.user) {
|
||||||
|
return fail(500, {
|
||||||
|
error: 'Failed to create admin account. Please try again.',
|
||||||
|
first_name: firstName,
|
||||||
|
last_name: lastName,
|
||||||
|
email
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get or create default membership status (active for admin)
|
||||||
|
let statusId: string;
|
||||||
|
const { data: activeStatus } = await supabaseAdmin
|
||||||
|
.from('membership_statuses')
|
||||||
|
.select('id')
|
||||||
|
.eq('name', 'Active')
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (activeStatus) {
|
||||||
|
statusId = activeStatus.id;
|
||||||
|
} else {
|
||||||
|
// Create active status if it doesn't exist
|
||||||
|
const { data: newStatus, error: statusError } = await supabaseAdmin
|
||||||
|
.from('membership_statuses')
|
||||||
|
.insert({
|
||||||
|
name: 'Active',
|
||||||
|
description: 'Active member in good standing',
|
||||||
|
is_default: false,
|
||||||
|
color: '#22c55e'
|
||||||
|
})
|
||||||
|
.select('id')
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (statusError || !newStatus) {
|
||||||
|
console.error('Failed to create membership status:', statusError);
|
||||||
|
await supabaseAdmin.auth.admin.deleteUser(authData.user.id);
|
||||||
|
return fail(500, {
|
||||||
|
error: 'Failed to set up membership status. Please try again.',
|
||||||
|
first_name: firstName,
|
||||||
|
last_name: lastName,
|
||||||
|
email
|
||||||
|
});
|
||||||
|
}
|
||||||
|
statusId = newStatus.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get or create default membership type
|
||||||
|
let typeId: string;
|
||||||
|
const { data: defaultType } = await supabaseAdmin
|
||||||
|
.from('membership_types')
|
||||||
|
.select('id')
|
||||||
|
.eq('is_default', true)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (defaultType) {
|
||||||
|
typeId = defaultType.id;
|
||||||
|
} else {
|
||||||
|
// Create default membership type if it doesn't exist
|
||||||
|
const { data: newType, error: typeError } = await supabaseAdmin
|
||||||
|
.from('membership_types')
|
||||||
|
.insert({
|
||||||
|
name: 'Standard',
|
||||||
|
description: 'Standard membership',
|
||||||
|
annual_dues: 0,
|
||||||
|
is_default: true
|
||||||
|
})
|
||||||
|
.select('id')
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (typeError || !newType) {
|
||||||
|
console.error('Failed to create membership type:', typeError);
|
||||||
|
await supabaseAdmin.auth.admin.deleteUser(authData.user.id);
|
||||||
|
return fail(500, {
|
||||||
|
error: 'Failed to set up membership type. Please try again.',
|
||||||
|
first_name: firstName,
|
||||||
|
last_name: lastName,
|
||||||
|
email
|
||||||
|
});
|
||||||
|
}
|
||||||
|
typeId = newType.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create admin member profile
|
||||||
|
const { error: memberError } = await supabaseAdmin.from('members').insert({
|
||||||
|
id: authData.user.id,
|
||||||
|
first_name: firstName,
|
||||||
|
last_name: lastName,
|
||||||
|
email,
|
||||||
|
role: 'admin', // Set as admin
|
||||||
|
membership_status_id: statusId,
|
||||||
|
membership_type_id: typeId,
|
||||||
|
nationality: ['US'] // Default nationality
|
||||||
|
});
|
||||||
|
|
||||||
|
if (memberError) {
|
||||||
|
console.error('Failed to create admin member profile:', memberError);
|
||||||
|
await supabaseAdmin.auth.admin.deleteUser(authData.user.id);
|
||||||
|
return fail(500, {
|
||||||
|
error: 'Failed to create admin profile. Please try again.',
|
||||||
|
first_name: firstName,
|
||||||
|
last_name: lastName,
|
||||||
|
email
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success - redirect to login
|
||||||
|
throw redirect(303, '/login?setup=complete');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { FormField, FormMessage, LoadingSpinner } from '$lib/components/auth';
|
||||||
|
|
||||||
|
let { form } = $props();
|
||||||
|
|
||||||
|
let firstName = $state('');
|
||||||
|
let lastName = $state('');
|
||||||
|
let email = $state('');
|
||||||
|
let password = $state('');
|
||||||
|
let confirmPassword = $state('');
|
||||||
|
let loading = $state(false);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Initial Setup | Monaco USA</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="relative flex min-h-screen items-center justify-center overflow-hidden px-4 py-12">
|
||||||
|
<!-- Background image -->
|
||||||
|
<div class="absolute inset-0 -z-20">
|
||||||
|
<img
|
||||||
|
src="/monaco_high_res.jpg"
|
||||||
|
alt=""
|
||||||
|
class="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-br from-slate-900/80 via-slate-900/60 to-monaco-900/70"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Decorative blur elements -->
|
||||||
|
<div class="absolute inset-0 -z-10">
|
||||||
|
<div class="absolute -left-40 -top-40 h-80 w-80 rounded-full bg-monaco-500/30 blur-3xl"></div>
|
||||||
|
<div class="absolute -right-40 top-1/3 h-96 w-96 rounded-full bg-monaco-400/20 blur-3xl"></div>
|
||||||
|
<div class="absolute bottom-0 left-1/3 h-72 w-72 rounded-full bg-white/10 blur-3xl"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full max-w-md">
|
||||||
|
<!-- Logo and Branding -->
|
||||||
|
<div class="mb-8 text-center">
|
||||||
|
<div class="inline-flex flex-col items-center">
|
||||||
|
<div class="mb-4 overflow-hidden rounded-2xl bg-white/90 p-2 shadow-xl backdrop-blur-sm">
|
||||||
|
<img
|
||||||
|
src="/MONACOUSA-Flags_376x376.png"
|
||||||
|
alt="Monaco USA"
|
||||||
|
class="h-20 w-20 object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-2xl font-bold text-white drop-shadow-lg">
|
||||||
|
Monaco <span class="text-monaco-300">USA</span>
|
||||||
|
</h1>
|
||||||
|
<p class="mt-1 text-sm text-white/80">Americans in Monaco</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Setup Card Container -->
|
||||||
|
<div class="rounded-2xl bg-white p-8 shadow-xl">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="text-center">
|
||||||
|
<h2 class="text-xl font-semibold text-slate-900">Welcome to Monaco USA</h2>
|
||||||
|
<p class="mt-1 text-sm text-slate-500">Create your administrator account to get started</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Setup Notice -->
|
||||||
|
<div class="rounded-lg bg-blue-50 p-4 text-sm text-blue-800">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<svg class="h-5 w-5 flex-shrink-0 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium">Initial Setup Required</p>
|
||||||
|
<p class="mt-1 text-blue-700">This is the first time the portal is being accessed. Create an admin account to manage the portal.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if form?.error}
|
||||||
|
<FormMessage type="error" message={form.error} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<form
|
||||||
|
method="POST"
|
||||||
|
use:enhance={() => {
|
||||||
|
loading = true;
|
||||||
|
return async ({ update }) => {
|
||||||
|
loading = false;
|
||||||
|
await update();
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
class="space-y-4"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
label="First name"
|
||||||
|
name="first_name"
|
||||||
|
type="text"
|
||||||
|
placeholder="John"
|
||||||
|
required
|
||||||
|
disabled={loading}
|
||||||
|
bind:value={firstName}
|
||||||
|
autocomplete="given-name"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
label="Last name"
|
||||||
|
name="last_name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Doe"
|
||||||
|
required
|
||||||
|
disabled={loading}
|
||||||
|
bind:value={lastName}
|
||||||
|
autocomplete="family-name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
label="Email address"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="admin@monacousa.org"
|
||||||
|
required
|
||||||
|
disabled={loading}
|
||||||
|
bind:value={email}
|
||||||
|
autocomplete="email"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
label="Password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="At least 8 characters"
|
||||||
|
required
|
||||||
|
disabled={loading}
|
||||||
|
bind:value={password}
|
||||||
|
autocomplete="new-password"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
label="Confirm password"
|
||||||
|
name="confirm_password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Confirm your password"
|
||||||
|
required
|
||||||
|
disabled={loading}
|
||||||
|
bind:value={confirmPassword}
|
||||||
|
autocomplete="new-password"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="monaco"
|
||||||
|
size="lg"
|
||||||
|
class="w-full"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{#if loading}
|
||||||
|
<LoadingSpinner size="sm" class="mr-2" />
|
||||||
|
Creating admin account...
|
||||||
|
{:else}
|
||||||
|
Create Admin Account
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<p class="mt-6 text-center text-xs text-white/60">
|
||||||
|
© 2026 Monaco USA. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Loading…
Reference in New Issue