From 2a81acbdde62a2761e05b58e67ecd7413de07bd5 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Thu, 17 Oct 2024 16:49:10 +0800 Subject: [PATCH] added ability for administrators to change user passwords --- .../modals/change-user-password.hbs | 18 +++++++ .../components/modals/change-user-password.js | 49 +++++++++++++++++++ addon/controllers/roles/index.js | 30 +++++++++++- addon/controllers/users/index.js | 18 +++++++ addon/templates/application.hbs | 2 +- app/components/modals/change-user-password.js | 1 + package.json | 4 +- pnpm-lock.yaml | 10 ++-- .../modals/change-user-password-test.js | 26 ++++++++++ translations/en-us.yaml | 1 + 10 files changed, 150 insertions(+), 9 deletions(-) create mode 100644 addon/components/modals/change-user-password.hbs create mode 100644 addon/components/modals/change-user-password.js create mode 100644 app/components/modals/change-user-password.js create mode 100644 tests/integration/components/modals/change-user-password-test.js diff --git a/addon/components/modals/change-user-password.hbs b/addon/components/modals/change-user-password.hbs new file mode 100644 index 0000000..35cd4b2 --- /dev/null +++ b/addon/components/modals/change-user-password.hbs @@ -0,0 +1,18 @@ + +
+
+ You are about to reset the password for + {{this.user.name}} +
+
Please enter the new password and confirm it below. You have the option to send the new credentials to the user via email by selecting the checkbox.
+ +
+ + +
+
+ + + +
+
\ No newline at end of file diff --git a/addon/components/modals/change-user-password.js b/addon/components/modals/change-user-password.js new file mode 100644 index 0000000..ec96a57 --- /dev/null +++ b/addon/components/modals/change-user-password.js @@ -0,0 +1,49 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; + +export default class ModalsChangeUserPasswordComponent extends Component { + @service fetch; + @service notifications; + @tracked options = {}; + @tracked password; + @tracked confirmPassword; + @tracked sendCredentials = true; + @tracked user; + + constructor(owner, { options }) { + super(...arguments); + this.user = options.user; + this.options = options; + this.setupOptions(); + } + + setupOptions() { + this.options.title = 'Reset User Credentials'; + this.options.acceptButtonText = 'Reset Credentials'; + this.options.declineButtonHidden = true; + this.options.confirm = async (modal) => { + modal.startLoading(); + + try { + await this.fetch.post('auth/change-user-password', { + user: this.user.id, + password: this.password, + password_confirmation: this.confirmPassword, + send_credentials: this.sendCredentials, + }); + + this.notifications.success('User password reset.'); + + if (typeof this.options.onPasswordResetComplete === 'function') { + this.options.onPasswordResetComplete(); + } + + modal.done(); + } catch (error) { + this.notifications.serverError(error); + modal.stopLoading(); + } + }; + } +} diff --git a/addon/controllers/roles/index.js b/addon/controllers/roles/index.js index 418e5ec..230375b 100644 --- a/addon/controllers/roles/index.js +++ b/addon/controllers/roles/index.js @@ -204,7 +204,7 @@ export default class RolesIndexController extends Controller { */ @action createRole() { const formPermission = 'iam create role'; - const role = this.store.createRecord('role'); + const role = this.store.createRecord('role', { is_mutable: true }); this.editRole(role, { title: this.intl.t('iam.roles.index.new-role'), @@ -213,16 +213,30 @@ export default class RolesIndexController extends Controller { acceptButtonDisabled: this.abilities.cannot(formPermission), acceptButtonHelpText: this.abilities.cannot(formPermission) ? this.intl.t('common.unauthorized') : null, formPermission, + keepOpen: true, confirm: async (modal) => { modal.startLoading(); + if (!role.name || typeof role.name !== 'string') { + modal.stopLoading(); + return this.notifications.warning('Role name is required.'); + } + + const roleName = role.name.toLowerCase(); + if (roleName === 'administrator' || roleName.startsWith('admin')) { + modal.stopLoading(); + return this.notifications.error('Creating a role with name "Administrator" or a role name that starts with "Admin" is prohibited, as the name is system reserved.'); + } + if (this.abilities.cannot(formPermission)) { + modal.stopLoading(); return this.notifications.warning(this.intl.t('common.permissions-required-for-changes')); } try { await role.save(); this.notifications.success(this.intl.t('iam.roles.index.new-role-create')); + modal.done(); return this.hostRouter.refresh(); } catch (error) { this.notifications.serverError(error); @@ -249,6 +263,7 @@ export default class RolesIndexController extends Controller { acceptButtonIcon: 'save', acceptButtonDisabled: this.abilities.cannot(formPermission), acceptButtonHelpText: this.abilities.cannot(formPermission) ? this.intl.t('common.unauthorized') : null, + keepOpen: true, formPermission, role, setPermissions: (permissions) => { @@ -257,13 +272,26 @@ export default class RolesIndexController extends Controller { confirm: async (modal) => { modal.startLoading(); + if (!role.name || typeof role.name !== 'string') { + modal.stopLoading(); + return this.notifications.warning('Role name is required.'); + } + + const roleName = role.name.toLowerCase(); + if (roleName === 'administrator' || roleName.startsWith('admin')) { + modal.stopLoading(); + return this.notifications.error('Creating a role with name "Administrator" or a role name that starts with "Admin" is prohibited, as the name is system reserved.'); + } + if (this.abilities.cannot(formPermission)) { + modal.stopLoading(); return this.notifications.warning(this.intl.t('common.permissions-required-for-changes')); } try { await role.save(); this.notifications.success(this.intl.t('iam.roles.index.changes-role-saved')); + modal.done(); return this.hostRouter.refresh(); } catch (error) { this.notifications.serverError(error); diff --git a/addon/controllers/users/index.js b/addon/controllers/users/index.js index e694dca..6b4a266 100644 --- a/addon/controllers/users/index.js +++ b/addon/controllers/users/index.js @@ -185,6 +185,12 @@ export default class UsersIndexController extends Controller { permission: 'iam verify user', isVisible: (user) => !user.get('email_verified_at'), }, + { + label: this.intl.t('iam.users.index.change-user-password'), + fn: this.changeUserPassword, + className: 'text-danger', + isVisible: (user) => this.abilities.can('iam change-password-for user') || user.role_name === 'Administrator' || user.is_admin === true, + }, { label: this.intl.t('iam.users.index.delete-user'), fn: this.deleteUser, @@ -454,6 +460,18 @@ export default class UsersIndexController extends Controller { }); } + /** + * Change password for a user + * + * @void + */ + @action changeUserPassword(user) { + this.modalsManager.show('modals/change-user-password', { + keepOpen: true, + user, + }); + } + /** * Resends invite for a user to join. * diff --git a/addon/templates/application.hbs b/addon/templates/application.hbs index 15ca5ed..b2b1196 100644 --- a/addon/templates/application.hbs +++ b/addon/templates/application.hbs @@ -3,7 +3,7 @@ {{t "iam.common.dashboard"}} {{t "iam.common.user"}} {{t "iam.common.group"}} - {{t "iam.common.role"}} + {{t "iam.common.roles"}} {{t "iam.common.policies"}} diff --git a/app/components/modals/change-user-password.js b/app/components/modals/change-user-password.js new file mode 100644 index 0000000..33a9578 --- /dev/null +++ b/app/components/modals/change-user-password.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/iam-engine/components/modals/change-user-password'; diff --git a/package.json b/package.json index 0d058f0..7007720 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fleetbase/iam-engine", - "version": "0.1.2", + "version": "0.1.3", "description": "Fleetbase IAM extension provides identity and access management module for managing users, permissions and policies.", "fleetbase": { "route": "iam" @@ -42,7 +42,7 @@ }, "dependencies": { "@fleetbase/ember-core": "^0.2.21", - "@fleetbase/ember-ui": "^0.2.34", + "@fleetbase/ember-ui": "^0.2.35", "@babel/core": "^7.23.2", "ember-cli-babel": "^8.2.0", "ember-cli-htmlbars": "^6.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e934e1a..765022a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^0.2.21 version: 0.2.21(@ember/string@3.1.1)(@ember/test-helpers@3.2.0(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(webpack@5.89.0))(ember-resolver@11.0.1(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)))(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(webpack@5.89.0) '@fleetbase/ember-ui': - specifier: ^0.2.34 - version: 0.2.34(@ember/test-helpers@3.2.0(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(webpack@5.89.0))(@glimmer/component@1.1.2(@babel/core@7.23.2))(@glimmer/tracking@1.1.2)(ember-resolver@11.0.1(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)))(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(postcss@8.4.35)(rollup@2.79.1)(tracked-built-ins@3.3.0)(webpack@5.89.0) + specifier: ^0.2.35 + version: 0.2.35(@ember/test-helpers@3.2.0(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(webpack@5.89.0))(@glimmer/component@1.1.2(@babel/core@7.23.2))(@glimmer/tracking@1.1.2)(ember-resolver@11.0.1(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)))(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(postcss@8.4.35)(rollup@2.79.1)(tracked-built-ins@3.3.0)(webpack@5.89.0) '@fortawesome/ember-fontawesome': specifier: ^2.0.0 version: 2.0.0(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(rollup@2.79.1)(webpack@5.89.0) @@ -1454,8 +1454,8 @@ packages: resolution: {integrity: sha512-Jwe4ME+cJp24Oaf6wfd1QJK6g3D6/Ff+qUMinJ40yoMImpo3zKA+IAxfQSBPIf+9BrmNG31wquTz4WR7Um47LQ==} engines: {node: '>= 18'} - '@fleetbase/ember-ui@0.2.34': - resolution: {integrity: sha512-9uLCufgIaMFgGR+G4m73shaiRz+F16CDs7jj4jFzQGSUQByaBcbSetiMoGXSnx+3IncaKAn8rjAVPd+QrDDRMg==} + '@fleetbase/ember-ui@0.2.35': + resolution: {integrity: sha512-diJRlY92LTSFhf2bMlO2j8uyVabeTTC5YHuRqYYpTHk6GjLcVHUVeL4MrLnskKzkkbRsAgmoavkW9HcXNBN8Ow==} engines: {node: '>= 18'} '@fleetbase/intl-lint@0.0.1': @@ -10602,7 +10602,7 @@ snapshots: - utf-8-validate - webpack - '@fleetbase/ember-ui@0.2.34(@ember/test-helpers@3.2.0(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(webpack@5.89.0))(@glimmer/component@1.1.2(@babel/core@7.23.2))(@glimmer/tracking@1.1.2)(ember-resolver@11.0.1(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)))(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(postcss@8.4.35)(rollup@2.79.1)(tracked-built-ins@3.3.0)(webpack@5.89.0)': + '@fleetbase/ember-ui@0.2.35(@ember/test-helpers@3.2.0(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(webpack@5.89.0))(@glimmer/component@1.1.2(@babel/core@7.23.2))(@glimmer/tracking@1.1.2)(ember-resolver@11.0.1(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)))(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(postcss@8.4.35)(rollup@2.79.1)(tracked-built-ins@3.3.0)(webpack@5.89.0)': dependencies: '@babel/core': 7.25.2 '@ember/render-modifiers': 2.1.0(@babel/core@7.25.2)(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)) diff --git a/tests/integration/components/modals/change-user-password-test.js b/tests/integration/components/modals/change-user-password-test.js new file mode 100644 index 0000000..3636972 --- /dev/null +++ b/tests/integration/components/modals/change-user-password-test.js @@ -0,0 +1,26 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'dummy/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | modals/change-user-password', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.set('myAction', function(val) { ... }); + + await render(hbs``); + + assert.dom().hasText(''); + + // Template block usage: + await render(hbs` + + template block text + + `); + + assert.dom().hasText('template block text'); + }); +}); diff --git a/translations/en-us.yaml b/translations/en-us.yaml index 2e4d979..96eed54 100644 --- a/translations/en-us.yaml +++ b/translations/en-us.yaml @@ -144,6 +144,7 @@ iam: activate-user: Activate user... delete-user: Delete user... verify-user: Verify user... + change-user-password: Change user password... new-user: New User user-invited-join-your-organization-success: User has been invited to join your organization. edit-user-title: Edit User