mirror of
https://github.com/logto-io/logto.git
synced 2025-01-20 21:32:31 -05:00
feat(core,console): change admin user password (#1268)
This commit is contained in:
parent
b75fea9258
commit
a4d0a940bd
6 changed files with 160 additions and 1 deletions
|
@ -0,0 +1,24 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.changePassword {
|
||||
border: 1px solid var(--color-divider);
|
||||
border-radius: 8px;
|
||||
padding: _.unit(4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 554px;
|
||||
|
||||
.description {
|
||||
font: var(--font-body-medium);
|
||||
color: var(--color-text);
|
||||
margin-right: _.unit(4);
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
.description {
|
||||
font: var(--font-body-medium);
|
||||
color: var(--color-caption);
|
||||
margin-bottom: _.unit(4);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ReactModal from 'react-modal';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import FormField from '@/components/FormField';
|
||||
import ModalLayout from '@/components/ModalLayout';
|
||||
import TextInput from '@/components/TextInput';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
import * as styles from './ChangePassword.module.scss';
|
||||
|
||||
type FormFields = {
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
};
|
||||
|
||||
const ChangePassword = () => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { watch, register, reset } = useForm<FormFields>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const api = useApi();
|
||||
const password = watch('password');
|
||||
const confirmPassword = watch('confirmPassword');
|
||||
const isDisabled = !password || password !== confirmPassword;
|
||||
|
||||
const onSubmit = async () => {
|
||||
setIsLoading(true);
|
||||
await api.patch(`/api/me/password`, { json: { password } }).json();
|
||||
setIsLoading(false);
|
||||
setIsOpen(false);
|
||||
toast.success(t('settings.password_changed'));
|
||||
reset({});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormField title="admin_console.settings.change_password">
|
||||
<div className={styles.changePassword}>
|
||||
<div className={styles.description}>{t('settings.change_password_description')}</div>
|
||||
<Button
|
||||
title="admin_console.settings.change_password"
|
||||
type="default"
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</FormField>
|
||||
<ReactModal
|
||||
isOpen={isOpen}
|
||||
className={modalStyles.content}
|
||||
overlayClassName={modalStyles.overlay}
|
||||
>
|
||||
<ModalLayout
|
||||
title="settings.change_modal_title"
|
||||
footer={
|
||||
<Button
|
||||
type="primary"
|
||||
title="admin_console.general.confirm"
|
||||
disabled={isDisabled || isLoading}
|
||||
onClick={onSubmit}
|
||||
/>
|
||||
}
|
||||
onClose={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className={styles.modal}>
|
||||
<div className={styles.description}>{t('settings.change_modal_description')}</div>
|
||||
<FormField title="admin_console.settings.new_password">
|
||||
<TextInput {...register('password', { required: true })} type="password" />
|
||||
</FormField>
|
||||
<FormField title="admin_console.settings.confirm_password">
|
||||
<TextInput {...register('confirmPassword', { required: true })} type="password" />
|
||||
</FormField>
|
||||
</div>
|
||||
</ModalLayout>
|
||||
</ReactModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangePassword;
|
|
@ -15,6 +15,7 @@ import TabNav, { TabNavItem } from '@/components/TabNav';
|
|||
import useUserPreferences, { UserPreferences } from '@/hooks/use-user-preferences';
|
||||
import * as detailsStyles from '@/scss/details.module.scss';
|
||||
|
||||
import ChangePassword from './components/ChangePassword';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
const Settings = () => {
|
||||
|
@ -94,6 +95,7 @@ const Settings = () => {
|
|||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<ChangePassword />
|
||||
</div>
|
||||
<div className={detailsStyles.footer}>
|
||||
<div className={detailsStyles.footerMain}>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { arbitraryObjectGuard } from '@logto/schemas';
|
||||
import { object } from 'zod';
|
||||
import { passwordRegEx } from '@logto/shared';
|
||||
import { object, string } from 'zod';
|
||||
|
||||
import { encryptUserPassword } from '@/lib/user';
|
||||
import koaGuard from '@/middleware/koa-guard';
|
||||
import { findUserById, updateUserById } from '@/queries/user';
|
||||
|
||||
|
@ -34,4 +36,25 @@ export default function meRoutes<T extends AuthedRouter>(router: T) {
|
|||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/me/password',
|
||||
koaGuard({ body: object({ password: string().regex(passwordRegEx) }) }),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
body: { password },
|
||||
} = ctx.guard;
|
||||
|
||||
const { passwordEncrypted, passwordEncryptionMethod } = await encryptUserPassword(password);
|
||||
|
||||
await updateUserById(ctx.auth, {
|
||||
passwordEncrypted,
|
||||
passwordEncryptionMethod,
|
||||
});
|
||||
|
||||
ctx.status = 204;
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -417,6 +417,18 @@ const translation = {
|
|||
appearance_system: 'Sync with system',
|
||||
appearance_light: 'Light mode',
|
||||
appearance_dark: 'Dark mode',
|
||||
saved: 'Saved!',
|
||||
change_password: 'Change password',
|
||||
change_password_description:
|
||||
'You can change your password for this account. You will use your current username with new password to sign in Admin Console.',
|
||||
change_modal_title: 'Change Account Password',
|
||||
change_modal_description:
|
||||
'You will use your current username with new password to sign in Admin Console.',
|
||||
new_password: 'New password',
|
||||
new_password_placeholder: 'Enter your password',
|
||||
confirm_password: 'Confirm password',
|
||||
confirm_password_placeholder: 'Confirm your password',
|
||||
password_changed: 'Password changed!',
|
||||
},
|
||||
dashboard: {
|
||||
title: 'Dashboard',
|
||||
|
|
|
@ -403,6 +403,16 @@ const translation = {
|
|||
appearance_system: '跟随系统',
|
||||
appearance_light: '浅色模式',
|
||||
appearance_dark: '深色模式',
|
||||
saved: '已保存!',
|
||||
change_password: '修改密码',
|
||||
change_password_description: '修改本账号密码。生效后使用当前用户名和新密码登录管理控制台。',
|
||||
change_modal_title: '修改账号密码',
|
||||
change_modal_description: '生效后使用当前用户名和新密码登录管理控制台。',
|
||||
new_password: '新密码',
|
||||
new_password_placeholder: '输入密码',
|
||||
confirm_password: '确认密码',
|
||||
confirm_password_placeholder: '确认密码',
|
||||
password_changed: '密码已修改!',
|
||||
},
|
||||
dashboard: {
|
||||
title: '仪表盘',
|
||||
|
|
Loading…
Add table
Reference in a new issue