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
@@ -14,12 +14,11 @@
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import UserAvatar from '../user-avatar.svelte'; import UserAvatar from '../user-avatar.svelte';
interface Props { type Props = {
onLogout: () => void;
onClose?: () => void; onClose?: () => void;
} };
let { onLogout, onClose = () => {} }: Props = $props(); let { onClose }: Props = $props();
let info: ServerAboutResponseDto | undefined = $state(); let info: ServerAboutResponseDto | undefined = $state();
@@ -48,7 +47,7 @@
size="tiny" size="tiny"
shape="round" shape="round"
onclick={async () => { onclick={async () => {
onClose(); onClose?.();
await modalManager.show(AvatarEditModal); await modalManager.show(AvatarEditModal);
}} }}
/> />
@@ -99,7 +98,7 @@
<div class="mb-4 flex flex-col"> <div class="mb-4 flex flex-col">
<Button <Button
class="m-1 mx-4 rounded-none rounded-b-3xl bg-white p-3 dark:bg-immich-dark-primary/10" 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} leadingIcon={mdiLogout}
variant="ghost" variant="ghost"
color="secondary">{$t('sign_out')}</Button color="secondary">{$t('sign_out')}</Button
@@ -109,7 +108,7 @@
type="button" type="button"
class="text-center mt-4 underline text-xs text-primary" class="text-center mt-4 underline text-xs text-primary"
onclick={async () => { onclick={async () => {
onClose(); onClose?.();
if (info) { if (info) {
await modalManager.show(HelpAndFeedbackModal, { info }); await modalManager.show(HelpAndFeedbackModal, { info });
} }
@@ -178,10 +178,7 @@
</button> </button>
{#if shouldShowAccountInfoPanel} {#if shouldShowAccountInfoPanel}
<AccountInfoPanel <AccountInfoPanel onClose={() => (shouldShowAccountInfoPanel = false)} />
onLogout={() => authManager.logout()}
onClose={() => (shouldShowAccountInfoPanel = false)}
/>
{/if} {/if}
</div> </div>
</section> </section>
+14 -12
View File
@@ -41,6 +41,12 @@ class AuthManager {
return this.#preferences; return this.#preferences;
} }
constructor() {
eventManager.on({
SessionDelete: () => goto(Route.logout()),
});
}
async load() { async load() {
if (authManager.authenticated) { if (authManager.authenticated) {
return; return;
@@ -84,30 +90,26 @@ class AuthManager {
} }
async logout() { async logout() {
let redirectUri; let redirectUri = Route.login();
try { try {
const response = await logout(); const response = await logout();
if (response.redirectUri) { if (response.redirectUri) {
redirectUri = response.redirectUri; redirectUri = response.redirectUri;
} }
} catch (error) { } catch {
console.log('Error logging out:', error); // noop
} }
redirectUri = redirectUri ?? Route.login(); if (redirectUri.startsWith('/')) {
try {
if (redirectUri.startsWith('/')) {
await goto(redirectUri);
} else {
globalThis.location.href = redirectUri;
}
} finally {
this.isPurchased = false; this.isPurchased = false;
this.reset(); this.reset();
eventManager.emit('AuthLogout'); eventManager.emit('AuthLogout');
await goto(redirectUri);
} else {
globalThis.location.href = redirectUri;
} }
} }
@@ -74,6 +74,7 @@ export type Events = {
UserAdminDeleted: [{ id: string }]; UserAdminDeleted: [{ id: string }];
SessionLocked: []; SessionLocked: [];
SessionDelete: [];
SystemConfigUpdate: [SystemConfigDto]; SystemConfigUpdate: [SystemConfigDto];
+1
View File
@@ -51,6 +51,7 @@ export const Docs = {
export const Route = { export const Route = {
// auth // auth
login: (params?: { continue?: string; autoLaunch?: 0 | 1 }) => '/auth/login' + asQueryString(params), login: (params?: { continue?: string; autoLaunch?: 0 | 1 }) => '/auth/login' + asQueryString(params),
logout: (params?: { continue?: string }) => '/auth/logout' + asQueryString(params),
register: () => '/auth/register', register: () => '/auth/register',
changePassword: () => '/auth/change-password', changePassword: () => '/auth/change-password',
onboarding: (params?: { step?: string }) => '/auth/onboarding' + asQueryString(params), onboarding: (params?: { step?: string }) => '/auth/onboarding' + asQueryString(params),
+1 -1
View File
@@ -78,7 +78,7 @@ websocket
} }
}) })
.on('on_new_release', (event) => eventManager.emit('ReleaseEvent', event)) .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_user_delete', (id) => eventManager.emit('UserAdminDeleted', { id }))
.on('on_asset_update', (asset) => eventManager.emit('AssetUpdate', asset)) .on('on_asset_update', (asset) => eventManager.emit('AssetUpdate', asset))
.on('on_person_thumbnail', (id) => eventManager.emit('PersonThumbnailReady', { id })) .on('on_person_thumbnail', (id) => eventManager.emit('PersonThumbnailReady', { id }))
+2
View File
@@ -1,3 +1,4 @@
import { authManager } from '$lib/managers/auth-manager.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte'; import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte'; import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import { initLanguage } from '$lib/utils'; import { initLanguage } from '$lib/utils';
@@ -13,6 +14,7 @@ async function _init(fetch: Fetch) {
defaults.fetch = fetch; defaults.fetch = fetch;
await initLanguage(); await initLanguage();
await serverConfigManager.init(); await serverConfigManager.init();
await authManager.load();
if (!serverConfigManager.value.maintenanceMode) { if (!serverConfigManager.value.maintenanceMode) {
await featureFlagsManager.init(); await featureFlagsManager.init();
@@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation';
import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte'; import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
import { Route } from '$lib/route';
import { updateMyUser } from '@immich/sdk'; import { updateMyUser } from '@immich/sdk';
import { Alert, Button, Field, HelperText, PasswordInput, Stack, Text } from '@immich/ui'; import { Alert, Button, Field, HelperText, PasswordInput, Stack, Text } from '@immich/ui';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@@ -23,7 +25,7 @@
} }
await updateMyUser({ userUpdateMeDto: { password } }); await updateMyUser({ userUpdateMeDto: { password } });
await authManager.logout(); await goto(Route.logout());
}; };
</script> </script>
+7 -1
View File
@@ -1,3 +1,4 @@
import { authManager } from '$lib/managers/auth-manager.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte'; import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import { Route } from '$lib/route'; import { Route } from '$lib/route';
import { getFormatter } from '$lib/utils/i18n'; import { getFormatter } from '$lib/utils/i18n';
@@ -7,6 +8,11 @@ import type { PageLoad } from './$types';
export const load = (async ({ parent, url }) => { export const load = (async ({ parent, url }) => {
await parent(); await parent();
const continueUrl = url.searchParams.get('continue') || Route.photos();
if (authManager.authenticated) {
redirect(307, continueUrl);
}
if (!serverConfigManager.value.isInitialized) { if (!serverConfigManager.value.isInitialized) {
// Admin not registered // Admin not registered
redirect(307, Route.register()); redirect(307, Route.register());
@@ -17,6 +23,6 @@ export const load = (async ({ parent, url }) => {
meta: { meta: {
title: $t('login'), title: $t('login'),
}, },
continueUrl: url.searchParams.get('continue') || Route.photos(), continueUrl,
}; };
}) satisfies PageLoad; }) satisfies PageLoad;
+12
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>