Skip to content

Commit cfbc245

Browse files
authored
feat(web): reset pin code (#20766)
1 parent 1d4d8e7 commit cfbc245

File tree

6 files changed

+112
-18
lines changed

6 files changed

+112
-18
lines changed

i18n/en.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -912,6 +912,7 @@
912912
"failed_to_load_notifications": "Failed to load notifications",
913913
"failed_to_load_people": "Failed to load people",
914914
"failed_to_remove_product_key": "Failed to remove product key",
915+
"failed_to_reset_pin_code": "Failed to reset PIN code",
915916
"failed_to_stack_assets": "Failed to stack assets",
916917
"failed_to_unstack_assets": "Failed to un-stack assets",
917918
"failed_to_update_notification_status": "Failed to update notification status",
@@ -1056,6 +1057,7 @@
10561057
"folder_not_found": "Folder not found",
10571058
"folders": "Folders",
10581059
"folders_feature_description": "Browsing the folder view for the photos and videos on the file system",
1060+
"forgot_pin_code_question": "Forgot your PIN?",
10591061
"forward": "Forward",
10601062
"gcast_enabled": "Google Cast",
10611063
"gcast_enabled_description": "This feature loads external resources from Google in order to work.",
@@ -1599,6 +1601,9 @@
15991601
"reset_password": "Reset password",
16001602
"reset_people_visibility": "Reset people visibility",
16011603
"reset_pin_code": "Reset PIN code",
1604+
"reset_pin_code_description": "If you forgot your PIN code, you can contact the server administrator to reset it",
1605+
"reset_pin_code_success": "Successfully reset PIN code",
1606+
"reset_pin_code_with_password": "You can always reset your PIN code with your password",
16021607
"reset_sqlite": "Reset SQLite Database",
16031608
"reset_sqlite_confirmation": "Are you sure you want to reset the SQLite database? You will need to log out and log in again to resync the data",
16041609
"reset_sqlite_success": "Successfully reset the SQLite database",

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

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import PinCodeInput from '$lib/components/user-settings-page/PinCodeInput.svelte';
77
import { handleError } from '$lib/utils/handle-error';
88
import { changePinCode } from '@immich/sdk';
9-
import { Button } from '@immich/ui';
9+
import { Button, Heading, Text } from '@immich/ui';
1010
import { t } from 'svelte-i18n';
1111
import { fade } from 'svelte/transition';
1212
@@ -16,11 +16,11 @@
1616
let isLoading = $state(false);
1717
let canSubmit = $derived(currentPinCode.length === 6 && confirmPinCode.length === 6 && newPinCode === confirmPinCode);
1818
19-
interface Props {
20-
onChanged?: () => void;
21-
}
19+
type Props = {
20+
onForgot: () => void;
21+
};
2222
23-
let { onChanged }: Props = $props();
23+
let { onForgot }: Props = $props();
2424
2525
const handleSubmit = async (event: Event) => {
2626
event.preventDefault();
@@ -38,8 +38,6 @@
3838
message: $t('pin_code_changed_successfully'),
3939
type: NotificationType.Info,
4040
});
41-
42-
onChanged?.();
4341
} catch (error) {
4442
handleError(error, $t('unable_to_change_pin_code'));
4543
} finally {
@@ -58,12 +56,13 @@
5856
<div in:fade={{ duration: 200 }}>
5957
<form autocomplete="off" onsubmit={handleSubmit} class="mt-6">
6058
<div class="flex flex-col gap-6 place-items-center place-content-center">
61-
<p class="text-dark">{$t('change_pin_code')}</p>
59+
<Heading>{$t('change_pin_code')}</Heading>
6260
<PinCodeInput label={$t('current_pin_code')} bind:value={currentPinCode} tabindexStart={1} pinLength={6} />
63-
6461
<PinCodeInput label={$t('new_pin_code')} bind:value={newPinCode} tabindexStart={7} pinLength={6} />
65-
6662
<PinCodeInput label={$t('confirm_new_pin_code')} bind:value={confirmPinCode} tabindexStart={13} pinLength={6} />
63+
<button type="button" onclick={onForgot}>
64+
<Text color="muted" class="underline" size="small">{$t('forgot_pin_code_question')}</Text>
65+
</button>
6766
</div>
6867

6968
<div class="flex justify-end gap-2 mt-4">

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import PinCodeInput from '$lib/components/user-settings-page/PinCodeInput.svelte';
77
import { handleError } from '$lib/utils/handle-error';
88
import { setupPinCode } from '@immich/sdk';
9-
import { Button } from '@immich/ui';
9+
import { Button, Heading } from '@immich/ui';
1010
import { t } from 'svelte-i18n';
1111
1212
interface Props {
@@ -54,10 +54,9 @@
5454
<form autocomplete="off" onsubmit={handleSubmit}>
5555
<div class="flex flex-col gap-6 place-items-center place-content-center">
5656
{#if showLabel}
57-
<p class="text-dark">{$t('setup_pin_code')}</p>
57+
<Heading>{$t('setup_pin_code')}</Heading>
5858
{/if}
5959
<PinCodeInput label={$t('new_pin_code')} bind:value={newPinCode} tabindexStart={1} pinLength={6} />
60-
6160
<PinCodeInput label={$t('confirm_new_pin_code')} bind:value={confirmPinCode} tabindexStart={7} pinLength={6} />
6261
</div>
6362

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script lang="ts">
2+
import { Label } from '@immich/ui';
23
import { onMount } from 'svelte';
34
45
interface Props {
@@ -115,7 +116,7 @@
115116

116117
<div class="flex flex-col gap-1">
117118
{#if label}
118-
<label class="text-xs text-dark" for={pinCodeInputElements[0]?.id}>{label.toUpperCase()}</label>
119+
<Label for={pinCodeInputElements[0]?.id}>{label}</Label>
119120
{/if}
120121
<div class="flex gap-2">
121122
{#each { length: pinLength } as _, index (index)}

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
<script lang="ts">
22
import PinCodeChangeForm from '$lib/components/user-settings-page/PinCodeChangeForm.svelte';
33
import PinCodeCreateForm from '$lib/components/user-settings-page/PinCodeCreateForm.svelte';
4+
import PinCodeResetModal from '$lib/modals/PinCodeResetModal.svelte';
45
import { getAuthStatus } from '@immich/sdk';
6+
import { modalManager } from '@immich/ui';
57
import { onMount } from 'svelte';
68
import { fade } from 'svelte/transition';
79
@@ -11,15 +13,22 @@
1113
const { pinCode } = await getAuthStatus();
1214
hasPinCode = pinCode;
1315
});
16+
17+
const handleResetPINCode = async () => {
18+
const success = await modalManager.show(PinCodeResetModal, {});
19+
if (success) {
20+
hasPinCode = false;
21+
}
22+
};
1423
</script>
1524

16-
<section class="my-4">
25+
<section>
1726
{#if hasPinCode}
18-
<div in:fade={{ duration: 200 }} class="mt-6">
19-
<PinCodeChangeForm />
27+
<div in:fade={{ duration: 200 }}>
28+
<PinCodeChangeForm onForgot={handleResetPINCode} />
2029
</div>
2130
{:else}
22-
<div in:fade={{ duration: 200 }} class="mt-6">
31+
<div in:fade={{ duration: 200 }}>
2332
<PinCodeCreateForm onCreated={() => (hasPinCode = true)} />
2433
</div>
2534
{/if}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<script lang="ts">
2+
import {
3+
notificationController,
4+
NotificationType,
5+
} from '$lib/components/shared-components/notification/notification';
6+
import { featureFlags } from '$lib/stores/server-config.store';
7+
import { handleError } from '$lib/utils/handle-error';
8+
import { resetPinCode } from '@immich/sdk';
9+
import {
10+
Button,
11+
Field,
12+
HelperText,
13+
HStack,
14+
Modal,
15+
ModalBody,
16+
ModalFooter,
17+
PasswordInput,
18+
Stack,
19+
Text,
20+
} from '@immich/ui';
21+
import { mdiLockReset } from '@mdi/js';
22+
import { t } from 'svelte-i18n';
23+
24+
type Props = {
25+
onClose: (success?: true) => void;
26+
};
27+
28+
let { onClose }: Props = $props();
29+
30+
let passwordLoginEnabled = $derived($featureFlags.passwordLogin);
31+
let password = $state('');
32+
33+
const handleReset = async () => {
34+
try {
35+
await resetPinCode({ pinCodeResetDto: { password } });
36+
notificationController.show({ message: $t('pin_code_reset_successfully'), type: NotificationType.Info });
37+
onClose(true);
38+
} catch (error) {
39+
handleError(error, $t('errors.failed_to_reset_pin_code'));
40+
}
41+
};
42+
43+
const onsubmit = async (event: Event) => {
44+
event.preventDefault();
45+
await handleReset();
46+
};
47+
</script>
48+
49+
<Modal title={$t('reset_pin_code')} icon={mdiLockReset} size="small" {onClose}>
50+
<ModalBody>
51+
<form {onsubmit} autocomplete="off" id="reset-pin-form">
52+
<Stack gap={4}>
53+
<div>{$t('reset_pin_code_description')}</div>
54+
{#if passwordLoginEnabled}
55+
<hr class="my-2 h-px w-full border-0 bg-gray-200 dark:bg-gray-600" />
56+
<section>
57+
<Field label={$t('confirm_password')} required>
58+
<PasswordInput bind:value={password} autocomplete="current-password" />
59+
<HelperText>
60+
<Text color="muted">{$t('reset_pin_code_with_password')}</Text>
61+
</HelperText>
62+
</Field>
63+
</section>
64+
{/if}
65+
</Stack>
66+
</form>
67+
</ModalBody>
68+
69+
<ModalFooter>
70+
{#if passwordLoginEnabled}
71+
<HStack fullWidth>
72+
<Button fullWidth shape="round" color="secondary" onclick={() => onClose()}>{$t('cancel')}</Button>
73+
<Button type="submit" form="reset-pin-form" fullWidth shape="round" color="danger" disabled={!password}>
74+
{$t('reset')}
75+
</Button>
76+
</HStack>
77+
{:else}
78+
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{$t('close')}</Button>
79+
{/if}
80+
</ModalFooter>
81+
</Modal>

0 commit comments

Comments
 (0)