mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
feat: oauth re-link via admin-provided token
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
import { Route } from '$lib/route';
|
||||
import { oauth } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { Button, toastManager } from '@immich/ui';
|
||||
import { Button, Stack, Text, toastManager } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
@@ -20,18 +20,28 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class="my-4">
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<div class="sm:ms-8 flex justify-end">
|
||||
{#if featureFlagsManager.value.oauth}
|
||||
{#if featureFlagsManager.value.oauth}
|
||||
<section class="my-4">
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<Stack gap={3}>
|
||||
{#if authManager.user.oauthId}
|
||||
<Button shape="round" size="small" onclick={() => handleUnlink()}>{$t('unlink_oauth')}</Button>
|
||||
<Text>{$t('oauth_account_is_linked')}</Text>
|
||||
{#if featureFlagsManager.value.passwordLogin}
|
||||
<div class="sm:ms-8 flex justify-end">
|
||||
<Button shape="round" size="small" color="danger" onclick={() => handleUnlink()}>
|
||||
{$t('unlink_oauth')}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Button shape="round" size="small" onclick={() => goto(Route.login({ autoLaunch: 1 }))}
|
||||
>{$t('link_to_oauth')}</Button
|
||||
>
|
||||
<Text>{$t('oauth_account_not_linked')}</Text>
|
||||
<div class="sm:ms-8 flex justify-end">
|
||||
<Button shape="round" size="small" onclick={() => goto(Route.login({ autoLaunch: 1 }))}>
|
||||
{$t('link_to_oauth')}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { oauth } from '$lib/utils';
|
||||
import { getServerErrorMessage, handleError } from '$lib/utils/handle-error';
|
||||
import { login, register } from '@immich/sdk';
|
||||
import { isHttpError, login, register, startOAuthReLink } from '@immich/sdk';
|
||||
import { Alert, Button, Field, Input, PasswordInput, Stack, toastManager } from '@immich/ui';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
@@ -22,6 +24,41 @@
|
||||
let errorMessage = $state('');
|
||||
let loading = $state(false);
|
||||
let registering = $state(false);
|
||||
let reLinkMode = $state(!!data.reLinkToken);
|
||||
let reLinkLoading = $state(!!data.reLinkToken);
|
||||
let reLinkError = $state('');
|
||||
|
||||
onMount(async () => {
|
||||
if (oauth.isCallback(globalThis.location)) {
|
||||
reLinkLoading = true;
|
||||
try {
|
||||
const user = await oauth.login(globalThis.location);
|
||||
eventManager.emit('AuthLogin', user);
|
||||
await authManager.refresh();
|
||||
toastManager.primary($t('linked_oauth_account'));
|
||||
await goto(Route.photos(), { invalidateAll: true });
|
||||
} catch (error) {
|
||||
reLinkLoading = false;
|
||||
reLinkMode = false;
|
||||
reLinkError =
|
||||
getServerErrorMessage(error) ||
|
||||
(isHttpError(error) ? error.message : undefined) ||
|
||||
$t('errors.unable_to_complete_oauth_login');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.reLinkToken) {
|
||||
try {
|
||||
await startOAuthReLink({ oAuthReLinkStartDto: { token: data.reLinkToken } });
|
||||
await oauth.authorize(globalThis.location);
|
||||
} catch (error) {
|
||||
reLinkLoading = false;
|
||||
reLinkMode = false;
|
||||
reLinkError = getServerErrorMessage(error) || $t('errors.invalid_oauth_relink_token');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const handleSubmit = async (event: Event) => {
|
||||
event.preventDefault();
|
||||
@@ -55,56 +92,66 @@
|
||||
|
||||
<AuthPageLayout title={data.meta.title}>
|
||||
<Stack gap={4}>
|
||||
{#if featureFlagsManager.value.passwordLogin}
|
||||
<Alert color="primary">
|
||||
{$t('oauth_link_existing_account')}
|
||||
</Alert>
|
||||
|
||||
<form onsubmit={handleSubmit} 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>
|
||||
{:else}
|
||||
<Alert color="warning">
|
||||
{$t('oauth_link_password_login_required')}
|
||||
</Alert>
|
||||
{#if reLinkError}
|
||||
<Alert color="danger" title={reLinkError} closable />
|
||||
{/if}
|
||||
|
||||
{#if featureFlagsManager.value.oauthAutoRegister}
|
||||
{#if reLinkMode && reLinkLoading}
|
||||
<Alert color="primary">
|
||||
{$t('oauth_relink_in_progress')}
|
||||
</Alert>
|
||||
{:else}
|
||||
{#if featureFlagsManager.value.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 uppercase"
|
||||
>
|
||||
{$t('or')}
|
||||
</span>
|
||||
</div>
|
||||
<Alert color="primary">
|
||||
{$t('oauth_link_existing_account')}
|
||||
</Alert>
|
||||
|
||||
<form onsubmit={handleSubmit} 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>
|
||||
{:else}
|
||||
<Alert color="warning">
|
||||
{$t('oauth_link_password_login_required')}
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
shape="round"
|
||||
size="large"
|
||||
fullWidth
|
||||
color={featureFlagsManager.value.passwordLogin ? 'secondary' : 'primary'}
|
||||
loading={registering}
|
||||
onclick={handleRegister}
|
||||
>
|
||||
{$t('create_new_account')}
|
||||
</Button>
|
||||
{#if featureFlagsManager.value.oauthAutoRegister}
|
||||
{#if featureFlagsManager.value.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 uppercase"
|
||||
>
|
||||
{$t('or')}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
shape="round"
|
||||
size="large"
|
||||
fullWidth
|
||||
color={featureFlagsManager.value.passwordLogin ? 'secondary' : 'primary'}
|
||||
loading={registering}
|
||||
onclick={handleRegister}
|
||||
>
|
||||
{$t('create_new_account')}
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
</Stack>
|
||||
</AuthPageLayout>
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ url }) => {
|
||||
const email = url.searchParams.get('email') || '';
|
||||
const reLinkToken = url.searchParams.get('token') || '';
|
||||
|
||||
const $t = await getFormatter();
|
||||
return {
|
||||
@@ -10,5 +11,6 @@ export const load = (async ({ url }) => {
|
||||
title: $t('link_to_oauth'),
|
||||
},
|
||||
email,
|
||||
reLinkToken,
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
|
||||
Reference in New Issue
Block a user