mirror of
https://github.com/immich-app/immich.git
synced 2025-08-11 09:16:31 -04:00
feat(web): reset pin code (#20766)
This commit is contained in:
parent
1d4d8e7a9a
commit
cfbc24579d
@ -912,6 +912,7 @@
|
|||||||
"failed_to_load_notifications": "Failed to load notifications",
|
"failed_to_load_notifications": "Failed to load notifications",
|
||||||
"failed_to_load_people": "Failed to load people",
|
"failed_to_load_people": "Failed to load people",
|
||||||
"failed_to_remove_product_key": "Failed to remove product key",
|
"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_stack_assets": "Failed to stack assets",
|
||||||
"failed_to_unstack_assets": "Failed to un-stack assets",
|
"failed_to_unstack_assets": "Failed to un-stack assets",
|
||||||
"failed_to_update_notification_status": "Failed to update notification status",
|
"failed_to_update_notification_status": "Failed to update notification status",
|
||||||
@ -1056,6 +1057,7 @@
|
|||||||
"folder_not_found": "Folder not found",
|
"folder_not_found": "Folder not found",
|
||||||
"folders": "Folders",
|
"folders": "Folders",
|
||||||
"folders_feature_description": "Browsing the folder view for the photos and videos on the file system",
|
"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",
|
"forward": "Forward",
|
||||||
"gcast_enabled": "Google Cast",
|
"gcast_enabled": "Google Cast",
|
||||||
"gcast_enabled_description": "This feature loads external resources from Google in order to work.",
|
"gcast_enabled_description": "This feature loads external resources from Google in order to work.",
|
||||||
@ -1599,6 +1601,9 @@
|
|||||||
"reset_password": "Reset password",
|
"reset_password": "Reset password",
|
||||||
"reset_people_visibility": "Reset people visibility",
|
"reset_people_visibility": "Reset people visibility",
|
||||||
"reset_pin_code": "Reset PIN code",
|
"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": "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_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",
|
"reset_sqlite_success": "Successfully reset the SQLite database",
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
import PinCodeInput from '$lib/components/user-settings-page/PinCodeInput.svelte';
|
import PinCodeInput from '$lib/components/user-settings-page/PinCodeInput.svelte';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { changePinCode } from '@immich/sdk';
|
import { changePinCode } from '@immich/sdk';
|
||||||
import { Button } from '@immich/ui';
|
import { Button, Heading, Text } from '@immich/ui';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
@ -16,11 +16,11 @@
|
|||||||
let isLoading = $state(false);
|
let isLoading = $state(false);
|
||||||
let canSubmit = $derived(currentPinCode.length === 6 && confirmPinCode.length === 6 && newPinCode === confirmPinCode);
|
let canSubmit = $derived(currentPinCode.length === 6 && confirmPinCode.length === 6 && newPinCode === confirmPinCode);
|
||||||
|
|
||||||
interface Props {
|
type Props = {
|
||||||
onChanged?: () => void;
|
onForgot: () => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
let { onChanged }: Props = $props();
|
let { onForgot }: Props = $props();
|
||||||
|
|
||||||
const handleSubmit = async (event: Event) => {
|
const handleSubmit = async (event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -38,8 +38,6 @@
|
|||||||
message: $t('pin_code_changed_successfully'),
|
message: $t('pin_code_changed_successfully'),
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
|
|
||||||
onChanged?.();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, $t('unable_to_change_pin_code'));
|
handleError(error, $t('unable_to_change_pin_code'));
|
||||||
} finally {
|
} finally {
|
||||||
@ -58,12 +56,13 @@
|
|||||||
<div in:fade={{ duration: 200 }}>
|
<div in:fade={{ duration: 200 }}>
|
||||||
<form autocomplete="off" onsubmit={handleSubmit} class="mt-6">
|
<form autocomplete="off" onsubmit={handleSubmit} class="mt-6">
|
||||||
<div class="flex flex-col gap-6 place-items-center place-content-center">
|
<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('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('new_pin_code')} bind:value={newPinCode} tabindexStart={7} pinLength={6} />
|
||||||
|
|
||||||
<PinCodeInput label={$t('confirm_new_pin_code')} bind:value={confirmPinCode} tabindexStart={13} 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>
|
||||||
|
|
||||||
<div class="flex justify-end gap-2 mt-4">
|
<div class="flex justify-end gap-2 mt-4">
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
import PinCodeInput from '$lib/components/user-settings-page/PinCodeInput.svelte';
|
import PinCodeInput from '$lib/components/user-settings-page/PinCodeInput.svelte';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { setupPinCode } from '@immich/sdk';
|
import { setupPinCode } from '@immich/sdk';
|
||||||
import { Button } from '@immich/ui';
|
import { Button, Heading } from '@immich/ui';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -54,10 +54,9 @@
|
|||||||
<form autocomplete="off" onsubmit={handleSubmit}>
|
<form autocomplete="off" onsubmit={handleSubmit}>
|
||||||
<div class="flex flex-col gap-6 place-items-center place-content-center">
|
<div class="flex flex-col gap-6 place-items-center place-content-center">
|
||||||
{#if showLabel}
|
{#if showLabel}
|
||||||
<p class="text-dark">{$t('setup_pin_code')}</p>
|
<Heading>{$t('setup_pin_code')}</Heading>
|
||||||
{/if}
|
{/if}
|
||||||
<PinCodeInput label={$t('new_pin_code')} bind:value={newPinCode} tabindexStart={1} pinLength={6} />
|
<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} />
|
<PinCodeInput label={$t('confirm_new_pin_code')} bind:value={confirmPinCode} tabindexStart={7} pinLength={6} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { Label } from '@immich/ui';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -115,7 +116,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
{#if label}
|
{#if label}
|
||||||
<label class="text-xs text-dark" for={pinCodeInputElements[0]?.id}>{label.toUpperCase()}</label>
|
<Label for={pinCodeInputElements[0]?.id}>{label}</Label>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
{#each { length: pinLength } as _, index (index)}
|
{#each { length: pinLength } as _, index (index)}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import PinCodeChangeForm from '$lib/components/user-settings-page/PinCodeChangeForm.svelte';
|
import PinCodeChangeForm from '$lib/components/user-settings-page/PinCodeChangeForm.svelte';
|
||||||
import PinCodeCreateForm from '$lib/components/user-settings-page/PinCodeCreateForm.svelte';
|
import PinCodeCreateForm from '$lib/components/user-settings-page/PinCodeCreateForm.svelte';
|
||||||
|
import PinCodeResetModal from '$lib/modals/PinCodeResetModal.svelte';
|
||||||
import { getAuthStatus } from '@immich/sdk';
|
import { getAuthStatus } from '@immich/sdk';
|
||||||
|
import { modalManager } from '@immich/ui';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
@ -11,15 +13,22 @@
|
|||||||
const { pinCode } = await getAuthStatus();
|
const { pinCode } = await getAuthStatus();
|
||||||
hasPinCode = pinCode;
|
hasPinCode = pinCode;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleResetPINCode = async () => {
|
||||||
|
const success = await modalManager.show(PinCodeResetModal, {});
|
||||||
|
if (success) {
|
||||||
|
hasPinCode = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="my-4">
|
<section>
|
||||||
{#if hasPinCode}
|
{#if hasPinCode}
|
||||||
<div in:fade={{ duration: 200 }} class="mt-6">
|
<div in:fade={{ duration: 200 }}>
|
||||||
<PinCodeChangeForm />
|
<PinCodeChangeForm onForgot={handleResetPINCode} />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div in:fade={{ duration: 200 }} class="mt-6">
|
<div in:fade={{ duration: 200 }}>
|
||||||
<PinCodeCreateForm onCreated={() => (hasPinCode = true)} />
|
<PinCodeCreateForm onCreated={() => (hasPinCode = true)} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
81
web/src/lib/modals/PinCodeResetModal.svelte
Normal file
81
web/src/lib/modals/PinCodeResetModal.svelte
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user