From 6ae0db8c456460a1741210a55a2520ea06daa579 Mon Sep 17 00:00:00 2001 From: Yannick Marcon Date: Thu, 2 Jan 2025 12:53:04 +0100 Subject: [PATCH 1/6] feat: users table added --- agate-ui/src/components/UsersTable.vue | 275 +++++++++++++++++++++++++ agate-ui/src/i18n/en/index.js | 44 +++- agate-ui/src/i18n/fr/index.js | 44 +++- agate-ui/src/pages/UsersPage.vue | 8 +- agate-ui/src/stores/user.ts | 19 ++ 5 files changed, 382 insertions(+), 8 deletions(-) create mode 100644 agate-ui/src/components/UsersTable.vue diff --git a/agate-ui/src/components/UsersTable.vue b/agate-ui/src/components/UsersTable.vue new file mode 100644 index 00000000..5ab1bfd8 --- /dev/null +++ b/agate-ui/src/components/UsersTable.vue @@ -0,0 +1,275 @@ + + + + diff --git a/agate-ui/src/i18n/en/index.js b/agate-ui/src/i18n/en/index.js index 30cf5d78..a55a53d1 100644 --- a/agate-ui/src/i18n/en/index.js +++ b/agate-ui/src/i18n/en/index.js @@ -10,10 +10,45 @@ export default { applications_hint: 'Members of a group get access to the applications associated to this group.', edit: 'Edit group', remove: 'Remove group', - remove_confirm: 'Please confirm group removal. User members of this group will not be removed.', + remove_confirm: 'Please confirm group removal: {name}. User members of this group will not be removed.', saved: 'Group saved', save_failed: 'Failed to save group', }, + user: { + add: 'Add user', + approve: 'Approve user', + approved: 'User approved', + approve_error: 'Failed to approve user', + edit: 'Edit user', + reject: 'Reject user', + reject_confirm: 'Please confirm user rejection: {name}', + remove: 'Remove user', + remove_confirm: 'Please confirm user removal: {name}', + removed: 'User removed', + remove_error: 'Failed to remove user', + reset_password: 'Reset password', + reset_password_confirm: 'Please confirm that you want to send a password reset notification to this user.', + reset_password_error: 'Failed to reset user password', + reset_password_success: 'Password reset notification sent', + role: { + 'agate-user': 'User', + 'agate-administrator': 'Administrator', + }, + saved: 'User saved', + save_failed: 'Failed to save user', + status: { + ACTIVE: 'Active', + APPROVED: 'Approved', + INACTIVE: 'Inactive', + PENDING: 'Pending', + }, + status_hint: { + ACTIVE: 'User is operational', + APPROVED: 'User was approved by administrator but user confirmation is still required (email, password reset)', + INACTIVE: 'User is disabled', + PENDING: 'User is pending approval', + }, + }, add: 'Add', administration: 'Administration', content_management: 'Content Management', @@ -36,4 +71,11 @@ export default { description: 'Description', name_required: 'Name is required', search: 'Search', + inherited_from: 'Inherited from: {parent}', + fullName: 'Full Name', + email: 'Email', + realm: 'Realm', + status: 'Status', + role: 'Role', + otpEnabled: '2FA', }; diff --git a/agate-ui/src/i18n/fr/index.js b/agate-ui/src/i18n/fr/index.js index 3642ee90..300bbf45 100644 --- a/agate-ui/src/i18n/fr/index.js +++ b/agate-ui/src/i18n/fr/index.js @@ -10,10 +10,45 @@ export default { applications_hint: "Les membres d'un groupe ont accès aux applications associées à ce groupe.", edit: 'Modifier le groupe', remove: 'Suppression du groupe', - remove_confirm: 'Veuillez confirmer la suppression du groupe. Les usagers membres de ce groupe ne seront pas affectés.', + remove_confirm: 'Veuillez confirmer la suppression du groupe: {name}. Les usagers membres de ce groupe ne seront pas affectés.', saved: 'Groupe enregistré', save_failed: "Échec de l'enregistrement du groupe", }, + user: { + add: 'Ajouter un usager', + approve: "Approuver l'usager", + approved: 'Usager approuvé', + approve_error: "Échec de l'approbation de l'usager", + edit: "Modifier l'usager", + reject: "Rejeter l'usager", + reject_confirm: "Veuillez confirmer le rejet de l'usager: {name}", + remove: "Suppression de l'usager", + remove_confirm: "Veuillez confirmer la suppression de l'usager: {name}", + removed: 'Usager supprimé', + remove_error: "Échec de la suppression de l'usager", + reset_password: 'Réinitialiser le mot de passe', + reset_password_confirm: 'Veuillez confirmer que vous souhaitez envoyer une notification de réinitialisation du mot de passe à cet usager.', + reset_password_error: "Échec de la réinitialisation du mot de passe de l'usager", + reset_password_success: 'Notification de réinitialisation du mot de passe envoyée', + role: { + 'agate-user': 'Usager', + 'agate-administrator': 'Administrateur', + }, + saved: 'Usager enregistré', + save_failed: "Échec de l'enregistrement de l'usager", + status: { + ACTIVE: 'Actif', + APPROVED: 'Approuvé', + INACTIVE: 'Inactif', + PENDING: 'En attente', + }, + status_hint: { + ACTIVE: 'Usager opérationnel', + APPROVED: "Usager approuvé par l'administrateur mais confirmation de l'usager requise (courriel, réinitialisation du mot de passe)", + INACTIVE: 'Usager désactivé', + PENDING: "Usager en attente d'approbation", + }, + }, add: 'ajouter', administration: 'Administration', content_management: 'Gestion de contenu', @@ -36,4 +71,11 @@ export default { description: 'Description', name_required: 'Le nom est requis', search: 'Rechercher', + inherited_from: 'Hérité de: {parent}', + fullName: 'Nom complet', + email: 'Courriel', + realm: 'Domaine', + status: 'Statut', + role: 'Rôle', + otpEnabled: '2FA', }; diff --git a/agate-ui/src/pages/UsersPage.vue b/agate-ui/src/pages/UsersPage.vue index beeb282f..a4ecc122 100644 --- a/agate-ui/src/pages/UsersPage.vue +++ b/agate-ui/src/pages/UsersPage.vue @@ -7,16 +7,12 @@ -
{{ userStore.users }}
+
diff --git a/agate-ui/src/stores/user.ts b/agate-ui/src/stores/user.ts index 9a3bbee2..b999471c 100644 --- a/agate-ui/src/stores/user.ts +++ b/agate-ui/src/stores/user.ts @@ -14,8 +14,27 @@ export const useUserStore = defineStore('user', () => { }); } + async function remove(user: UserDto) { + return api.delete(`/user/${user.id}`); + } + + async function resetPassword(user: UserDto) { + return api.put(`/user/${user.id}/reset_password`); + } + + async function approve(user: UserDto) { + return api.put( + `/user/${user.id}/status`, + { status: 'approved' }, + { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }, + ); + } + return { users, init, + remove, + resetPassword, + approve, }; }); From d92ef5b041dd7aa5dd97c4a003fe82b0aaba0722 Mon Sep 17 00:00:00 2001 From: Yannick Marcon Date: Thu, 2 Jan 2025 17:43:17 +0100 Subject: [PATCH 2/6] feat: add and edit user in a dialog --- agate-ui/src/components/GroupDialog.vue | 16 +- agate-ui/src/components/UserDialog.vue | 224 ++++++++++++++++++++++++ agate-ui/src/components/UsersTable.vue | 4 +- agate-ui/src/i18n/en/index.js | 65 ++++--- agate-ui/src/i18n/fr/index.js | 65 ++++--- agate-ui/src/stores/group.ts | 1 + agate-ui/src/stores/system.ts | 4 +- agate-ui/src/stores/user.ts | 6 + 8 files changed, 327 insertions(+), 58 deletions(-) create mode 100644 agate-ui/src/components/UserDialog.vue diff --git a/agate-ui/src/components/GroupDialog.vue b/agate-ui/src/components/GroupDialog.vue index ec1dca44..c2982821 100644 --- a/agate-ui/src/components/GroupDialog.vue +++ b/agate-ui/src/components/GroupDialog.vue @@ -11,10 +11,14 @@ - + - diff --git a/agate-ui/src/components/UsersTable.vue b/agate-ui/src/components/UsersTable.vue index 5ab1bfd8..db45443f 100644 --- a/agate-ui/src/components/UsersTable.vue +++ b/agate-ui/src/components/UsersTable.vue @@ -142,7 +142,7 @@ :text="t('user.reject_confirm', { name: selected?.name })" @confirm="onDelete" /> - + @@ -153,7 +153,7 @@ export default defineComponent({ diff --git a/agate-ui/src/components/UsersTable.vue b/agate-ui/src/components/UsersTable.vue index db45443f..6726b212 100644 --- a/agate-ui/src/components/UsersTable.vue +++ b/agate-ui/src/components/UsersTable.vue @@ -39,13 +39,14 @@ @click="onShowDelete(props.row)" /> diff --git a/agate-ui/src/i18n/en/index.js b/agate-ui/src/i18n/en/index.js index 48abc6e0..6dfb8329 100644 --- a/agate-ui/src/i18n/en/index.js +++ b/agate-ui/src/i18n/en/index.js @@ -20,11 +20,18 @@ export default { approve: 'Approve user', approved: 'User approved', approve_error: 'Failed to approve user', + copy_password: 'Copy password', edit: 'Edit user', firstName: 'First Name', + generate_password: 'Generate password', groups_hint: 'Users can be members of groups to access applications associated to these groups.', language: 'Language', lastName: 'Last Name', + password: 'Password', + password_copied: 'Password copied', + password_hint: 'Password must contain at least one digit, one upper case alphabet, one lower case alphabet, one special character (which includes @#$%^&+=!) and no white spaces.', + password_min_length: 'Password must be at least {min} characters', + password_required: 'Password is required', realm_hint: 'Realm in which user authenticates.', reject: 'Reject user', reject_confirm: 'Please confirm user rejection: {name}', @@ -46,6 +53,7 @@ export default { }, saved: 'User saved', save_failed: 'Failed to save user', + show_password: 'Show password', status: { ACTIVE: 'Active', APPROVED: 'Approved', diff --git a/agate-ui/src/i18n/fr/index.js b/agate-ui/src/i18n/fr/index.js index b1cd0273..df578e10 100644 --- a/agate-ui/src/i18n/fr/index.js +++ b/agate-ui/src/i18n/fr/index.js @@ -20,11 +20,18 @@ export default { approve: "Approuver l'usager", approved: 'Usager approuvé', approve_error: "Échec de l'approbation de l'usager", + copy_password: 'Copier le mot de passe', edit: "Modifier l'usager", firstName: 'Prénom', + generate_password: 'Générer un mot de passe', groups_hint: 'Les usagers peuvent être membres de groupes pour accéder aux applications associées à ces groupes.', language: 'Langue', lastName: 'Nom de famille', + password: 'Mot de passe', + password_copied: 'Mot de passe copié', + password_hint: 'Le mot de passe doit contenir au moins un chiffre, une lettre majuscule, une lettre minuscule, un caractère spécial (qui inclut @#$%^&+=!) et aucun espace blanc.', + password_min_length: 'Le mot de passe doit comporter au moins {min} caractères', + password_required: 'Le mot de passe est requis', realm_hint: "Domaine dans lequel l'usager est authentifié.", reject: "Rejeter l'usager", reject_confirm: "Veuillez confirmer le rejet de l'usager: {name}", @@ -46,6 +53,7 @@ export default { }, saved: 'Usager enregistré', save_failed: "Échec de l'enregistrement de l'usager", + show_password: 'Afficher le mot de passe', status: { ACTIVE: 'Actif', APPROVED: 'Approuvé', diff --git a/agate-ui/src/stores/user.ts b/agate-ui/src/stores/user.ts index 5bf35ae3..a2c168ba 100644 --- a/agate-ui/src/stores/user.ts +++ b/agate-ui/src/stores/user.ts @@ -22,6 +22,14 @@ export const useUserStore = defineStore('user', () => { return api.put(`/user/${user.id}/reset_password`); } + async function updatePassword(user: UserDto, password: string) { + return api.put( + `/user/${user.id}/password`, + { password }, + { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }, + ); + } + async function approve(user: UserDto) { return api.put( `/user/${user.id}/status`, @@ -30,9 +38,42 @@ export const useUserStore = defineStore('user', () => { ); } - async function save(user: UserDto) { + async function save(user: UserDto, password: string | undefined = undefined) { user.name = user.name.trim(); - return user.id ? api.put(`/user/${user.id}`, user) : api.post('/users', { user }); + return user.id ? api.put(`/user/${user.id}`, user) : api.post('/users', { password, user }); + } + + function generatePassword(length: number = 12): string { + if (length < 8) { + throw new Error('Password length should be at least 8 characters for strength.'); + } + + const upperCase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + const lowerCase = 'abcdefghijklmnopqrstuvwxyz'; + const numbers = '0123456789'; + const specialChars = '@#$%^&+=!'; + + const allChars = upperCase + lowerCase + numbers + specialChars; + + const getRandomChar = (chars: string) => chars[Math.floor(Math.random() * chars.length)]; + + // Ensure the password contains at least one of each character type + let password = [ + getRandomChar(upperCase), + getRandomChar(lowerCase), + getRandomChar(numbers), + getRandomChar(specialChars), + ]; + + // Fill the rest of the password length with random characters from all types + for (let i = password.length; i < length; i++) { + password.push(getRandomChar(allChars)); + } + + // Shuffle the password to make it more random + password = password.sort(() => Math.random() - 0.5); + + return password.join(''); } return { @@ -42,5 +83,7 @@ export const useUserStore = defineStore('user', () => { resetPassword, approve, save, + generatePassword, + updatePassword, }; }); From 7b0003bab29a4c1b0e85927120a10344b8c2f927 Mon Sep 17 00:00:00 2001 From: Yannick Marcon Date: Fri, 3 Jan 2025 15:19:36 +0100 Subject: [PATCH 4/6] feat: update user password --- agate-ui/src/components/ConfirmDialog.vue | 5 - agate-ui/src/components/EssentialLink.vue | 5 - agate-ui/src/components/GroupsTable.vue | 5 - agate-ui/src/components/MainDrawer.vue | 5 - agate-ui/src/components/SchemaForm.vue | 14 +- agate-ui/src/components/SchemaFormItem.vue | 5 - .../src/components/UpdatePasswordDialog.vue | 131 ++++++++++++++++++ agate-ui/src/components/UsersTable.vue | 31 +++-- agate-ui/src/i18n/en/index.js | 1 + agate-ui/src/i18n/fr/index.js | 1 + 10 files changed, 157 insertions(+), 46 deletions(-) create mode 100644 agate-ui/src/components/UpdatePasswordDialog.vue diff --git a/agate-ui/src/components/ConfirmDialog.vue b/agate-ui/src/components/ConfirmDialog.vue index 8f985cb5..ecf06432 100644 --- a/agate-ui/src/components/ConfirmDialog.vue +++ b/agate-ui/src/components/ConfirmDialog.vue @@ -21,11 +21,6 @@ - diff --git a/agate-ui/src/components/UsersTable.vue b/agate-ui/src/components/UsersTable.vue index 6726b212..bcd713cb 100644 --- a/agate-ui/src/components/UsersTable.vue +++ b/agate-ui/src/components/UsersTable.vue @@ -45,11 +45,21 @@ flat size="sm" color="secondary" - :title="t('user.reset_password')" - :icon="toolsVisible[props.row.name] ? 'send' : 'none'" + :title="t('user.more_actions')" + :icon="toolsVisible[props.row.name] ? 'lock' : 'none'" class="q-ml-xs" - @click="onShowResetPassword(props.row)" - /> + > + + + + {{ t('user.reset_password') }} + + + {{ t('user.update_password') }} + + + + + - diff --git a/agate-ui/src/i18n/en/index.js b/agate-ui/src/i18n/en/index.js index d5259bd3..c71b1ebb 100644 --- a/agate-ui/src/i18n/en/index.js +++ b/agate-ui/src/i18n/en/index.js @@ -21,12 +21,15 @@ export default { approved: 'User approved', approve_error: 'Failed to approve user', copy_password: 'Copy password', + disable_2fa: 'Disable 2FA', edit: 'Edit user', firstName: 'First Name', generate_password: 'Generate password', groups_hint: 'Users can be members of groups to access applications associated to these groups.', language: 'Language', lastName: 'Last Name', + otp_disabled: '2FA disabled', + otp_disable_error: 'Failed to disable 2FA', password: 'Password', password_copied: 'Password copied', password_hint: 'Password must contain at least one digit, one upper case alphabet, one lower case alphabet, one special character (which includes @#$%^&+=!) and no white spaces.', diff --git a/agate-ui/src/i18n/fr/index.js b/agate-ui/src/i18n/fr/index.js index ab95af22..a12d977c 100644 --- a/agate-ui/src/i18n/fr/index.js +++ b/agate-ui/src/i18n/fr/index.js @@ -21,12 +21,15 @@ export default { approved: 'Usager approuvé', approve_error: "Échec de l'approbation de l'usager", copy_password: 'Copier le mot de passe', + disable_2fa: 'Désactiver le 2FA', edit: "Modifier l'usager", firstName: 'Prénom', generate_password: 'Générer un mot de passe', groups_hint: 'Les usagers peuvent être membres de groupes pour accéder aux applications associées à ces groupes.', language: 'Langue', lastName: 'Nom de famille', + otp_disabled: '2FA désactivé', + otp_disable_error: "Échec de la désactivation du 2FA", password: 'Mot de passe', password_copied: 'Mot de passe copié', password_hint: 'Le mot de passe doit contenir au moins un chiffre, une lettre majuscule, une lettre minuscule, un caractère spécial (qui inclut @#$%^&+=!) et aucun espace blanc.', diff --git a/agate-ui/src/stores/user.ts b/agate-ui/src/stores/user.ts index a2c168ba..af4a2677 100644 --- a/agate-ui/src/stores/user.ts +++ b/agate-ui/src/stores/user.ts @@ -30,6 +30,10 @@ export const useUserStore = defineStore('user', () => { ); } + async function disableOTP(user: UserDto) { + return api.delete(`/user/${user.id}/otp`); + } + async function approve(user: UserDto) { return api.put( `/user/${user.id}/status`, @@ -85,5 +89,6 @@ export const useUserStore = defineStore('user', () => { save, generatePassword, updatePassword, + disableOTP, }; }); From 35a0c4cf327227a8fddda9bb60f09b518d44adab Mon Sep 17 00:00:00 2001 From: Yannick Marcon Date: Fri, 3 Jan 2025 15:50:57 +0100 Subject: [PATCH 6/6] fix: case user is undefined --- agate-ui/src/components/UpdatePasswordDialog.vue | 5 ++++- agate-ui/src/components/UserDialog.vue | 12 ++++++++++-- agate-ui/src/components/UsersTable.vue | 4 ++-- agate-ui/src/i18n/en/index.js | 1 + agate-ui/src/i18n/fr/index.js | 3 ++- 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/agate-ui/src/components/UpdatePasswordDialog.vue b/agate-ui/src/components/UpdatePasswordDialog.vue index 9c7a3b15..f21ebe28 100644 --- a/agate-ui/src/components/UpdatePasswordDialog.vue +++ b/agate-ui/src/components/UpdatePasswordDialog.vue @@ -74,7 +74,7 @@ const userStore = useUserStore(); interface DialogProps { modelValue: boolean; - user: UserDto; + user: UserDto | undefined; } const props = defineProps(); @@ -103,6 +103,9 @@ function onCancel() { } function onSave() { + if (!props.user) { + return; + } userStore .updatePassword(props.user, password.value) .then(() => { diff --git a/agate-ui/src/components/UserDialog.vue b/agate-ui/src/components/UserDialog.vue index d8b53c6b..43d670fa 100644 --- a/agate-ui/src/components/UserDialog.vue +++ b/agate-ui/src/components/UserDialog.vue @@ -193,14 +193,22 @@ const systemStore = useSystemStore(); interface DialogProps { modelValue: boolean; - user: UserDto; + user: UserDto | undefined; } const props = defineProps(); const emit = defineEmits(['update:modelValue', 'saved', 'cancel']); const showDialog = ref(props.modelValue); -const selected = ref(props.user); +const selected = ref( + props.user ?? + ({ + realm: 'agate-user-realm', + role: 'agate-user', + status: 'INACTIVE', + preferredLanguage: '', + } as UserDto), +); const editMode = ref(false); const password = ref(''); const passwordVisible = ref(false); diff --git a/agate-ui/src/components/UsersTable.vue b/agate-ui/src/components/UsersTable.vue index 4a0cde70..9385ac7c 100644 --- a/agate-ui/src/components/UsersTable.vue +++ b/agate-ui/src/components/UsersTable.vue @@ -45,8 +45,8 @@ flat size="sm" color="secondary" - :title="t('user.more_actions')" - :icon="toolsVisible[props.row.name] ? 'lock' : 'none'" + :title="t('more_actions')" + :icon="toolsVisible[props.row.name] ? 'more_vert' : 'none'" class="q-ml-xs" > diff --git a/agate-ui/src/i18n/en/index.js b/agate-ui/src/i18n/en/index.js index c71b1ebb..fa5c7c87 100644 --- a/agate-ui/src/i18n/en/index.js +++ b/agate-ui/src/i18n/en/index.js @@ -91,6 +91,7 @@ export default { groups: 'Groups', help: 'Help', inherited_from: 'Inherited from: {parent}', + more_actions: 'More actions', my_profile: 'My Profile', name_hint: 'Name must be unique', name_min_length: 'Name must be at least {min} characters', diff --git a/agate-ui/src/i18n/fr/index.js b/agate-ui/src/i18n/fr/index.js index a12d977c..c7094fde 100644 --- a/agate-ui/src/i18n/fr/index.js +++ b/agate-ui/src/i18n/fr/index.js @@ -29,7 +29,7 @@ export default { language: 'Langue', lastName: 'Nom de famille', otp_disabled: '2FA désactivé', - otp_disable_error: "Échec de la désactivation du 2FA", + otp_disable_error: 'Échec de la désactivation du 2FA', password: 'Mot de passe', password_copied: 'Mot de passe copié', password_hint: 'Le mot de passe doit contenir au moins un chiffre, une lettre majuscule, une lettre minuscule, un caractère spécial (qui inclut @#$%^&+=!) et aucun espace blanc.', @@ -91,6 +91,7 @@ export default { groups: 'Groupes', help: 'Aide', inherited_from: 'Hérité de: {parent}', + more_actions: 'Plus d’actions', my_profile: 'Mon profil', name_hint: 'Le nom doit être unique', name_min_length: 'Le nom doit comporter au moins {min} caractères',