Skip to content

Commit 394810e

Browse files
committed
feat: Refactor user settings page to support OAuth features
- Added conditional rendering for user profile and password settings based on OAuth feature flag. - Introduced new components for OAuth settings and password change functionality. - Implemented logic to open external URLs for password and authenticator setup in new tabs. - Updated user profile settings to allow editing of user information with error handling and success notifications.
1 parent 65ea376 commit 394810e

4 files changed

Lines changed: 169 additions & 14 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<script lang="ts">
2+
import { Button } from '@immich/ui';
3+
import { t } from 'svelte-i18n';
4+
import { fade } from 'svelte/transition';
5+
6+
const PASSWORD_CHANGE_URL = 'https://login.pixelunion.eu/if/flow/default-password-change/';
7+
8+
const openPasswordChange = () => {
9+
// Open in a new tab with noopener for security, adding redirect back
10+
try {
11+
const current = globalThis.location;
12+
const next = encodeURIComponent(current.origin + current.pathname + current.search + current.hash);
13+
const joinChar = PASSWORD_CHANGE_URL.includes('?') ? '&' : '?';
14+
const finalUrl = `${PASSWORD_CHANGE_URL}${joinChar}next=${next}`;
15+
globalThis.open(finalUrl, '_blank', 'noopener');
16+
} catch {
17+
globalThis.open(PASSWORD_CHANGE_URL, '_blank', 'noopener');
18+
}
19+
};
20+
</script>
21+
22+
<section class="my-4">
23+
<div in:fade={{ duration: 500 }}>
24+
<div class="flex">
25+
<Button shape="round" size="small" onclick={openPasswordChange}>{$t('change_password')}</Button>
26+
</div>
27+
</div>
28+
</section>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<script lang="ts">
2+
import { Button } from '@immich/ui';
3+
import { fade } from 'svelte/transition';
4+
5+
const TOTP_SETUP_URL = 'https://login.pixelunion.eu/if/flow/default-authenticator-totp-setup/';
6+
const WEBAUTHN_SETUP_URL = 'https://login.pixelunion.eu/if/flow/default-authenticator-webauthn-setup/';
7+
8+
const openInNewTab = (baseUrl: string) => {
9+
try {
10+
const current = globalThis.location;
11+
const next = encodeURIComponent(current.origin + current.pathname + current.search + current.hash);
12+
const joinChar = baseUrl.includes('?') ? '&' : '?';
13+
const finalUrl = `${baseUrl}${joinChar}next=${next}`;
14+
globalThis.open(finalUrl, '_blank', 'noopener');
15+
} catch {
16+
// fallback without next param if anything unexpected happens
17+
globalThis.open(baseUrl, '_blank', 'noopener');
18+
}
19+
};
20+
</script>
21+
22+
<section class="my-4">
23+
<div class="flex flex-col gap-4" in:fade={{ duration: 500 }}>
24+
<div class="flex">
25+
<Button shape="round" size="small" onclick={() => openInNewTab(TOTP_SETUP_URL)}>
26+
Set up authenticator app (TOTP)
27+
</Button>
28+
</div>
29+
<div class="flex">
30+
<Button shape="round" size="small" onclick={() => openInNewTab(WEBAUTHN_SETUP_URL)}>
31+
Set up passkey (WebAuthn)
32+
</Button>
33+
</div>
34+
</div>
35+
</section>
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<script lang="ts">
2+
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
3+
import { SettingInputFieldType } from '$lib/constants';
4+
import { user } from '$lib/stores/user.store';
5+
import { handleError } from '$lib/utils/handle-error';
6+
import { updateMyUser } from '@immich/sdk';
7+
import { Button, toastManager } from '@immich/ui';
8+
import { cloneDeep } from 'lodash-es';
9+
import { t } from 'svelte-i18n';
10+
import { createBubbler, preventDefault } from 'svelte/legacy';
11+
import { fade } from 'svelte/transition';
12+
13+
let editedUser = $state(cloneDeep($user));
14+
const bubble = createBubbler();
15+
16+
const handleSaveProfile = async () => {
17+
try {
18+
const data = await updateMyUser({
19+
userUpdateMeDto: {
20+
email: editedUser.email,
21+
name: editedUser.name,
22+
},
23+
});
24+
25+
Object.assign(editedUser, data);
26+
$user = data;
27+
28+
toastManager.success($t('saved_profile'));
29+
} catch (error) {
30+
handleError(error, $t('errors.unable_to_save_profile'));
31+
}
32+
};
33+
</script>
34+
35+
<section class="my-4">
36+
<div in:fade={{ duration: 500 }}>
37+
<form autocomplete="off" onsubmit={preventDefault(bubble('submit'))}>
38+
<div class="ms-4 mt-4 flex flex-col gap-4">
39+
<SettingInputField
40+
inputType={SettingInputFieldType.TEXT}
41+
label={$t('user_id')}
42+
bind:value={editedUser.id}
43+
disabled={true}
44+
/>
45+
46+
<SettingInputField
47+
inputType={SettingInputFieldType.EMAIL}
48+
label={$t('email')}
49+
bind:value={editedUser.email}
50+
disabled={true}
51+
/>
52+
53+
<SettingInputField
54+
inputType={SettingInputFieldType.TEXT}
55+
label={$t('name')}
56+
bind:value={editedUser.name}
57+
required={true}
58+
/>
59+
60+
<SettingInputField
61+
inputType={SettingInputFieldType.TEXT}
62+
label={$t('storage_label')}
63+
disabled={true}
64+
value={editedUser.storageLabel || ''}
65+
required={false}
66+
/>
67+
68+
<div class="flex justify-end">
69+
<Button shape="round" type="submit" size="small" onclick={() => handleSaveProfile()}>{$t('save')}</Button>
70+
</div>
71+
</div>
72+
</form>
73+
</div>
74+
</section>

web/src/lib/components/user-settings-page/user-settings-list.svelte

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
mdiDevices,
2020
mdiDownload,
2121
mdiFeatureSearchOutline,
22-
mdiLockSmart,
2322
mdiFormTextboxPassword,
23+
mdiLockSmart,
2424
mdiServerOutline,
2525
mdiTwoFactorAuthentication,
2626
} from '@mdi/js';
@@ -30,8 +30,10 @@
3030
import AppSettings from './app-settings.svelte';
3131
import ChangePasswordSettings from './change-password-settings.svelte';
3232
import DeviceList from './device-list.svelte';
33-
import OAuthSettings from './oauth-settings.svelte';
3433
import PartnerSettings from './partner-settings.svelte';
34+
import PuChangePasswordSettingsOauth from './pu-change-password-settings-oauth.svelte';
35+
import PuOAuthSettings from './pu-oauth-settings.svelte';
36+
import PuUserProfileSettings from './pu-user-profile-settings.svelte';
3537
import UserAPIKeyList from './user-api-key-list.svelte';
3638
import UserProfileSettings from './user-profile-settings.svelte';
3739
@@ -57,9 +59,15 @@
5759
<AppSettings />
5860
</SettingAccordion>
5961

60-
<SettingAccordion icon={mdiAccountOutline} key="account" title={$t('account')} subtitle={$t('manage_your_account')}>
61-
<UserProfileSettings />
62-
</SettingAccordion>
62+
{#if $featureFlags.loaded && !$featureFlags.oauth}
63+
<SettingAccordion icon={mdiAccountOutline} key="account" title={$t('account')} subtitle={$t('manage_your_account')}>
64+
<UserProfileSettings />
65+
</SettingAccordion>
66+
{:else}
67+
<SettingAccordion icon={mdiAccountOutline} key="account" title={$t('account')} subtitle={$t('manage_your_account')}>
68+
<PuUserProfileSettings />
69+
</SettingAccordion>
70+
{/if}
6371

6472
<SettingAccordion
6573
icon={mdiServerOutline}
@@ -118,18 +126,28 @@
118126
subtitle={$t('manage_your_oauth_connection')}
119127
isOpen={oauthOpen || undefined}
120128
>
121-
<OAuthSettings user={$user} />
129+
<PuOAuthSettings user={$user} />
130+
</SettingAccordion>
131+
<SettingAccordion
132+
icon={mdiFormTextboxPassword}
133+
key="password"
134+
title={$t('password')}
135+
subtitle={$t('change_your_password')}
136+
>
137+
<PuChangePasswordSettingsOauth />
122138
</SettingAccordion>
123139
{/if}
124140

125-
<SettingAccordion
126-
icon={mdiFormTextboxPassword}
127-
key="password"
128-
title={$t('password')}
129-
subtitle={$t('change_your_password')}
130-
>
131-
<ChangePasswordSettings />
132-
</SettingAccordion>
141+
{#if $featureFlags.loaded && !$featureFlags.oauth}
142+
<SettingAccordion
143+
icon={mdiFormTextboxPassword}
144+
key="password"
145+
title={$t('password')}
146+
subtitle={$t('change_your_password')}
147+
>
148+
<ChangePasswordSettings />
149+
</SettingAccordion>
150+
{/if}
133151

134152
<SettingAccordion
135153
icon={mdiAccountGroupOutline}

0 commit comments

Comments
 (0)