mirror of
https://github.com/immich-app/immich.git
synced 2025-01-07 20:36:48 +01:00
feat(web): randomize password on reest (#7943)
* feat(web): randomize password on reest * prettier * chore: clean up --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
parent
ab4b8eca15
commit
cda45f9bfb
3 changed files with 61 additions and 26 deletions
|
@ -8,8 +8,9 @@
|
||||||
export let color: Color = 'transparent-gray';
|
export let color: Color = 'transparent-gray';
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
export let fullwidth = false;
|
export let fullwidth = false;
|
||||||
|
export let title: string | undefined = undefined;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button size="link" {color} shadow={false} rounded="lg" {disabled} on:click {fullwidth}>
|
<Button {title} size="link" {color} shadow={false} rounded="lg" {disabled} on:click {fullwidth}>
|
||||||
<slot />
|
<slot />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
|
|
||||||
export let user: UserResponseDto;
|
export let user: UserResponseDto;
|
||||||
export let canResetPassword = true;
|
export let canResetPassword = true;
|
||||||
|
export let newPassword: string;
|
||||||
|
|
||||||
let error: string;
|
let error: string;
|
||||||
let success: string;
|
let success: string;
|
||||||
|
@ -53,12 +54,12 @@
|
||||||
|
|
||||||
const resetPassword = async () => {
|
const resetPassword = async () => {
|
||||||
try {
|
try {
|
||||||
const defaultPassword = 'password';
|
newPassword = generatePassword();
|
||||||
|
|
||||||
await updateUser({
|
await updateUser({
|
||||||
updateUserDto: {
|
updateUserDto: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
password: defaultPassword,
|
password: newPassword,
|
||||||
shouldChangePassword: true,
|
shouldChangePassword: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -70,6 +71,23 @@
|
||||||
isShowResetPasswordConfirmation = false;
|
isShowResetPasswordConfirmation = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO move password reset server-side
|
||||||
|
function generatePassword(length: number = 16) {
|
||||||
|
let generatedPassword = '';
|
||||||
|
|
||||||
|
const characterSet = '0123456789' + 'abcdefghijklmnopqrstuvwxyz' + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + ',.-{}+!#$%/()=?';
|
||||||
|
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
let randomNumber = crypto.getRandomValues(new Uint32Array(1))[0];
|
||||||
|
randomNumber = randomNumber / 2 ** 32;
|
||||||
|
randomNumber = Math.floor(randomNumber * characterSet.length);
|
||||||
|
|
||||||
|
generatedPassword += characterSet[randomNumber];
|
||||||
|
}
|
||||||
|
|
||||||
|
return generatedPassword;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||||
import DeleteConfirmDialog from '$lib/components/admin-page/delete-confirm-dialoge.svelte';
|
import DeleteConfirmDialog from '$lib/components/admin-page/delete-confirm-dialoge.svelte';
|
||||||
|
import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
|
||||||
import RestoreDialogue from '$lib/components/admin-page/restore-dialoge.svelte';
|
import RestoreDialogue from '$lib/components/admin-page/restore-dialoge.svelte';
|
||||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
|
@ -17,8 +19,9 @@
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import { websocketEvents } from '$lib/stores/websocket';
|
import { websocketEvents } from '$lib/stores/websocket';
|
||||||
import { asByteUnitString } from '$lib/utils/byte-units';
|
import { asByteUnitString } from '$lib/utils/byte-units';
|
||||||
|
import { copyToClipboard } from '$lib/utils';
|
||||||
import { UserStatus, getAllUsers, type UserResponseDto } from '@immich/sdk';
|
import { UserStatus, getAllUsers, type UserResponseDto } from '@immich/sdk';
|
||||||
import { mdiClose, mdiDeleteRestore, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js';
|
import { mdiClose, mdiContentCopy, mdiDeleteRestore, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
@ -28,10 +31,11 @@
|
||||||
let allUsers: UserResponseDto[] = [];
|
let allUsers: UserResponseDto[] = [];
|
||||||
let shouldShowEditUserForm = false;
|
let shouldShowEditUserForm = false;
|
||||||
let shouldShowCreateUserForm = false;
|
let shouldShowCreateUserForm = false;
|
||||||
let shouldShowInfoPanel = false;
|
let shouldShowPasswordResetSuccess = false;
|
||||||
let shouldShowDeleteConfirmDialog = false;
|
let shouldShowDeleteConfirmDialog = false;
|
||||||
let shouldShowRestoreDialog = false;
|
let shouldShowRestoreDialog = false;
|
||||||
let selectedUser: UserResponseDto;
|
let selectedUser: UserResponseDto;
|
||||||
|
let newPassword: string;
|
||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
allUsers = await getAllUsers({ isAll: false });
|
allUsers = await getAllUsers({ isAll: false });
|
||||||
|
@ -84,7 +88,7 @@
|
||||||
const onEditPasswordSuccess = async () => {
|
const onEditPasswordSuccess = async () => {
|
||||||
await refresh();
|
await refresh();
|
||||||
shouldShowEditUserForm = false;
|
shouldShowEditUserForm = false;
|
||||||
shouldShowInfoPanel = true;
|
shouldShowPasswordResetSuccess = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteUserHandler = (user: UserResponseDto) => {
|
const deleteUserHandler = (user: UserResponseDto) => {
|
||||||
|
@ -121,6 +125,7 @@
|
||||||
<FullScreenModal onClose={() => (shouldShowEditUserForm = false)}>
|
<FullScreenModal onClose={() => (shouldShowEditUserForm = false)}>
|
||||||
<EditUserForm
|
<EditUserForm
|
||||||
user={selectedUser}
|
user={selectedUser}
|
||||||
|
bind:newPassword
|
||||||
canResetPassword={selectedUser?.id !== $user.id}
|
canResetPassword={selectedUser?.id !== $user.id}
|
||||||
on:editSuccess={onEditUserSuccess}
|
on:editSuccess={onEditUserSuccess}
|
||||||
on:resetPasswordSuccess={onEditPasswordSuccess}
|
on:resetPasswordSuccess={onEditPasswordSuccess}
|
||||||
|
@ -147,29 +152,40 @@
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if shouldShowInfoPanel}
|
{#if shouldShowPasswordResetSuccess}
|
||||||
<FullScreenModal onClose={() => (shouldShowInfoPanel = false)}>
|
<FullScreenModal onClose={() => (shouldShowPasswordResetSuccess = false)}>
|
||||||
<div
|
<ConfirmDialogue
|
||||||
class="w-[500px] max-w-[95vw] rounded-3xl bg-immich-bg p-8 text-immich-fg shadow-sm dark:bg-immich-dark-gray dark:text-immich-dark-fg"
|
title="Password Reset Success"
|
||||||
|
confirmText="Done"
|
||||||
|
onConfirm={() => (shouldShowPasswordResetSuccess = false)}
|
||||||
|
onClose={() => (shouldShowPasswordResetSuccess = false)}
|
||||||
|
hideCancelButton={true}
|
||||||
|
confirmColor="green"
|
||||||
>
|
>
|
||||||
<h1 class="mb-4 text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">
|
<svelte:fragment slot="prompt">
|
||||||
Password reset success
|
<div class="flex flex-col gap-4">
|
||||||
</h1>
|
<p>The user's password has been reset:</p>
|
||||||
|
|
||||||
<p>
|
<div class="flex justify-center gap-2">
|
||||||
The user's password has been reset to the default <code
|
<code
|
||||||
class="rounded-md bg-gray-200 px-2 py-1 font-bold text-immich-primary dark:text-immich-dark-primary dark:bg-gray-700"
|
class="rounded-md bg-gray-200 px-2 py-1 font-bold text-immich-primary dark:text-immich-dark-primary dark:bg-gray-700"
|
||||||
>password</code
|
>
|
||||||
>
|
{newPassword}
|
||||||
<br />
|
</code>
|
||||||
<br />
|
<LinkButton on:click={() => copyToClipboard(newPassword)} title="Copy password">
|
||||||
Please inform the user, and they will need to change the password at the next log-on.
|
<div class="flex place-items-center gap-2 text-sm">
|
||||||
</p>
|
<Icon path={mdiContentCopy} size="18" />
|
||||||
|
</div>
|
||||||
|
</LinkButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex w-full">
|
<p>
|
||||||
<Button fullwidth on:click={() => (shouldShowInfoPanel = false)}>Done</Button>
|
Please provide the temporary password to the user and inform them they will need to change the
|
||||||
</div>
|
password at their next login.
|
||||||
</div>
|
</p>
|
||||||
|
</div>
|
||||||
|
</svelte:fragment>
|
||||||
|
</ConfirmDialogue>
|
||||||
</FullScreenModal>
|
</FullScreenModal>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue