feat(web): improve /auth pages (#1969)

* feat(web): improve /auth pages

* invalidate load functions after login

* handle login server errors more graceful

* add loading state to oauth button
This commit is contained in:
Michel Heusschen 2023-03-15 22:38:29 +01:00 committed by GitHub
parent 04955a4123
commit 87d84b922f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 299 additions and 276 deletions

View file

@ -1,14 +1,11 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { AppRoute } from '$lib/constants';
import { api } from '@api';
import ImmichLogo from '../shared-components/immich-logo.svelte';
let error: string;
let success: string;
let password = '';
let confirmPassowrd = '';
let canRegister = false;
$: {
@ -21,13 +18,11 @@
}
}
async function registerAdmin(event: SubmitEvent) {
async function registerAdmin(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
if (canRegister) {
error = '';
const formElement = event.target as HTMLFormElement;
const form = new FormData(formElement);
const form = new FormData(event.currentTarget);
const email = form.get('email');
const password = form.get('password');
@ -42,7 +37,7 @@
});
if (status === 201) {
goto('/auth/login');
goto(AppRoute.AUTH_LOGIN);
return;
} else {
error = 'Error create admin account';
@ -52,81 +47,74 @@
}
</script>
<div
class="border bg-immich-bg dark:bg-immich-dark-gray dark:border-immich-dark-gray p-4 shadow-sm w-[500px] max-w-[95vw] rounded-3xl py-8 dark:text-immich-dark-fg"
>
<div class="flex flex-col place-items-center place-content-center gap-4 px-4">
<ImmichLogo class="text-center" height="100" width="100" />
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">
Admin Registration
</h1>
<p
class="text-sm border rounded-md p-4 font-mono text-gray-600 dark:border-immich-dark-bg dark:text-gray-300"
>
Since you are the first user on the system, you will be assigned as the Admin and are
responsible for administrative tasks, and additional users will be created by you.
</p>
<form on:submit|preventDefault={registerAdmin} method="post" class="flex flex-col gap-5 mt-5">
<div class="flex flex-col gap-2">
<label class="immich-form-label" for="email">Admin Email</label>
<input
class="immich-form-input"
id="email"
name="email"
type="email"
autocomplete="email"
required
/>
</div>
<form on:submit|preventDefault={registerAdmin} method="post" action="" autocomplete="off">
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="email">Admin Email</label>
<input class="immich-form-input" id="email" name="email" type="email" required />
</div>
<div class="flex flex-col gap-2">
<label class="immich-form-label" for="password">Admin Password</label>
<input
class="immich-form-input"
id="password"
name="password"
type="password"
autocomplete="new-password"
required
bind:value={password}
/>
</div>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="password">Admin Password</label>
<input
class="immich-form-input"
id="password"
name="password"
type="password"
required
bind:value={password}
/>
</div>
<div class="flex flex-col gap-2">
<label class="immich-form-label" for="confirmPassword">Confirm Admin Password</label>
<input
class="immich-form-input"
id="confirmPassword"
name="password"
type="password"
autocomplete="new-password"
required
bind:value={confirmPassowrd}
/>
</div>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="confirmPassword">Confirm Admin Password</label>
<input
class="immich-form-input"
id="confirmPassword"
name="password"
type="password"
required
bind:value={confirmPassowrd}
/>
</div>
<div class="flex flex-col gap-2">
<label class="immich-form-label" for="firstName">First Name</label>
<input
class="immich-form-input"
id="firstName"
name="firstName"
type="text"
autocomplete="given-name"
required
/>
</div>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="firstName">First Name</label>
<input class="immich-form-input" id="firstName" name="firstName" type="text" required />
</div>
<div class="flex flex-col gap-2">
<label class="immich-form-label" for="lastName">Last Name</label>
<input
class="immich-form-input"
id="lastName"
name="lastName"
type="text"
autocomplete="family-name"
required
/>
</div>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="lastName">Last Name</label>
<input class="immich-form-input" id="lastName" name="lastName" type="text" required />
</div>
{#if error}
<p class="text-red-400">{error}</p>
{/if}
{#if error}
<p class="text-red-400 ml-4">{error}</p>
{/if}
{#if success}
<div>
<p>Admin account has been registered</p>
<p>
<a href="/auth/login">Login</a>
</p>
</div>
{/if}
<div class="flex w-full">
<button
type="submit"
class="m-4 p-2 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-6 py-4 text-white rounded-md shadow-md w-full"
>Sign Up</button
>
</div>
</form>
</div>
<div class="my-5 flex w-full">
<button type="submit" class="immich-btn-primary-big">Sign Up</button>
</div>
</form>

View file

@ -1,7 +1,6 @@
<script lang="ts">
import { api, UserResponseDto } from '@api';
import { createEventDispatcher } from 'svelte';
import ImmichLogo from '../shared-components/immich-logo.svelte';
export let user: UserResponseDto;
let error: string;
@ -44,61 +43,41 @@
}
</script>
<div
class="border bg-gray-50 dark:bg-immich-dark-gray dark:border-immich-dark-gray p-4 shadow-sm w-[500px] max-w-[95vw] rounded-3xl py-8 dark:text-immich-dark-fg"
>
<div class="flex flex-col place-items-center place-content-center gap-4 px-4">
<ImmichLogo class="text-center" height="100" width="100" />
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">
Change Password
</h1>
<p
class="text-sm border rounded-3xl p-6 text-gray-600 dark:border-immich-dark-bg dark:text-gray-300 bg-immich-bg dark:bg-gray-900"
>
Hi {user.firstName}
{user.lastName} ({user.email}),
<br />
<br />
This is either the first time you are signing into the system or a request has been made to change
your password. Please enter the new password below.
</p>
<form on:submit|preventDefault={changePassword} method="post" class="flex flex-col gap-5 mt-5">
<div class="flex flex-col gap-2">
<label class="immich-form-label" for="password">New Password</label>
<input
class="immich-form-input"
id="password"
name="password"
type="password"
autocomplete="new-password"
required
bind:value={password}
/>
</div>
<form on:submit|preventDefault={changePassword} method="post" autocomplete="off">
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="password">New Password</label>
<input
class="immich-form-input"
id="password"
name="password"
type="password"
required
bind:value={password}
/>
</div>
<div class="flex flex-col gap-2">
<label class="immich-form-label" for="confirmPassword">Confirm Password</label>
<input
class="immich-form-input"
id="confirmPassword"
name="password"
type="password"
autocomplete="current-password"
required
bind:value={confirmPassowrd}
/>
</div>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="confirmPassword">Confirm Password</label>
<input
class="immich-form-input"
id="confirmPassword"
name="password"
type="password"
required
bind:value={confirmPassowrd}
/>
</div>
{#if error}
<p class="text-red-400 text-sm">{error}</p>
{/if}
{#if error}
<p class="text-red-400 ml-4 text-sm">{error}</p>
{/if}
{#if success}
<p class="text-immich-primary ml-4 text-sm">{success}</p>
{/if}
<div class="flex w-full">
<button type="submit" class="immich-btn-primary-big m-4">Change Password</button>
</div>
</form>
</div>
{#if success}
<p class="text-immich-primary text-sm">{success}</p>
{/if}
<div class="my-5 flex w-full">
<button type="submit" class="immich-btn-primary-big">Change Password</button>
</div>
</form>

View file

@ -1,33 +1,33 @@
<script lang="ts">
import { goto } from '$app/navigation';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import { loginPageMessage } from '$lib/constants';
import { AppRoute } from '$lib/constants';
import { handleError } from '$lib/utils/handle-error';
import { api, oauth, OAuthConfigResponseDto } from '@api';
import { createEventDispatcher, onMount } from 'svelte';
import { fade } from 'svelte/transition';
import ImmichLogo from '../shared-components/immich-logo.svelte';
let error: string;
let email = '';
let password = '';
let oauthError: string;
let authConfig: OAuthConfigResponseDto = { enabled: false, passwordLoginEnabled: false };
let loading = true;
export let authConfig: OAuthConfigResponseDto;
let loading = false;
let oauthLoading = true;
const dispatch = createEventDispatcher();
onMount(async () => {
if (oauth.isCallback(window.location)) {
try {
loading = true;
await oauth.login(window.location);
dispatch('success');
return;
} catch (e) {
console.error('Error [login-form] [oauth.callback]', e);
oauthError = 'Unable to complete OAuth login';
loading = false;
} finally {
oauthLoading = false;
}
}
@ -38,7 +38,7 @@
const { enabled, url, autoLaunch } = authConfig;
if (enabled && url && autoLaunch && !oauth.isAutoLaunchDisabled(window.location)) {
await goto('/auth/login?autoLaunch=0', { replaceState: true });
await goto(`${AppRoute.AUTH_LOGIN}?autoLaunch=0`, { replaceState: true });
await goto(url);
return;
}
@ -47,7 +47,7 @@
handleError(error, 'Unable to connect!');
}
loading = false;
oauthLoading = false;
});
const login = async () => {
@ -75,100 +75,89 @@
};
</script>
<div
class="border bg-white dark:bg-immich-dark-gray dark:border-immich-dark-gray p-8 shadow-sm w-full max-w-lg rounded-3xl"
>
<div class="flex flex-col place-items-center place-content-center gap-4 py-4">
<ImmichLogo class="text-center h-24 w-24" />
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">Login</h1>
</div>
{#if loginPageMessage}
<p
class="text-sm border rounded-xl p-4 text-immich-primary dark:text-immich-dark-primary font-medium bg-immich-primary/5 dark:border-immich-dark-bg w-full border-immich-primary border-2"
>
{@html loginPageMessage}
</p>
{/if}
{#if authConfig.passwordLoginEnabled}
<form on:submit|preventDefault={login} class="flex flex-col gap-5 mt-5">
{#if error}
<p class="text-red-400" transition:fade>
{error}
</p>
{/if}
<div class="flex flex-col gap-2">
<label class="immich-form-label" for="email">Email</label>
<input
class="immich-form-input"
id="email"
name="email"
type="email"
bind:value={email}
required
/>
</div>
<div class="flex flex-col gap-2">
<label class="immich-form-label" for="password">Password</label>
<input
class="immich-form-input"
id="password"
name="password"
type="password"
bind:value={password}
required
/>
</div>
<div class="my-5 flex w-full">
<button
type="submit"
class="immich-btn-primary-big inline-flex items-center h-14"
disabled={loading}
>
{#if loading}
<LoadingSpinner />
{:else}
Login
{/if}
</button>
</div>
</form>
{/if}
{#if authConfig.enabled}
{#if authConfig.passwordLoginEnabled}
<div class="inline-flex items-center justify-center w-full">
<hr class="w-3/4 h-px my-4 bg-gray-200 border-0 dark:bg-gray-600" />
<span
class="absolute px-3 font-medium text-gray-900 -translate-x-1/2 left-1/2 dark:text-white bg-white dark:bg-immich-dark-gray"
>
or
</span>
</div>
{#if authConfig.passwordLoginEnabled}
<form on:submit|preventDefault={login} class="flex flex-col gap-5 mt-5">
{#if error}
<p class="text-red-400" transition:fade>
{error}
</p>
{/if}
<div class="my-5 flex flex-col gap-5">
{#if oauthError}
<p class="text-red-400" transition:fade>{oauthError}</p>
{/if}
<a href={authConfig.url} class="flex w-full">
<button
type="button"
disabled={loading}
class={authConfig.passwordLoginEnabled
? 'immich-btn-secondary-big'
: 'immich-btn-primary-big'}
>
{authConfig.buttonText || 'Login with OAuth'}
</button>
</a>
<div class="flex flex-col gap-2">
<label class="immich-form-label" for="email">Email</label>
<input
class="immich-form-input"
id="email"
name="email"
type="email"
autocomplete="email"
bind:value={email}
required
/>
</div>
<div class="flex flex-col gap-2">
<label class="immich-form-label" for="password">Password</label>
<input
class="immich-form-input"
id="password"
name="password"
type="password"
autocomplete="current-password"
bind:value={password}
required
/>
</div>
<div class="my-5 flex w-full">
<button
type="submit"
class="immich-btn-primary-big inline-flex items-center h-14"
disabled={loading || oauthLoading}
>
{#if loading}
<LoadingSpinner />
{:else}
Login
{/if}
</button>
</div>
</form>
{/if}
{#if authConfig.enabled}
{#if authConfig.passwordLoginEnabled}
<div class="inline-flex items-center justify-center w-full">
<hr class="w-3/4 h-px my-4 bg-gray-200 border-0 dark:bg-gray-600" />
<span
class="absolute px-3 font-medium text-gray-900 -translate-x-1/2 left-1/2 dark:text-white bg-white dark:bg-immich-dark-gray"
>
or
</span>
</div>
{/if}
<div class="my-5 flex flex-col gap-5">
{#if oauthError}
<p class="text-red-400" transition:fade>{oauthError}</p>
{/if}
<a href={authConfig.url} class="flex w-full">
<button
type="button"
disabled={loading || oauthLoading}
class={'inline-flex items-center h-14 ' + authConfig.passwordLoginEnabled
? 'immich-btn-secondary-big'
: 'immich-btn-primary-big'}
>
{#if oauthLoading}
<LoadingSpinner />
{:else}
{authConfig.buttonText || 'Login with OAuth'}
{/if}
</button>
</a>
</div>
{/if}
{#if !authConfig.enabled && !authConfig.passwordLoginEnabled}
<p class="text-center dark:text-immich-dark-fg p-4">Login has been disabled.</p>
{/if}
</div>
{#if !authConfig.enabled && !authConfig.passwordLoginEnabled}
<p class="text-center dark:text-immich-dark-fg p-4">Login has been disabled.</p>
{/if}