feat: auth logout page (#27831)

* feat: auth logout page

* feat: skip login if already logged in
This commit is contained in:
Jason Rasmussen
2026-04-15 16:33:52 -04:00
committed by GitHub
parent ac06514db5
commit 4ffa26c969
10 changed files with 48 additions and 26 deletions

View File

@@ -14,12 +14,11 @@
import { fade } from 'svelte/transition';
import UserAvatar from '../user-avatar.svelte';
interface Props {
onLogout: () => void;
type Props = {
onClose?: () => void;
}
};
let { onLogout, onClose = () => {} }: Props = $props();
let { onClose }: Props = $props();
let info: ServerAboutResponseDto | undefined = $state();
@@ -48,7 +47,7 @@
size="tiny"
shape="round"
onclick={async () => {
onClose();
onClose?.();
await modalManager.show(AvatarEditModal);
}}
/>
@@ -99,7 +98,7 @@
<div class="mb-4 flex flex-col">
<Button
class="m-1 mx-4 rounded-none rounded-b-3xl bg-white p-3 dark:bg-immich-dark-primary/10"
onclick={onLogout}
href={Route.logout()}
leadingIcon={mdiLogout}
variant="ghost"
color="secondary">{$t('sign_out')}</Button
@@ -109,7 +108,7 @@
type="button"
class="text-center mt-4 underline text-xs text-primary"
onclick={async () => {
onClose();
onClose?.();
if (info) {
await modalManager.show(HelpAndFeedbackModal, { info });
}

View File

@@ -178,10 +178,7 @@
</button>
{#if shouldShowAccountInfoPanel}
<AccountInfoPanel
onLogout={() => authManager.logout()}
onClose={() => (shouldShowAccountInfoPanel = false)}
/>
<AccountInfoPanel onClose={() => (shouldShowAccountInfoPanel = false)} />
{/if}
</div>
</section>

View File

@@ -41,6 +41,12 @@ class AuthManager {
return this.#preferences;
}
constructor() {
eventManager.on({
SessionDelete: () => goto(Route.logout()),
});
}
async load() {
if (authManager.authenticated) {
return;
@@ -84,30 +90,26 @@ class AuthManager {
}
async logout() {
let redirectUri;
let redirectUri = Route.login();
try {
const response = await logout();
if (response.redirectUri) {
redirectUri = response.redirectUri;
}
} catch (error) {
console.log('Error logging out:', error);
} catch {
// noop
}
redirectUri = redirectUri ?? Route.login();
try {
if (redirectUri.startsWith('/')) {
await goto(redirectUri);
} else {
globalThis.location.href = redirectUri;
}
} finally {
if (redirectUri.startsWith('/')) {
this.isPurchased = false;
this.reset();
eventManager.emit('AuthLogout');
await goto(redirectUri);
} else {
globalThis.location.href = redirectUri;
}
}

View File

@@ -74,6 +74,7 @@ export type Events = {
UserAdminDeleted: [{ id: string }];
SessionLocked: [];
SessionDelete: [];
SystemConfigUpdate: [SystemConfigDto];

View File

@@ -51,6 +51,7 @@ export const Docs = {
export const Route = {
// auth
login: (params?: { continue?: string; autoLaunch?: 0 | 1 }) => '/auth/login' + asQueryString(params),
logout: (params?: { continue?: string }) => '/auth/logout' + asQueryString(params),
register: () => '/auth/register',
changePassword: () => '/auth/change-password',
onboarding: (params?: { step?: string }) => '/auth/onboarding' + asQueryString(params),

View File

@@ -78,7 +78,7 @@ websocket
}
})
.on('on_new_release', (event) => eventManager.emit('ReleaseEvent', event))
.on('on_session_delete', () => authManager.logout())
.on('on_session_delete', () => eventManager.emit('SessionDelete'))
.on('on_user_delete', (id) => eventManager.emit('UserAdminDeleted', { id }))
.on('on_asset_update', (asset) => eventManager.emit('AssetUpdate', asset))
.on('on_person_thumbnail', (id) => eventManager.emit('PersonThumbnailReady', { id }))

View File

@@ -1,3 +1,4 @@
import { authManager } from '$lib/managers/auth-manager.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import { initLanguage } from '$lib/utils';
@@ -13,6 +14,7 @@ async function _init(fetch: Fetch) {
defaults.fetch = fetch;
await initLanguage();
await serverConfigManager.init();
await authManager.load();
if (!serverConfigManager.value.maintenanceMode) {
await featureFlagsManager.init();

View File

@@ -1,6 +1,8 @@
<script lang="ts">
import { goto } from '$app/navigation';
import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { Route } from '$lib/route';
import { updateMyUser } from '@immich/sdk';
import { Alert, Button, Field, HelperText, PasswordInput, Stack, Text } from '@immich/ui';
import { t } from 'svelte-i18n';
@@ -23,7 +25,7 @@
}
await updateMyUser({ userUpdateMeDto: { password } });
await authManager.logout();
await goto(Route.logout());
};
</script>

View File

@@ -1,3 +1,4 @@
import { authManager } from '$lib/managers/auth-manager.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import { Route } from '$lib/route';
import { getFormatter } from '$lib/utils/i18n';
@@ -7,6 +8,11 @@ import type { PageLoad } from './$types';
export const load = (async ({ parent, url }) => {
await parent();
const continueUrl = url.searchParams.get('continue') || Route.photos();
if (authManager.authenticated) {
redirect(307, continueUrl);
}
if (!serverConfigManager.value.isInitialized) {
// Admin not registered
redirect(307, Route.register());
@@ -17,6 +23,6 @@ export const load = (async ({ parent, url }) => {
meta: {
title: $t('login'),
},
continueUrl: url.searchParams.get('continue') || Route.photos(),
continueUrl,
};
}) satisfies PageLoad;

View File

@@ -0,0 +1,12 @@
<script lang="ts">
import { authManager } from '$lib/managers/auth-manager.svelte';
import { LoadingSpinner } from '@immich/ui';
void authManager.logout();
</script>
<div class="h-screen w-screen overflow-hidden">
<div class="m-auto">
<LoadingSpinner />
</div>
</div>