0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(console): display user password information on user details page (#6544)

This commit is contained in:
Xiao Yijun 2024-09-06 11:01:06 +08:00 committed by GitHub
parent 27d2c91d2e
commit f150a67d58
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 198 additions and 99 deletions

View file

@ -0,0 +1,6 @@
---
"@logto/console": minor
"@logto/phrases": minor
---
display user password information on user details page

View file

@ -0,0 +1,34 @@
@use '@/scss/underscore' as _;
.password {
display: flex;
align-items: center;
user-select: none;
border: 1px solid var(--color-divider);
border-radius: 12px;
padding: _.unit(3) _.unit(6);
justify-content: space-between;
font: var(--font-body-2);
.label {
flex: 5;
display: flex;
align-items: center;
.icon {
margin-right: _.unit(3);
color: var(--color-text-secondary);
}
}
.text {
flex: 8;
color: var(--color-text-secondary);
}
.actionButton {
flex: 3;
display: flex;
justify-content: center;
}
}

View file

@ -0,0 +1,102 @@
import { type UserProfileResponse } from '@logto/schemas';
import { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import ReactModal from 'react-modal';
import Key from '@/assets/icons/key.svg?react';
import UserAccountInformation from '@/components/UserAccountInformation';
import Button from '@/ds-components/Button';
import DynamicT from '@/ds-components/DynamicT';
import modalStyles from '@/scss/modal.module.scss';
import ResetPasswordForm from '../../components/ResetPasswordForm';
import styles from './index.module.scss';
type Props = {
readonly user: UserProfileResponse;
readonly onResetPassword: () => void;
};
function UserPassword({ user, onResetPassword }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { hasPassword = false } = user;
const [isResetPasswordFormOpen, setIsResetPasswordFormOpen] = useState(false);
const [newPassword, setNewPassword] = useState<string>();
// Use a ref to store the initial state of hasPassword to determine which title to show when the password has been reset
const initialHasPassword = useRef(hasPassword);
return (
<>
<div className={styles.password}>
<div className={styles.label}>
<Key className={styles.icon} />
<span>
<DynamicT forKey="user_details.field_password" />
</span>
</div>
<div className={styles.text}>
<DynamicT
forKey={`user_details.${hasPassword ? 'password_already_set' : 'no_password_set'}`}
/>
</div>
<div className={styles.actionButton}>
<Button
title={`general.${hasPassword ? 'reset' : 'generate'}`}
type="text"
size="small"
onClick={() => {
setIsResetPasswordFormOpen(true);
}}
/>
</div>
</div>
<ReactModal
shouldCloseOnEsc
isOpen={isResetPasswordFormOpen}
className={modalStyles.content}
overlayClassName={modalStyles.overlay}
onRequestClose={() => {
setIsResetPasswordFormOpen(false);
}}
>
<ResetPasswordForm
userId={user.id}
hasPassword={hasPassword}
onClose={(password) => {
setIsResetPasswordFormOpen(false);
if (password) {
setNewPassword(password);
onResetPassword();
}
}}
/>
</ReactModal>
{newPassword && (
<UserAccountInformation
title={`user_details.reset_password.${
initialHasPassword.current ? 'reset_complete' : 'generate_complete'
}`}
user={user}
password={newPassword}
passwordLabel={t(
`user_details.reset_password.${
initialHasPassword.current ? 'new_password' : 'password'
}`
)}
onClose={() => {
setNewPassword(undefined);
// Update the initial state to true once the user has acknowledged the new password
// eslint-disable-next-line @silverhand/fp/no-mutation
initialHasPassword.current = true;
}}
/>
)}
</>
);
}
export default UserPassword;

View file

@ -28,6 +28,7 @@ import { userDetailsParser } from '../utils';
import PersonalAccessTokens from './PersonalAccessTokens';
import UserMfaVerifications from './UserMfaVerifications';
import UserPassword from './UserPassword';
import UserSocialIdentities from './UserSocialIdentities';
import UserSsoIdentities from './UserSsoIdentities';
@ -150,6 +151,14 @@ function UserSettings() {
placeholder={t('users.placeholder_username')}
/>
</FormField>
<FormField title="user_details.field_password">
<UserPassword
user={user}
onResetPassword={() => {
onUserUpdated({ ...user, hasPassword: true });
}}
/>
</FormField>
<FormField title="user_details.field_connectors">
<UserSocialIdentities
userId={user.id}

View file

@ -8,10 +8,11 @@ import { generateRandomPassword } from '@/utils/password';
type Props = {
readonly userId: string;
readonly hasPassword: boolean;
readonly onClose?: (password?: string) => void;
};
function ResetPasswordForm({ onClose, userId }: Props) {
function ResetPasswordForm({ onClose, userId, hasPassword }: Props) {
const { t } = useTranslation(undefined, {
keyPrefix: 'admin_console',
});
@ -29,7 +30,7 @@ function ResetPasswordForm({ onClose, userId }: Props) {
return (
<ConfirmModal
isOpen
title="user_details.reset_password.title"
title={`user_details.reset_password.${hasPassword ? 'reset_title' : 'generate_title'}`}
isLoading={isLoading}
onCancel={() => {
onClose?.();

View file

@ -3,13 +3,11 @@ import classNames from 'classnames';
import { useEffect, useState } from 'react';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import ReactModal from 'react-modal';
import { Outlet, useLocation, useParams } from 'react-router-dom';
import useSWR from 'swr';
import Delete from '@/assets/icons/delete.svg?react';
import Forbidden from '@/assets/icons/forbidden.svg?react';
import Reset from '@/assets/icons/reset.svg?react';
import Shield from '@/assets/icons/shield.svg?react';
import DetailsPage from '@/components/DetailsPage';
import DetailsPageHeader from '@/components/DetailsPage/DetailsPageHeader';
@ -22,14 +20,11 @@ import TabNav, { TabNavItem } from '@/ds-components/TabNav';
import type { RequestError } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import useTenantPathname from '@/hooks/use-tenant-pathname';
import modalStyles from '@/scss/modal.module.scss';
import { buildUrl } from '@/utils/url';
import { getUserTitle, getUserSubtitle } from '@/utils/user';
import UserAccountInformation from '../../components/UserAccountInformation';
import SuspendedTag from '../Users/components/SuspendedTag';
import ResetPasswordForm from './components/ResetPasswordForm';
import styles from './index.module.scss';
import { type UserDetailsOutletContext } from './types';
@ -41,10 +36,8 @@ function UserDetails() {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [isDeleteFormOpen, setIsDeleteFormOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [isResetPasswordFormOpen, setIsResetPasswordFormOpen] = useState(false);
const [isToggleSuspendFormOpen, setIsToggleSuspendFormOpen] = useState(false);
const [isUpdatingSuspendState, setIsUpdatingSuspendState] = useState(false);
const [resetResult, setResetResult] = useState<string>();
// Get user info with user's SSO identities in a single API call.
const { data, error, mutate } = useSWR<UserProfileResponse, RequestError>(
@ -59,7 +52,6 @@ function UserDetails() {
useEffect(() => {
setIsDeleteFormOpen(false);
setIsResetPasswordFormOpen(false);
setIsToggleSuspendFormOpen(false);
}, [pathname]);
@ -119,13 +111,6 @@ function UserDetails() {
primaryTag={isSuspendedUser && <SuspendedTag />}
identifier={{ name: 'User ID', value: data.id }}
actionMenuItems={[
{
title: 'user_details.reset_password.reset_password',
icon: <Reset />,
onClick: () => {
setIsResetPasswordFormOpen(true);
},
},
{
title: isSuspendedUser
? 'user_details.reactivate_user'
@ -145,26 +130,6 @@ function UserDetails() {
},
]}
/>
<ReactModal
shouldCloseOnEsc
isOpen={isResetPasswordFormOpen}
className={modalStyles.content}
overlayClassName={modalStyles.overlay}
onRequestClose={() => {
setIsResetPasswordFormOpen(false);
}}
>
<ResetPasswordForm
userId={data.id}
onClose={(password) => {
setIsResetPasswordFormOpen(false);
if (password) {
setResetResult(password);
}
}}
/>
</ReactModal>
<DeleteConfirmModal
isOpen={isDeleteFormOpen}
isLoading={isDeleting}
@ -217,17 +182,6 @@ function UserDetails() {
} satisfies UserDetailsOutletContext
}
/>
{resetResult && (
<UserAccountInformation
title="user_details.reset_password.congratulations"
user={data}
password={resetResult}
passwordLabel={t('user_details.reset_password.new_password')}
onClose={() => {
setResetResult(undefined);
}}
/>
)}
</>
)}
</DetailsPage>

View file

@ -1,4 +1,4 @@
import type { User, UserProfileResponse } from '@logto/schemas';
import type { UserProfileResponse } from '@logto/schemas';
export type UserDetailsForm = {
primaryEmail: string;
@ -13,5 +13,5 @@ export type UserDetailsForm = {
export type UserDetailsOutletContext = {
user: UserProfileResponse;
isDeleting: boolean;
onUserUpdated: (user?: User) => void;
onUserUpdated: (user?: UserProfileResponse) => void;
};

View file

@ -11,11 +11,10 @@ const user_details = {
delete_description: 'لا يمكن التراجع عن هذا الإجراء. سيتم حذف المستخدم نهائيًا.',
deleted: 'تم حذف المستخدم بنجاح',
reset_password: {
reset_password: 'إعادة تعيين كلمة المرور',
title: 'هل أنت متأكد أنك تريد إعادة تعيين كلمة المرور؟',
reset_title: 'هل أنت متأكد أنك تريد إعادة تعيين كلمة المرور؟',
content:
'لا يمكن التراجع عن هذا الإجراء. سيتم إعادة تعيين معلومات تسجيل الدخول الخاصة بالمستخدم.',
congratulations: 'تم إعادة تعيين هذا المستخدم',
reset_complete: 'تم إعادة تعيين هذا المستخدم',
new_password: 'كلمة المرور الجديدة:',
},
tab_settings: 'الإعدادات',

View file

@ -12,11 +12,10 @@ const user_details = {
'Diese Aktion kann nicht rückgängig gemacht werden. Der Benutzer wird permanent gelöscht.',
deleted: 'Der Benutzer wurde erfolgreich gelöscht',
reset_password: {
reset_password: 'Passwort zurücksetzen',
title: 'Willst du das Passwort wirklich zurücksetzen?',
reset_title: 'Willst du das Passwort wirklich zurücksetzen?',
content:
'Diese Aktion kann nicht rückgängig gemacht werden. Das Anmeldeinformationen werden zurückgesetzt.',
congratulations: 'Der Benutzer wurde erfolgreich zurückgesetzt',
reset_complete: 'Der Benutzer wurde erfolgreich zurückgesetzt',
new_password: 'Neues Passwort:',
},
tab_settings: 'Einstellungen',

View file

@ -71,6 +71,8 @@ const general = {
delete_field: 'Delete {{field}}',
coming_soon: 'Coming soon',
or: 'Or',
reset: 'Reset',
generate: 'Generate',
};
export default Object.freeze(general);

View file

@ -11,11 +11,13 @@ const user_details = {
delete_description: 'This action cannot be undone. It will permanently delete the user.',
deleted: 'The user has been successfully deleted',
reset_password: {
reset_password: 'Reset password',
title: 'Are you sure you want to reset the password?',
content: "This action cannot be undone. This will reset the user's log in information.",
congratulations: 'This user has been reset',
reset_title: 'Are you sure you want to reset the password?',
generate_title: 'Are you sure you want to generate a password?',
content: "This action cannot be undone. This will update the user's sign-in information.",
reset_complete: 'The password has been reset',
generate_complete: 'The password has been generated',
new_password: 'New password:',
password: 'Password:',
},
tab_settings: 'Settings',
tab_roles: 'Roles',
@ -28,6 +30,7 @@ const user_details = {
field_email: 'Email address',
field_phone: 'Phone number',
field_username: 'Username',
field_password: 'Password',
field_name: 'Name',
field_avatar: 'Avatar image URL',
field_avatar_placeholder: 'https://your.cdn.domain/avatar.png',
@ -41,6 +44,8 @@ const user_details = {
field_sso_connectors: 'Enterprise connections',
custom_data_invalid: 'Custom data must be a valid JSON object',
profile_invalid: 'Profile must be a valid JSON object',
password_already_set: 'Password already set',
no_password_set: 'No password set',
connectors: {
connectors: 'Connectors',
user_id: 'User ID',

View file

@ -12,11 +12,10 @@ const user_details = {
delete_description: 'Esta acción no se puede deshacer. Eliminará permanentemente al usuario.',
deleted: 'Usuario eliminado con éxito',
reset_password: {
reset_password: 'Restablecer contraseña',
title: '¿Está seguro de que desea restablecer la contraseña?',
reset_title: '¿Está seguro de que desea restablecer la contraseña?',
content:
'Esta acción no se puede deshacer. Esto restablecerá la información de inicio de sesión del usuario.',
congratulations: 'Se ha restablecido la información de inicio de sesión del usuario',
reset_complete: 'Se ha restablecido la información de inicio de sesión del usuario',
new_password: 'Nueva contraseña:',
},
tab_settings: 'Configuración',

View file

@ -12,11 +12,10 @@ const user_details = {
"Cette action ne peut être annulée. Elle supprimera définitivement l'utilisateur.",
deleted: "L'utilisateur a été supprimé avec succès",
reset_password: {
reset_password: 'Réinitialiser le mot de passe',
title: 'Êtes-vous sûr de vouloir réinitialiser le mot de passe ?',
reset_title: 'Êtes-vous sûr de vouloir réinitialiser le mot de passe ?',
content:
"Cette action ne peut être annulée. Cette action réinitialisera les informations de connexion de l'utilisateur.",
congratulations: 'Cet utilisateur a été réinitialisé',
reset_complete: 'Cet utilisateur a été réinitialisé',
new_password: 'Nouveau mot de passe :',
},
tab_settings: 'Paramètres',

View file

@ -12,11 +12,10 @@ const user_details = {
"Questa azione non può essere annullata. Eliminerai l'utente in modo permanente.",
deleted: "L'utente è stato eliminato con successo",
reset_password: {
reset_password: 'Resetta la password',
title: 'Sei sicuro di voler reimpostare la password?',
reset_title: 'Sei sicuro di voler reimpostare la password?',
content:
"Questa azione non può essere annullata. Questo reimposterà le informazioni di accesso dell'utente.",
congratulations: "L'utente è stato reimpostato",
reset_complete: "L'utente è stato reimpostato",
new_password: 'Nuova password:',
},
tab_settings: 'Impostazioni',

View file

@ -11,10 +11,9 @@ const user_details = {
delete_description: 'この操作は取り消せません。ユーザーが永久に削除されます。',
deleted: 'ユーザーは正常に削除されました',
reset_password: {
reset_password: 'パスワードをリセット',
title: '本当にパスワードをリセットしますか?',
reset_title: '本当にパスワードをリセットしますか?',
content: 'この操作は取り消せません。ユーザーのログイン情報がリセットされます。',
congratulations: 'このユーザーはリセットされました',
reset_complete: 'このユーザーはリセットされました',
new_password: '新しいパスワード:',
},
tab_settings: '設定',

View file

@ -11,10 +11,9 @@ const user_details = {
delete_description: '이 사용자를 영원히 삭제할까요? 이 행동은 취소될 수 없어요.',
deleted: '해당 사용자가 성공적으로 삭제되었어요.',
reset_password: {
reset_password: '비밀번호 초기화',
title: '정말로 비밀번호를 초기화 할까요?',
reset_title: '정말로 비밀번호를 초기화 할까요?',
content: '정말로 비밀번호를 초기화 할까요? 이 행동은 취소될 수 없어요.',
congratulations: '해당 사용자의 비밀번호가 성공적으로 초기화 되었어요.',
reset_complete: '해당 사용자의 비밀번호가 성공적으로 초기화 되었어요.',
new_password: '새로운 비밀번호:',
},
tab_settings: '설정',

View file

@ -11,10 +11,9 @@ const user_details = {
delete_description: 'Tej akcji nie można cofnąć. Usunie to użytkownika na stałe.',
deleted: 'Użytkownik został pomyślnie usunięty',
reset_password: {
reset_password: 'Zresetuj hasło',
title: 'Czy na pewno chcesz zresetować hasło?',
reset_title: 'Czy na pewno chcesz zresetować hasło?',
content: 'Tej akcji nie można cofnąć. To zresetuje informacje o logowaniu użytkownika.',
congratulations: 'Ten użytkownik został zresetowany',
reset_complete: 'Ten użytkownik został zresetowany',
new_password: 'Nowe hasło:',
},
tab_settings: 'Ustawienia',

View file

@ -11,10 +11,9 @@ const user_details = {
delete_description: 'Essa ação não pode ser desfeita. Isso excluirá permanentemente o usuário.',
deleted: 'O usuário foi excluído com sucesso',
reset_password: {
reset_password: 'Redefinir senha',
title: 'Tem certeza de que deseja redefinir a senha?',
reset_title: 'Tem certeza de que deseja redefinir a senha?',
content: 'Essa ação não pode ser desfeita. Isso redefinirá as informações de login do usuário.',
congratulations: 'Este usuário foi redefinido',
reset_complete: 'Este usuário foi redefinido',
new_password: 'Nova senha:',
},
tab_settings: 'Configurações',

View file

@ -12,11 +12,10 @@ const user_details = {
'Esta ação não pode ser desfeita. Isso irá eliminar o utilizador permanentemente.',
deleted: 'O utilizador foi eliminado com sucesso',
reset_password: {
reset_password: 'Redefinir palavra-passe',
title: 'Tem a certeza de que deseja redefinir a palavra-passe?',
reset_title: 'Tem a certeza de que deseja redefinir a palavra-passe?',
content:
'Esta ação não pode ser desfeita. Isso irá redefinir as informações de login do utilizador.',
congratulations: 'Este utilizador foi redefinido',
reset_complete: 'Este utilizador foi redefinido',
new_password: 'Nova palavra-passe:',
},
tab_settings: 'Definições',

View file

@ -11,10 +11,9 @@ const user_details = {
delete_description: 'Это действие нельзя отменить. Оно окончательно удалит пользователя.',
deleted: 'Пользователь успешно удален',
reset_password: {
reset_password: 'Сбросить пароль',
title: 'Вы уверены, что хотите сбросить пароль?',
reset_title: 'Вы уверены, что хотите сбросить пароль?',
content: 'Это действие нельзя отменить. Это сбросит информацию для входа пользователя.',
congratulations: 'Пользователь был сброшен',
reset_complete: 'Пользователь был сброшен',
new_password: 'Новый пароль:',
},
tab_settings: 'Настройки',

View file

@ -11,10 +11,9 @@ const user_details = {
delete_description: 'Bu işlem geri alınamaz. Kullanıcıyı kalıcı olarak siler.',
deleted: 'Kullanıcı başarıyla silindi.',
reset_password: {
reset_password: 'Şifreyi sıfırla',
title: 'Şifreyi sıfırlamak istediğinizden emin misiniz?',
reset_title: 'Şifreyi sıfırlamak istediğinizden emin misiniz?',
content: 'Bu işlem geri alınamaz. Bu, kullanıcının oturum açma bilgilerini sıfırlayacaktır.',
congratulations: 'Bu kullanıcı sıfırlandı',
reset_complete: 'Bu kullanıcı sıfırlandı',
new_password: 'Yeni şifre:',
},
tab_settings: 'Ayarlar',

View file

@ -11,10 +11,9 @@ const user_details = {
delete_description: '本操作将永久删除该用户,且无法撤销。',
deleted: '用户已成功删除。',
reset_password: {
reset_password: '重置密码',
title: '确定要重置密码?',
reset_title: '确定要重置密码?',
content: '本操作不可撤销,将会重置用户的登录信息。',
congratulations: '该用户已被重置',
reset_complete: '该用户已被重置',
new_password: '新密码:',
},
tab_settings: '设置',

View file

@ -11,10 +11,10 @@ const user_details = {
delete_description: '本操作將永久刪除該用戶,且無法撤銷。',
deleted: '用戶已成功刪除。',
reset_password: {
reset_password: '重置密碼',
title: '確定要重置密碼?',
reset_title: '確定要重置密碼?',
generate_title: '確定要生成密碼?',
content: '本操作不可撤銷,將會重置用戶的登錄信息。',
congratulations: '該用戶已被重置',
reset_complete: '該用戶已被重置',
new_password: '新密碼:',
},
tab_settings: '設置',

View file

@ -11,10 +11,9 @@ const user_details = {
delete_description: '本操作將永久刪除該用戶,且無法撤銷。',
deleted: '用戶已成功刪除。',
reset_password: {
reset_password: '重置密碼',
title: '確定要重置密碼?',
reset_title: '確定要重置密碼?',
content: '本操作不可撤銷,將會重置用戶的登入資訊。',
congratulations: '該用戶已被重置',
reset_complete: '該用戶已被重置',
new_password: '新密碼:',
},
tab_settings: '設定',