Skip to content

Commit

Permalink
Fix up login form to display errors better
Browse files Browse the repository at this point in the history
  • Loading branch information
sneakycrow committed Jan 11, 2025
1 parent 01f4e40 commit 9c562c8
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 63 deletions.
32 changes: 27 additions & 5 deletions services/barn-ui/src/lib/components/Form.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,30 @@
export let error: string | null = null;
let isSubmitting = false;
let formError: string | null = null;
function handleSubmit() {
isSubmitting = true;
return async ({ update }: { update: () => Promise<void> }) => {
await update();
isSubmitting = false;
formError = null; // Reset error state before submission
return async ({
update,
result
}: {
update: () => Promise<void>;
result: { type: string; error?: { message: string } };
}) => {
try {
await update();
if (result.type === 'error') {
formError = result.error?.message || 'An unexpected error occurred';
}
} catch (err) {
formError = err instanceof Error ? err.message : 'An unexpected error occurred';
} finally {
isSubmitting = false;
}
};
}
</script>
Expand All @@ -25,8 +43,12 @@
use:enhance={handleSubmit}
class="mt-8 w-full max-w-sm flex-grow rounded border-2 border-secondary-900 bg-white p-6 shadow-md dark:border-primary-800 dark:bg-primary-900 dark:shadow-xl"
>
{#if error}
<Alert type="error" message={error} class="mb-4" />
{#if error || formError}
<Alert
type="error"
message={error || formError || 'There was an error submitting, try again'}
class="mb-4"
/>
{/if}

<slot />
Expand Down
166 changes: 120 additions & 46 deletions services/barn-ui/src/routes/(guest)/login/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,136 @@
import { redirect } from '@sveltejs/kit';
import { redirect, error } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';
import { env } from '$env/dynamic/private';

export const load: PageServerLoad = async ({ url, cookies, locals }) => {
const token = url.searchParams.get('token');
if (token) {
cookies.set('jwt', token, {
path: '/',
expires: new Date(Date.now() + 1000 * 60 * 60 * 24),
sameSite: true
});
}
try {
const token = url.searchParams.get('token');
if (token) {
cookies.set('jwt', token, {
path: '/',
expires: new Date(Date.now() + 1000 * 60 * 60 * 24),
sameSite: true,
httpOnly: true, // Added security measure
secure: process.env.NODE_ENV === 'production' // Secure in production
});
}

return {
loggedIn: Boolean(locals.user || token)
};
return {
loggedIn: Boolean(locals.user || token)
};
} catch (err) {
console.error('Login load error:', err);
throw error(500, 'Failed to process login request');
}
};

export const actions = {
default: async ({ request, cookies }) => {
const data = await request.formData();
const username = data.get('username');
const password = data.get('password');
try {
const data = await request.formData();
const username = data.get('username');
const password = data.get('password');

// Basic validation
if (!username || !password) {
return {
error: 'Username and password are required',
username: username?.toString()
};
}
// Validation
if (!username || !password) {
return {
type: 'error',
error: {
message: 'Username and password are required'
},
data: {
username: username?.toString()
}
};
}

// Input sanitization
const sanitizedUsername = username.toString().trim();
const sanitizedPassword = password.toString();

if (!sanitizedUsername || !sanitizedPassword) {
return {
type: 'error',
error: {
message: 'Invalid input provided'
},
data: {
username: sanitizedUsername
}
};
}

// API call
let response;
try {
response = await fetch(`${env.API_URL}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: sanitizedUsername,
password: sanitizedPassword
})
});
} catch (fetchError) {
console.error('Login API call failed:', fetchError);
return {
type: 'error',
error: {
message: 'Unable to connect to authentication service'
},
data: {
username: sanitizedUsername
}
};
}

if (!response.ok) {
const errorData = await response.json();
return {
type: 'error',
error: {
message: errorData.message || 'Invalid credentials'
},
data: {
username: sanitizedUsername
}
};
}

const { token } = await response.json();

if (!token) {
return {
type: 'error',
error: {
message: 'Invalid response from authentication service'
},
data: {
username: sanitizedUsername
}
};
}

// Set cookie with security options
cookies.set('jwt', token, {
path: '/',
expires: new Date(Date.now() + 1000 * 60 * 60 * 24), // 24 hours
sameSite: true,
httpOnly: true, // Prevents JavaScript access to the cookie
secure: process.env.NODE_ENV === 'production' // Secure in production
});

const response = await fetch(`${env.API_URL}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: username.toString(),
password: password.toString()
})
});

if (!response.ok) {
const errorData = await response.json();
throw redirect(303, '/');
} catch (err) {
console.error('Login action error:', err);
return {
error: errorData.message || 'Invalid credentials',
username: username.toString()
type: 'error',
error: {
message: 'An unexpected error occurred during login'
}
};
}
const { token } = await response.json();
// Set the cookie so we can get the user again later
cookies.set('jwt', token, {
path: '/',
expires: new Date(Date.now() + 1000 * 60 * 60 * 24), // 24 hours
sameSite: true
});
// Redirect the user
throw redirect(303, '/');
}
} satisfies Actions;
51 changes: 39 additions & 12 deletions services/barn-ui/src/routes/(guest)/login/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,64 @@
import Form from '$lib/components/Form.svelte';
import Input from '$lib/components/Input.svelte';
import BrandButton from '$lib/components/BrandButton.svelte';
import type { ActionData, PageServerData } from './$types';
import type { ActionData, PageData } from './$types';
import { goto } from '$app/navigation';
let { form, data }: { form: ActionData; data: PageServerData } = $props();
import { onMount } from 'svelte';
import Throbber from '$lib/components/Throbber.svelte';
let { form, data } = $props<{ form: ActionData; data: PageData }>();
let isRedirecting = $state(false);
onMount(() => {
if (!data.loggedIn) return;
isRedirecting = true;
const interval = setInterval(async () => {
if (data?.loggedIn) {
try {
if (data?.loggedIn) {
clearInterval(interval);
await goto('/', { invalidateAll: true });
}
} catch (error) {
console.error('Login redirect error:', error);
isRedirecting = false;
clearInterval(interval);
goto('/', { invalidateAll: true });
}
}, 250);
return () => clearInterval(interval);
});
function handleTwitchLogin() {
try {
goto('/auth/twitch');
} catch (error) {
console.error('Twitch login error:', error);
// Could add error state handling here if needed
}
}
</script>

{#if data.loggedIn}
<section class="flex min-h-[400px] w-full flex-col items-center justify-center space-y-4">
{#if isRedirecting || data.loggedIn}
<section
class="flex min-h-[400px] w-full flex-col items-center justify-center space-y-4"
role="status"
aria-live="polite"
>
<Throbber width="w-10" />
<p class="text-sm">Logging you in</p>
<p class="text-sm">Logging you in...</p>
</section>
{:else}
<section class="flex flex-col items-center space-y-4">
<aside class="flex flex-col space-y-4 text-center">
<header class="flex flex-col space-y-4 text-center">
<h1 class="font-serif text-2xl text-secondary-700 dark:text-primary-500">Login</h1>
<p class="text-secondary-800 dark:text-primary-100">Login to your farmhand account</p>
</aside>
<BrandButton brand="twitch" onclick={() => goto('/auth/twitch')} />
<Form submitText="Login" loadingText="Logging in..." error={form?.error}>
<Input label="Username" name="username" type="text" value={form?.username ?? ''} />
</header>

<BrandButton brand="twitch" onclick={handleTwitchLogin} />

<Form submitText="Login" loadingText="Logging in..." error={form?.error?.message}>
<Input label="Username" name="username" type="text" value={form?.data?.username ?? ''} />
<Input label="Password" name="password" type="password" />
</Form>
</section>
Expand Down

0 comments on commit 9c562c8

Please sign in to comment.