Explorar el Código

fix(web): login error handling (#1322)

Jason Rasmussen hace 2 años
padre
commit
5fb3ea465f

+ 6 - 0
web/src/app.d.ts

@@ -8,6 +8,12 @@ declare namespace App {
 	}
 
 	// interface Platform {}
+
+	interface Error {
+		message: string;
+		stack?: string;
+		code?: string;
+	}
 }
 
 // Source: https://stackoverflow.com/questions/63814432/typescript-typing-of-non-standard-window-event-in-svelte

+ 11 - 1
web/src/hooks.server.ts

@@ -1,5 +1,15 @@
-import type { Handle } from '@sveltejs/kit';
+import type { Handle, HandleServerError } from '@sveltejs/kit';
+import { AxiosError } from 'axios';
 
 export const handle: Handle = async ({ event, resolve }) => {
 	return await resolve(event);
 };
+
+export const handleError: HandleServerError = async ({ error }) => {
+	const httpError = error as AxiosError;
+	return {
+		message: httpError?.message || 'Hmm, not sure about that. Check the logs or open a ticket?',
+		stack: httpError?.stack,
+		code: httpError.code || '500'
+	};
+};

+ 13 - 11
web/src/lib/components/forms/login-form.svelte

@@ -2,6 +2,7 @@
 	import { goto } from '$app/navigation';
 	import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
 	import { loginPageMessage } from '$lib/constants';
+	import { handleError } from '$lib/utils/handle-error';
 	import { api, oauth, OAuthConfigResponseDto } from '@api';
 	import { createEventDispatcher, onMount } from 'svelte';
 
@@ -9,7 +10,7 @@
 	let email = '';
 	let password = '';
 	let oauthError: string;
-	let oauthConfig: OAuthConfigResponseDto = { enabled: false, passwordLoginEnabled: false };
+	let authConfig: OAuthConfigResponseDto = { enabled: false, passwordLoginEnabled: false };
 	let loading = true;
 
 	const dispatch = createEventDispatcher();
@@ -30,17 +31,18 @@
 
 		try {
 			const { data } = await oauth.getConfig(window.location);
-			oauthConfig = data;
+			authConfig = data;
 
-			const { enabled, url, autoLaunch } = oauthConfig;
+			const { enabled, url, autoLaunch } = authConfig;
 
 			if (enabled && url && autoLaunch && !oauth.isAutoLaunchDisabled(window.location)) {
 				await goto('/auth/login?autoLaunch=0', { replaceState: true });
 				await goto(url);
 				return;
 			}
-		} catch (e) {
-			console.error('Error [login-form] [oauth.generateConfig]', e);
+		} catch (error) {
+			authConfig.passwordLoginEnabled = true;
+			handleError(error, 'Unable to connect!');
 		}
 
 		loading = false;
@@ -92,7 +94,7 @@
 			<LoadingSpinner />
 		</div>
 	{:else}
-		{#if oauthConfig.passwordLoginEnabled}
+		{#if authConfig.passwordLoginEnabled}
 			<form on:submit|preventDefault={login} autocomplete="off">
 				<div class="m-4 flex flex-col gap-2">
 					<label class="immich-form-label" for="email">Email</label>
@@ -133,26 +135,26 @@
 			</form>
 		{/if}
 
-		{#if oauthConfig.enabled}
+		{#if authConfig.enabled}
 			<div class="flex flex-col gap-4 px-4">
-				{#if oauthConfig.passwordLoginEnabled}
+				{#if authConfig.passwordLoginEnabled}
 					<hr />
 				{/if}
 				{#if oauthError}
 					<p class="text-red-400">{oauthError}</p>
 				{/if}
-				<a href={oauthConfig.url} class="flex w-full">
+				<a href={authConfig.url} class="flex w-full">
 					<button
 						type="button"
 						disabled={loading}
 						class="bg-immich-primary dark:bg-immich-dark-primary dark:text-immich-dark-gray dark:hover:bg-immich-dark-primary/80 hover:bg-immich-primary/75 px-6 py-4 text-white rounded-md shadow-md w-full font-semibold"
-						>{oauthConfig.buttonText || 'Login with OAuth'}</button
+						>{authConfig.buttonText || 'Login with OAuth'}</button
 					>
 				</a>
 			</div>
 		{/if}
 
-		{#if !oauthConfig.enabled && !oauthConfig.passwordLoginEnabled}
+		{#if !authConfig.enabled && !authConfig.passwordLoginEnabled}
 			<p class="text-center dark:text-immich-dark-fg p-4">Login has been disabled.</p>
 		{/if}
 	{/if}

+ 3 - 1
web/src/lib/components/shared-components/notification/notification-card.svelte

@@ -90,5 +90,7 @@
 		</button>
 	</div>
 
-	<p class="text-sm pl-[28px] pr-[16px]" data-testid="message">{@html notificationInfo.message}</p>
+	<p class="whitespace-pre text-sm pl-[28px] pr-[16px]" data-testid="message">
+		{@html notificationInfo.message}
+	</p>
 </div>

+ 7 - 1
web/src/lib/utils/handle-error.ts

@@ -6,8 +6,14 @@ import {
 
 export function handleError(error: unknown, message: string) {
 	console.error(`[handleError]: ${message}`, error);
+
+	let serverMessage = (error as ApiError)?.response?.data?.message;
+	if (serverMessage) {
+		serverMessage = `${String(serverMessage).slice(0, 50)}\n<i>(Immich Server Error)<i>`;
+	}
+
 	notificationController.show({
-		message: (error as ApiError)?.response?.data?.message || message,
+		message: serverMessage || message,
 		type: NotificationType.Error
 	});
 }

+ 113 - 20
web/src/routes/+error.svelte

@@ -1,29 +1,122 @@
 <script>
 	import { page } from '$app/stores';
+	import Message from 'svelte-material-icons/Message.svelte';
+	import PartyPopper from 'svelte-material-icons/PartyPopper.svelte';
+	import CodeTags from 'svelte-material-icons/CodeTags.svelte';
+	import ContentCopy from 'svelte-material-icons/ContentCopy.svelte';
+	import {
+		notificationController,
+		NotificationType
+	} from '$lib/components/shared-components/notification/notification';
+	import { handleError } from '$lib/utils/handle-error';
+
+	const handleCopy = async () => {
+		//
+		const error = $page.error || null;
+		if (!error) {
+			return;
+		}
+
+		try {
+			await navigator.clipboard.writeText(`${error.message} - ${error.code}\n${error.stack}`);
+			notificationController.show({
+				type: NotificationType.Info,
+				message: 'Copied error to clipboard'
+			});
+		} catch (error) {
+			handleError(error, 'Unable to copy to clipboard');
+		}
+	};
 </script>
 
-<div class="h-screen w-screen  flex place-items-center place-content-center flex-col">
-	<div class="min-w-[500px] max-w-[95vw]  bg-gray-300 rounded-2xl my-4 p-4">
-		<code class="text-xs text-red-500">Error code {$page.status}</code>
-		<br />
-		<code class="text-sm">
-			{$page.error?.message}
-		</code>
-		<br />
-		<div class="mt-5">
-			<p class="text-sm font-medium">Verbose</p>
-			<pre class="text-xs">{JSON.stringify($page.error)}</pre>
+<div class="h-screen w-screen">
+	<section class="bg-immich-bg dark:bg-immich-dark-bg">
+		<div class="flex border-b dark:border-b-immich-dark-gray place-items-center px-6 py-4">
+			<a class="flex gap-2 place-items-center hover:cursor-pointer" href="/photos">
+				<img src="/immich-logo.svg" alt="immich logo" height="35" width="35" />
+				<h1 class="font-immich-title text-2xl text-immich-primary dark:text-immich-dark-primary">
+					IMMICH
+				</h1>
+			</a>
 		</div>
+	</section>
 
-		<a
-			href="https://github.com/immich-app/immich/issues/new/choose"
-			target="_blank"
-			rel="noopener noreferrer"
-		>
-			<button
-				class="px-5 py-2 rounded-lg text-sm mt-6 bg-immich-primary text-white hover:bg-immich-primary/75"
-				>Get help</button
+	<div
+		class="fixed top-0 w-full h-full  bg-black/50 flex place-items-center place-content-center overflow-hidden"
+	>
+		<div>
+			<div
+				class="border bg-immich-bg dark:bg-immich-dark-gray dark:border-immich-dark-gray shadow-sm w-[500px] max-w-[95vw] rounded-3xl dark:text-immich-dark-fg"
 			>
-		</a>
+				<div>
+					<div class="flex items-center justify-between gap-4 px-4 py-4">
+						<h1 class="text-immich-primary dark:text-immich-dark-primary font-medium">
+							🚨 Error - Something went wrong
+						</h1>
+						<div class="flex justify-end">
+							<button
+								on:click={() => handleCopy()}
+								class="transition-colors bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 dark:hover:bg-immich-dark-primary/80 dark:text-immich-dark-gray px-3 py-2 text-white rounded-full shadow-md text-sm"
+							>
+								<ContentCopy size={24} />
+							</button>
+						</div>
+					</div>
+
+					<hr />
+
+					<div class="p-4 max-h-[75vh] min-h-[300px] overflow-y-auto immich-scrollbar pb-4 gap-4">
+						<div class="flex flex-col w-full gap-2">
+							<p class="text-red-500">{$page.error?.message} - {$page.error?.code}</p>
+							{#if $page.error?.stack}
+								<label for="stacktrace">Stacktrace</label>
+								<pre id="stacktrace" class="text-xs">{$page.error?.stack || 'No stack'}</pre>
+							{/if}
+						</div>
+					</div>
+
+					<hr />
+
+					<div class="flex justify-around place-items-center place-content-center">
+						<!-- href="https://github.com/immich-app/immich/issues/new" -->
+						<a
+							href="https://discord.com/invite/D8JsnBEuKb"
+							target="_blank"
+							rel="noopener noreferrer"
+							class="flex justify-center grow basis-0 p-4"
+						>
+							<button class="flex flex-col gap-2 place-items-center place-content-center">
+								<Message size={24} />
+								<p class="text-sm">Get Help</p>
+							</button>
+						</a>
+
+						<a
+							href="https://github.com/immich-app/immich/releases"
+							target="_blank"
+							rel="noopener noreferrer"
+							class="flex justify-center grow basis-0 p-4"
+						>
+							<button class="flex flex-col gap-2 place-items-center place-content-center">
+								<PartyPopper size={24} />
+								<p class="text-sm">Read Changelog</p>
+							</button>
+						</a>
+
+						<a
+							href="https://immich.app/docs/guides/docker-help"
+							target="_blank"
+							rel="noopener noreferrer"
+							class="flex justify-center grow basis-0 p-4"
+						>
+							<button class="flex flex-col gap-2 place-items-center place-content-center">
+								<CodeTags size={24} />
+								<p class="text-sm">Check Logs</p>
+							</button>
+						</a>
+					</div>
+				</div>
+			</div>
+		</div>
 	</div>
 </div>

+ 1 - 1
web/src/routes/+layout.svelte

@@ -78,7 +78,7 @@
 </script>
 
 <svelte:head>
-	<title>{$page.data.meta?.title} - Immich</title>
+	<title>{$page.data.meta?.title || 'Web'} - Immich</title>
 	{#if $page.data.meta}
 		<meta name="description" content={$page.data.meta.description} />