added cache to github card component, add password verification for changing email or password, ensured reset password link is is invalidated when expired or deleted

This commit is contained in:
Ronald A. Richardson
2024-04-02 17:09:10 +08:00
parent 39d405cb57
commit 6cce6a9db2
13 changed files with 212 additions and 159 deletions

View File

@@ -0,0 +1 @@
<div id="console-wormhole" />

View File

@@ -1,6 +1,6 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action, computed } from '@ember/object';
import { computed } from '@ember/object';
import { isArray } from '@ember/array';
import { isBlank } from '@ember/utils';
import { task } from 'ember-concurrency';

View File

@@ -0,0 +1,13 @@
<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
<div class="px-5">
{{#if @options.body}}
<p class="dark:text-gray-400 text-gray-700 mb-4">{{@options.body}}</p>
{{/if}}
<InputGroup>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<InputGroup @name="Password" @type="password" @value={{this.password}} @wrapperClass="mb-0i" />
<InputGroup @name="Confirm Password" @type="password" @value={{this.confirmPassword}} @wrapperClass="mb-0i" />
</div>
</InputGroup>
</div>
</Modal::Default>

View File

@@ -0,0 +1,49 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { task } from 'ember-concurrency';
export default class ModalsValidatePasswordComponent extends Component {
@service fetch;
@service notifications;
@tracked options = {};
@tracked password;
@tracked confirmPassword;
constructor(owner, { options }) {
super(...arguments);
this.options = options;
this.setupOptions();
}
setupOptions() {
this.options.title = 'Validate Current Password';
this.options.acceptButtonText = 'Validate Password';
this.options.declineButtonHidden = true;
this.options.confirm = (modal) => {
modal.startLoading();
return this.validatePassword.perform();
};
}
@task *validatePassword() {
let isPasswordValid = false;
try {
yield this.fetch.post('users/validate-password', {
password: this.password,
password_confirmation: this.confirmPassword,
});
isPasswordValid = true;
} catch (error) {
this.notifications.serverError(error, 'Invalid current password.');
}
if (typeof this.options.onValidated === 'function') {
this.options.onValidated(isPasswordValid);
}
return isPasswordValid;
}
}

View File

@@ -1,7 +1,7 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { task } from 'ember-concurrency';
export default class AuthResetPasswordController extends Controller {
/**
@@ -54,38 +54,23 @@ export default class AuthResetPasswordController extends Controller {
@tracked password_confirmation;
/**
* Loading stae of password reset.
* The reset password task.
*
* @memberof AuthResetPasswordController
*/
@tracked isLoading;
/**
* The reset password action.
*
* @memberof AuthResetPasswordController
*/
@action resetPassword(event) {
// firefox patch
@task *resetPassword(event) {
event.preventDefault();
const { code, password, password_confirmation } = this;
const { id } = this.model;
this.isLoading = true;
try {
yield this.fetch.post('auth/reset-password', { link: id, code, password, password_confirmation });
} catch (error) {
return this.notifications.serverError(error);
}
this.fetch
.post('auth/reset-password', { link: id, code, password, password_confirmation })
.then(() => {
this.notifications.success(this.intl.t('auth.reset-password.success-message'));
return this.router.transitionTo('auth.login');
})
.catch((error) => {
this.notifications.serverError(error);
})
.finally(() => {
this.isLoading = false;
});
this.notifications.success(this.intl.t('auth.reset-password.success-message'));
yield this.router.transitionTo('auth.login');
}
}

View File

@@ -34,17 +34,11 @@ export default class ConsoleAccountAuthController extends Controller {
@service router;
/**
* The user's current password.
* @type {string}
*/
@tracked password;
/**
* The user's confirmation of the new password.
* Service for managing modals.
*
* @type {string}
* @type {router}
*/
@tracked confirmPassword;
@service modalsManager;
/**
* The new password the user intends to set.
@@ -60,13 +54,6 @@ export default class ConsoleAccountAuthController extends Controller {
*/
@tracked newConfirmPassword;
/**
* Flag indicating whether the current password has been validated.
*
* @type {boolean}
*/
@tracked isPasswordValidated = false;
/**
* System-wide two-factor authentication configuration.
*
@@ -106,28 +93,6 @@ export default class ConsoleAccountAuthController extends Controller {
this.loadUserTwoFaSettings.perform();
}
/**
* Validates the user's current password.
*
* @method validatePassword
* @param {Event} event - The event object triggering the action.
*/
@action validatePassword(event) {
event.preventDefault();
this.validatePasswordTask.perform();
}
/**
* Initiates the task to change the user's password asynchronously.
*
* @method changeUserPasswordTask
* @param {Event} event - The event object triggering the action.
*/
@action changeUserPassword(event) {
event.preventDefault();
this.changeUserPasswordTask.perform();
}
/**
* Handles the event when two-factor authentication is toggled.
*
@@ -163,6 +128,58 @@ export default class ConsoleAccountAuthController extends Controller {
this.saveUserTwoFaSettings.perform(this.twoFaSettings);
}
/**
* Initiates the task to change the user's password asynchronously.
*
* @method changePassword
*/
@task *changePassword(event) {
// If from event fired
if (event instanceof Event) {
event.preventDefault();
}
// Validate current password
const isPasswordValid = yield this.validatePassword.perform();
if (!isPasswordValid) {
this.newPassword = undefined;
this.newConfirmPassword = undefined;
return;
}
try {
yield this.fetch.post('users/change-password', {
password: this.newPassword,
password_confirmation: this.newConfirmPassword,
});
this.notifications.success('Password change successfully.');
} catch (error) {
this.notifications.serverError(error, 'Failed to change password.');
}
this.newPassword = undefined;
this.newConfirmPassword = undefined;
}
/**
* Task to validate current password
*
* @return {boolean}
*/
@task *validatePassword() {
let isPasswordValid = false;
yield this.modalsManager.show('modals/validate-password', {
body: 'You must validate your current password before it can be changed.',
onValidated: (isValid) => {
isPasswordValid = isValid;
},
});
return isPasswordValid;
}
/**
* Initiates the task to save user-specific two-factor authentication settings asynchronously.
*
@@ -209,40 +226,4 @@ export default class ConsoleAccountAuthController extends Controller {
}
return twoFaConfig;
}
/**
* Initiates the task to validate the user's current password asynchronously.
*
* @method validatePasswordTask
*/
@task *validatePasswordTask() {
try {
yield this.fetch.post('users/validate-password', {
password: this.password,
password_confirmation: this.confirmPassword,
});
this.isPasswordValidated = true;
} catch (error) {
this.notifications.serverError(error, 'Invalid current password.');
}
}
/**
* Initiates the task to change the user's password asynchronously.
*
* @method changeUserPasswordTask
*/
@task *changeUserPasswordTask() {
try {
yield this.fetch.post('users/change-password', {
password: this.newPassword,
password_confirmation: this.newConfirmPassword,
});
this.notifications.success('Password change successfully.');
} catch (error) {
this.notifications.error('Failed to change password');
}
}
}

View File

@@ -1,8 +1,8 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { alias } from '@ember/object/computed';
import { task } from 'ember-concurrency';
export default class ConsoleAccountIndexController extends Controller {
/**
@@ -18,6 +18,7 @@ export default class ConsoleAccountIndexController extends Controller {
* @memberof ConsoleAccountIndexController
*/
@service fetch;
/**
* Inject the `notifications` service.
*
@@ -26,11 +27,11 @@ export default class ConsoleAccountIndexController extends Controller {
@service notifications;
/**
* The loading state of request.
* Inject the `modalsManager` service.
*
* @memberof ConsoleAccountIndexController
*/
@tracked isLoading = false;
@service modalsManager;
/**
* Alias to the currentUser service user record.
@@ -66,27 +67,64 @@ export default class ConsoleAccountIndexController extends Controller {
}
/**
* Save the Profile settings.
* Starts the task to change password
*
* @return {Promise}
* @param {Event} event
* @memberof ConsoleAccountIndexController
*/
@action saveProfile() {
const user = this.user;
@task *saveProfile(event) {
// If from event fired
if (event instanceof Event) {
event.preventDefault();
}
this.isLoading = true;
let canUpdateProfile = true;
// If email has been changed prompt for password validation
if (this.changedUserAttribute('email')) {
canUpdateProfile = yield this.validatePassword.perform();
}
return user
.save()
.then((user) => {
if (canUpdateProfile === true) {
try {
const user = yield this.user.save();
this.notifications.success('Profile changes saved.');
this.currentUser.set('user', user);
})
.catch((error) => {
} catch (error) {
this.notifications.serverError(error);
})
.finally(() => {
this.isLoading = false;
});
}
} else {
this.user.rollbackAttributes();
}
}
/**
* Task to validate current password
*
* @return {boolean}
* @memberof ConsoleAccountIndexController
*/
@task *validatePassword() {
let isPasswordValid = false;
yield this.modalsManager.show('modals/validate-password', {
body: 'You must validate your password to update the account email address.',
onValidated: (isValid) => {
isPasswordValid = isValid;
},
});
return isPasswordValid;
}
/**
* Checks if any user attribute has been changed
*
* @param {string} attributeKey
* @return {boolean}
* @memberof ConsoleAccountIndexController
*/
changedUserAttribute(attributeKey) {
const changedAttributes = this.user.changedAttributes();
return changedAttributes[attributeKey] !== undefined;
}
}

View File

@@ -3,13 +3,21 @@ import { inject as service } from '@ember/service';
export default class AuthResetPasswordRoute extends Route {
@service store;
@service fetch;
@service router;
@service notifications;
@service intl;
async model(params) {
return params;
async model({ id }) {
return this.fetch.get('auth/validate-verification', { id });
}
async setupController(controller) {
async setupController(controller, model) {
super.setupController(...arguments);
if (model.is_valid === false) {
this.notifications.warning(this.intl.t('auth.reset-password.invalid-verification-code'));
return this.router.transitionTo('auth');
}
// set brand to controller
controller.brand = await this.store.findRecord('brand', 1);

View File

@@ -6,14 +6,14 @@
</h2>
</div>
<form class="space-y-6" {{on "submit" this.resetPassword}}>
<form class="space-y-6" {{on "submit" (perform this.resetPassword)}}>
<InputGroup @name={{t "auth.reset-password.form.code.label"}} @value={{this.code}} @inputClass="form-input-lg" @helpText={{t "auth.reset-password.form.code.help-text"}} />
<InputGroup @name={{t "auth.reset-password.form.password.label"}} @value={{this.password}} @type="password" @inputClass="form-input-lg" @helpText={{t "auth.reset-password.form.password.help-text"}} />
<InputGroup @name={{t "auth.reset-password.form.confirm-password.label"}} @value={{this.password_confirmation}} @type="password" @inputClass="form-input-lg" @helpText={{t "auth.reset-password.form.confirm-password.help-text"}} />
<div class="flex space-x-2">
<Button @icon="check" @size="lg" @type="primary" @buttonType="submit" @text={{t "auth.reset-password.form.submit-button"}} @onClick={{this.resetPassword}} @isLoading={{this.isLoading}} />
<Button @size="lg" @buttonType="button" @text={{t "auth.reset-password.form.back-button"}} @onClick={{fn (transition-to "auth.login")}} @disabled={{this.isLoading}} />
<Button @icon="check" @size="lg" @type="primary" @buttonType="submit" @text={{t "auth.reset-password.form.submit-button"}} @onClick={{perform this.resetPassword}} @isLoading={{not this.resetPassword.isIdle}} />
<Button @size="lg" @buttonType="button" @text={{t "auth.reset-password.form.back-button"}} @onClick={{fn (transition-to "auth.login")}} @disabled={{not this.resetPassword.isIdle}} />
</div>
</form>
</div>

View File

@@ -19,4 +19,4 @@
<EmberWormhole @to="view-header-actions">
<LocaleSelector class="mr-0.5" />
</EmberWormhole>
<div id="console-wormhole" />
<ConsoleWormhole />

View File

@@ -5,51 +5,28 @@
<div class="container mx-auto h-screen" {{increase-height-by 500}}>
<div class="max-w-3xl my-10 mx-auto space-y-6">
<ContentPanel @title="Change Password" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
{{#if this.isPasswordValidated}}
<form id="change-password-form" aria-label="change-password" {{on "submit" this.changeUserPassword}}>
<legend class="mb-3">Change Password</legend>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<InputGroup @name="Enter new Password" @type="password" @value={{this.newPassword}} />
<InputGroup @name="Confirm Password" @type="password" @value={{this.newConfirmPassword}} />
</div>
<Button @type="primary" @buttonType="submit" @text="Confirm & Change Password" @icon="save" {{on "click" this.changeUserPassword}} />
</form>
{{else}}
<form id="validate-password-form" aria-label="validate-password" {{on "submit" this.validatePassword}}>
<legend class="mb-3">Validate Current Password</legend>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<InputGroup @name="Password" @type="password" @value={{this.password}} />
<InputGroup @name="Confirm Password" @type="password" @value={{this.confirmPassword}} />
</div>
<Button @type="primary" @buttonType="submit" @text="Continue" @icon="check" {{on "click" this.validatePassword}} />
</form>
{{/if}}
<form id="change-password-form" aria-label="change-password" {{on "submit" (perform this.changePassword)}}>
<legend class="mb-3">Change Password</legend>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<InputGroup @name="Enter new Password" @type="password" @value={{this.newPassword}} />
<InputGroup @name="Confirm Password" @type="password" @value={{this.newConfirmPassword}} />
</div>
<Button @type="primary" @buttonType="submit" @text="Confirm & Change Password" @icon="save" {{on "click" (perform this.changePassword)}} />
</form>
</ContentPanel>
{{#if this.isSystemTwoFaEnabled}}
<ContentPanel @title="2FA Settings" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<div class="mb-3">
{{#if this.loadUserTwoFaSettings.isIdle}}
<TwoFaSettings
@twoFaMethods={{this.methods}}
@twoFaSettings={{this.twoFaSettings}}
@onTwoFaToggled={{this.onTwoFaToggled}}
@onTwoFaMethodSelected={{this.onTwoFaMethodSelected}}
/>
<TwoFaSettings @twoFaMethods={{this.methods}} @twoFaSettings={{this.twoFaSettings}} @onTwoFaToggled={{this.onTwoFaToggled}} @onTwoFaMethodSelected={{this.onTwoFaMethodSelected}} />
{{else}}
<div class="flex items-center justify-center p-4">
<Spinner @loadingMessage="Loading User 2FA Settings..." @wrapperClass="flex flex-row" @iconClass="mr-2" />
</div>
{{/if}}
</div>
<Button
@type="primary"
@buttonType="submit"
@text="Save 2FA Settings"
@icon="save"
@onClick={{this.saveTwoFactorAuthSettings}}
@isLoading={{this.saveUserTwoFaSettings.isRunning}}
/>
<Button @type="primary" @buttonType="submit" @text="Save 2FA Settings" @icon="save" @onClick={{this.saveTwoFactorAuthSettings}} @isLoading={{this.saveUserTwoFaSettings.isRunning}} />
</ContentPanel>
{{/if}}
</div>

View File

@@ -4,7 +4,7 @@
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto">
<ContentPanel @title={{t "common.your-profile"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<form class="flex flex-col md:flex-row" {{on "submit" this.saveProfile}}>
<form class="flex flex-col md:flex-row" {{on "submit" (perform this.saveProfile)}}>
<div class="w-32 flex flex-col justify-center mb-6 mr-6">
<Image src={{this.user.avatar_url}} @fallbackSrc={{config "defaultValues.userImage"}} alt={{this.user.name}} class="w-32 h-32 rounded-md" />
<FileUpload @name={{t "console.account.index.photos"}} @accept="image/*" @onFileAdded={{this.uploadNewPhoto}} @labelClass="flex flex-row items-center justify-center" as |queue|>
@@ -35,7 +35,7 @@
<InputGroup @name={{t "common.date-of-birth"}} @type="date" @value={{this.user.date_of_birth}} />
</div>
<div class="mt-3 flex items-center justify-end">
<Button @buttonType="submit" @type="primary" @size="lg" @icon="save" @text={{t "common.save-button-text"}} @onClick={{this.saveProfile}} @isLoading={{this.isLoading}} />
<Button @buttonType="submit" @type="primary" @size="lg" @icon="save" @text={{t "common.save-button-text"}} @onClick={{perform this.saveProfile}} @isLoading={{not this.saveProfile.isIdle}} />
</div>
</div>
</form>

View File

@@ -179,6 +179,7 @@ auth:
slow-connection-message: Experiencing connectivity issues.
reset-password:
success-message: Your password has been reset! Login to continue.
invalid-verification-code: This reset password link is invalid or expired.
title: Reset your password
form:
code: