feat(web): reset pin code (#20766)

This commit is contained in:
Jason Rasmussen 2025-08-07 15:07:31 -04:00 committed by GitHub
parent 1d4d8e7a9a
commit cfbc24579d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 112 additions and 18 deletions

View File

@ -912,6 +912,7 @@
"failed_to_load_notifications": "Failed to load notifications",
"failed_to_load_people": "Failed to load people",
"failed_to_remove_product_key": "Failed to remove product key",
"failed_to_reset_pin_code": "Failed to reset PIN code",
"failed_to_stack_assets": "Failed to stack assets",
"failed_to_unstack_assets": "Failed to un-stack assets",
"failed_to_update_notification_status": "Failed to update notification status",
@ -1056,6 +1057,7 @@
"folder_not_found": "Folder not found",
"folders": "Folders",
"folders_feature_description": "Browsing the folder view for the photos and videos on the file system",
"forgot_pin_code_question": "Forgot your PIN?",
"forward": "Forward",
"gcast_enabled": "Google Cast",
"gcast_enabled_description": "This feature loads external resources from Google in order to work.",
@ -1599,6 +1601,9 @@
"reset_password": "Reset password",
"reset_people_visibility": "Reset people visibility",
"reset_pin_code": "Reset PIN code",
"reset_pin_code_description": "If you forgot your PIN code, you can contact the server administrator to reset it",
"reset_pin_code_success": "Successfully reset PIN code",
"reset_pin_code_with_password": "You can always reset your PIN code with your password",
"reset_sqlite": "Reset SQLite Database",
"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",
"reset_sqlite_success": "Successfully reset the SQLite database",

View File

@ -6,7 +6,7 @@
import PinCodeInput from '$lib/components/user-settings-page/PinCodeInput.svelte';
import { handleError } from '$lib/utils/handle-error';
import { changePinCode } from '@immich/sdk';
import { Button } from '@immich/ui';
import { Button, Heading, Text } from '@immich/ui';
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
@ -16,11 +16,11 @@
let isLoading = $state(false);
let canSubmit = $derived(currentPinCode.length === 6 && confirmPinCode.length === 6 && newPinCode === confirmPinCode);
interface Props {
onChanged?: () => void;
}
type Props = {
onForgot: () => void;
};
let { onChanged }: Props = $props();
let { onForgot }: Props = $props();
const handleSubmit = async (event: Event) => {
event.preventDefault();
@ -38,8 +38,6 @@
message: $t('pin_code_changed_successfully'),
type: NotificationType.Info,
});
onChanged?.();
} catch (error) {
handleError(error, $t('unable_to_change_pin_code'));
} finally {
@ -58,12 +56,13 @@
<div in:fade={{ duration: 200 }}>
<form autocomplete="off" onsubmit={handleSubmit} class="mt-6">
<div class="flex flex-col gap-6 place-items-center place-content-center">
<p class="text-dark">{$t('change_pin_code')}</p>
<Heading>{$t('change_pin_code')}</Heading>
<PinCodeInput label={$t('current_pin_code')} bind:value={currentPinCode} tabindexStart={1} pinLength={6} />
<PinCodeInput label={$t('new_pin_code')} bind:value={newPinCode} tabindexStart={7} pinLength={6} />
<PinCodeInput label={$t('confirm_new_pin_code')} bind:value={confirmPinCode} tabindexStart={13} pinLength={6} />
<button type="button" onclick={onForgot}>
<Text color="muted" class="underline" size="small">{$t('forgot_pin_code_question')}</Text>
</button>
</div>
<div class="flex justify-end gap-2 mt-4">

View File

@ -6,7 +6,7 @@
import PinCodeInput from '$lib/components/user-settings-page/PinCodeInput.svelte';
import { handleError } from '$lib/utils/handle-error';
import { setupPinCode } from '@immich/sdk';
import { Button } from '@immich/ui';
import { Button, Heading } from '@immich/ui';
import { t } from 'svelte-i18n';
interface Props {
@ -54,10 +54,9 @@
<form autocomplete="off" onsubmit={handleSubmit}>
<div class="flex flex-col gap-6 place-items-center place-content-center">
{#if showLabel}
<p class="text-dark">{$t('setup_pin_code')}</p>
<Heading>{$t('setup_pin_code')}</Heading>
{/if}
<PinCodeInput label={$t('new_pin_code')} bind:value={newPinCode} tabindexStart={1} pinLength={6} />
<PinCodeInput label={$t('confirm_new_pin_code')} bind:value={confirmPinCode} tabindexStart={7} pinLength={6} />
</div>

View File

@ -1,4 +1,5 @@
<script lang="ts">
import { Label } from '@immich/ui';
import { onMount } from 'svelte';
interface Props {
@ -115,7 +116,7 @@
<div class="flex flex-col gap-1">
{#if label}
<label class="text-xs text-dark" for={pinCodeInputElements[0]?.id}>{label.toUpperCase()}</label>
<Label for={pinCodeInputElements[0]?.id}>{label}</Label>
{/if}
<div class="flex gap-2">
{#each { length: pinLength } as _, index (index)}

View File

@ -1,7 +1,9 @@
<script lang="ts">
import PinCodeChangeForm from '$lib/components/user-settings-page/PinCodeChangeForm.svelte';
import PinCodeCreateForm from '$lib/components/user-settings-page/PinCodeCreateForm.svelte';
import PinCodeResetModal from '$lib/modals/PinCodeResetModal.svelte';
import { getAuthStatus } from '@immich/sdk';
import { modalManager } from '@immich/ui';
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
@ -11,15 +13,22 @@
const { pinCode } = await getAuthStatus();
hasPinCode = pinCode;
});
const handleResetPINCode = async () => {
const success = await modalManager.show(PinCodeResetModal, {});
if (success) {
hasPinCode = false;
}
};
</script>
<section class="my-4">
<section>
{#if hasPinCode}
<div in:fade={{ duration: 200 }} class="mt-6">
<PinCodeChangeForm />
<div in:fade={{ duration: 200 }}>
<PinCodeChangeForm onForgot={handleResetPINCode} />
</div>
{:else}
<div in:fade={{ duration: 200 }} class="mt-6">
<div in:fade={{ duration: 200 }}>
<PinCodeCreateForm onCreated={() => (hasPinCode = true)} />
</div>
{/if}

View File

@ -0,0 +1,81 @@
<script lang="ts">
import {
notificationController,
NotificationType,
} from '$lib/components/shared-components/notification/notification';
import { featureFlags } from '$lib/stores/server-config.store';
import { handleError } from '$lib/utils/handle-error';
import { resetPinCode } from '@immich/sdk';
import {
Button,
Field,
HelperText,
HStack,
Modal,
ModalBody,
ModalFooter,
PasswordInput,
Stack,
Text,
} from '@immich/ui';
import { mdiLockReset } from '@mdi/js';
import { t } from 'svelte-i18n';
type Props = {
onClose: (success?: true) => void;
};
let { onClose }: Props = $props();
let passwordLoginEnabled = $derived($featureFlags.passwordLogin);
let password = $state('');
const handleReset = async () => {
try {
await resetPinCode({ pinCodeResetDto: { password } });
notificationController.show({ message: $t('pin_code_reset_successfully'), type: NotificationType.Info });
onClose(true);
} catch (error) {
handleError(error, $t('errors.failed_to_reset_pin_code'));
}
};
const onsubmit = async (event: Event) => {
event.preventDefault();
await handleReset();
};
</script>
<Modal title={$t('reset_pin_code')} icon={mdiLockReset} size="small" {onClose}>
<ModalBody>
<form {onsubmit} autocomplete="off" id="reset-pin-form">
<Stack gap={4}>
<div>{$t('reset_pin_code_description')}</div>
{#if passwordLoginEnabled}
<hr class="my-2 h-px w-full border-0 bg-gray-200 dark:bg-gray-600" />
<section>
<Field label={$t('confirm_password')} required>
<PasswordInput bind:value={password} autocomplete="current-password" />
<HelperText>
<Text color="muted">{$t('reset_pin_code_with_password')}</Text>
</HelperText>
</Field>
</section>
{/if}
</Stack>
</form>
</ModalBody>
<ModalFooter>
{#if passwordLoginEnabled}
<HStack fullWidth>
<Button fullWidth shape="round" color="secondary" onclick={() => onClose()}>{$t('cancel')}</Button>
<Button type="submit" form="reset-pin-form" fullWidth shape="round" color="danger" disabled={!password}>
{$t('reset')}
</Button>
</HStack>
{:else}
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{$t('close')}</Button>
{/if}
</ModalFooter>
</Modal>