Files
immich/web/src/routes/auth/login/+page.svelte
2025-08-13 19:45:06 -04:00

187 lines
5.7 KiB
Svelte

<script lang="ts">
import { goto } from '$app/navigation';
import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte';
import { AppRoute } from '$lib/constants';
import { eventManager } from '$lib/managers/event-manager.svelte';
import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
import { oauth } from '$lib/utils';
import { getServerErrorMessage, handleError } from '$lib/utils/handle-error';
import { login, type LoginResponseDto } from '@immich/sdk';
import { Alert, Button, Field, Input, PasswordInput, Stack } from '@immich/ui';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
interface Props {
data: PageData;
}
let { data }: Props = $props();
let errorMessage: string = $state('');
let email = $state('');
let password = $state('');
let oauthError = $state('');
let loading = $state(false);
let oauthLoading = $state(true);
const onSuccess = async (user: LoginResponseDto) => {
await goto(data.continueUrl, { invalidateAll: true });
eventManager.emit('auth.login', user);
};
const onFirstLogin = () => goto(AppRoute.AUTH_CHANGE_PASSWORD);
const onOnboarding = () => goto(AppRoute.AUTH_ONBOARDING);
onMount(async () => {
if (!$featureFlags.oauth) {
oauthLoading = false;
return;
}
if (oauth.isCallback(globalThis.location)) {
try {
const user = await oauth.login(globalThis.location);
if (!user.isOnboarded) {
await onOnboarding();
return;
}
await onSuccess(user);
return;
} catch (error) {
console.error('Error [login-form] [oauth.callback]', error);
oauthError = getServerErrorMessage(error) || $t('errors.unable_to_complete_oauth_login');
oauthLoading = false;
return;
}
}
try {
if (
($featureFlags.oauthAutoLaunch && !oauth.isAutoLaunchDisabled(globalThis.location)) ||
oauth.isAutoLaunchEnabled(globalThis.location)
) {
await goto(`${AppRoute.AUTH_LOGIN}?autoLaunch=0`, { replaceState: true });
await oauth.authorize(globalThis.location);
return;
}
} catch (error) {
handleError(error, $t('errors.unable_to_connect'));
}
oauthLoading = false;
});
const handleLogin = async () => {
try {
errorMessage = '';
loading = true;
const user = await login({ loginCredentialDto: { email, password } });
if (user.isAdmin && !$serverConfig.isOnboarded) {
await onOnboarding();
return;
}
// change the user password before we onboard them
if (!user.isAdmin && user.shouldChangePassword) {
await onFirstLogin();
return;
}
// We want to onboard after the first login since their password will change
// and handleLogin will be called again (relogin). We then do onboarding on that next call.
if (!user.isOnboarded) {
await onOnboarding();
return;
}
await onSuccess(user);
return;
} catch (error) {
errorMessage = getServerErrorMessage(error) || $t('errors.incorrect_email_or_password');
loading = false;
return;
}
};
const handleOAuthLogin = async () => {
oauthLoading = true;
oauthError = '';
const success = await oauth.authorize(globalThis.location);
if (!success) {
oauthLoading = false;
oauthError = $t('errors.unable_to_login_with_oauth');
}
};
const onsubmit = async (event: Event) => {
event.preventDefault();
await handleLogin();
};
</script>
{#if $featureFlags.loaded}
<AuthPageLayout title={data.meta.title}>
<Stack gap={4}>
{#if $serverConfig.loginPageMessage}
<Alert color="primary" class="mb-6">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html $serverConfig.loginPageMessage}
</Alert>
{/if}
{#if !oauthLoading && $featureFlags.passwordLogin}
<form {onsubmit} class="flex flex-col gap-4">
{#if errorMessage}
<Alert color="danger" title={errorMessage} closable />
{/if}
<Field label={$t('email')}>
<Input id="email" name="email" type="email" autocomplete="email" bind:value={email} />
</Field>
<Field label={$t('password')}>
<PasswordInput id="password" bind:value={password} autocomplete="current-password" />
</Field>
<Button type="submit" size="large" shape="round" fullWidth {loading} class="mt-6">{$t('to_login')}</Button>
</form>
{/if}
{#if $featureFlags.oauth}
{#if $featureFlags.passwordLogin}
<div class="inline-flex w-full items-center justify-center my-4">
<hr class="my-4 h-px w-3/4 border-0 bg-gray-200 dark:bg-gray-600" />
<span
class="absolute start-1/2 -translate-x-1/2 bg-gray-50 px-3 font-medium text-gray-900 dark:bg-neutral-900 dark:text-white"
>
{$t('or').toUpperCase()}
</span>
</div>
{/if}
{#if oauthError}
<Alert color="danger" title={oauthError} closable />
{/if}
<Button
shape="round"
loading={loading || oauthLoading}
disabled={loading || oauthLoading}
size="large"
fullWidth
color={$featureFlags.passwordLogin ? 'secondary' : 'primary'}
onclick={handleOAuthLogin}
>
{$serverConfig.oauthButtonText}
</Button>
{/if}
{#if !$featureFlags.passwordLogin && !$featureFlags.oauth}
<Alert color="warning" title={$t('login_has_been_disabled')} />
{/if}
</Stack>
</AuthPageLayout>
{/if}