mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-18 02:21:47 -05:00
Switched editor re-authenticate modal to new modal pattern
refs https://github.com/TryGhost/Team/issues/1734 refs https://github.com/TryGhost/Team/issues/559 refs https://github.com/TryGhost/Ghost/issues/14101 - switches to newer modal patterns ready for later Ember upgrades - simplified unauthed save behaviour because we now have a promise for the modal enabling us to wait for the modal to close before continuing
This commit is contained in:
parent
f74913cfb6
commit
332dd4fbf1
9 changed files with 153 additions and 168 deletions
ghost/admin
app
components
controllers
routes
templates
tests/acceptance
37
ghost/admin/app/components/editor/modals/re-authenticate.hbs
Normal file
37
ghost/admin/app/components/editor/modals/re-authenticate.hbs
Normal file
|
@ -0,0 +1,37 @@
|
|||
<div class="modal-content" data-test-modal="re-authenticate">
|
||||
<header class="modal-header">
|
||||
<h1>Please re-authenticate</h1>
|
||||
</header>
|
||||
<button type="button" class="close" title="Close" {{on "click" @close}}>{{svg-jar "close"}}<span class="hidden">Close</span></button>
|
||||
|
||||
<div class="modal-body {{if this.authenticationError 'error'}}">
|
||||
<form id="login" class="login-form" method="post" novalidate="novalidate" {{on "submit" (perform this.reauthenticateTask)}}>
|
||||
<GhValidationStatusContainer @class="password-wrap" @errors={{this.signup.errors}} @property="password" @hasValidated={{this.signup.hasValidated}}>
|
||||
<input
|
||||
type="password"
|
||||
class="gh-input password"
|
||||
placeholder="Password"
|
||||
name="password"
|
||||
value={{this.signup.password}}
|
||||
aria-label="Your password"
|
||||
{{on "input" this.setPassword}}
|
||||
/>
|
||||
</GhValidationStatusContainer>
|
||||
|
||||
<div>
|
||||
<GhTaskButton
|
||||
@type="submit"
|
||||
@buttonText="Log in"
|
||||
@runningText="Authenticating"
|
||||
@showSuccess={{false}}
|
||||
@task={{this.reauthenticateTask}}
|
||||
@class="gh-btn gh-btn-black gh-btn-icon"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{#if this.authenticationError}}
|
||||
<p class="response">{{this.authenticationError}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
93
ghost/admin/app/components/editor/modals/re-authenticate.js
Normal file
93
ghost/admin/app/components/editor/modals/re-authenticate.js
Normal file
|
@ -0,0 +1,93 @@
|
|||
import Component from '@glimmer/component';
|
||||
import EmberObject from '@ember/object';
|
||||
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
|
||||
import classic from 'ember-classic-decorator';
|
||||
import {action} from '@ember/object';
|
||||
import {htmlSafe} from '@ember/template';
|
||||
import {isVersionMismatchError} from 'ghost-admin/services/ajax';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
// EmberObject is still needed here for ValidationEngine
|
||||
@classic
|
||||
class Signin extends EmberObject.extend(ValidationEngine) {
|
||||
@tracked identification = '';
|
||||
@tracked password = '';
|
||||
|
||||
validationType = 'signin';
|
||||
}
|
||||
|
||||
export default class ReAuthenticateModal extends Component {
|
||||
@service notifications;
|
||||
@service session;
|
||||
|
||||
@tracked authenticationError = null;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.signin = Signin.create();
|
||||
this.signin.identification = this.session.user.email;
|
||||
}
|
||||
|
||||
@action
|
||||
setPassword(event) {
|
||||
this.signin.password = event.target.value;
|
||||
}
|
||||
|
||||
@task({drop: true})
|
||||
*reauthenticateTask() {
|
||||
// Manually trigger events for input fields, ensuring legacy compatibility with
|
||||
// browsers and password managers that don't send proper events on autofill
|
||||
const inputs = document.querySelectorAll('#login input');
|
||||
inputs.forEach(input => input.dispatchEvent(new Event('change')));
|
||||
|
||||
this.authenticationError = null;
|
||||
|
||||
try {
|
||||
yield this.signin.validate({property: 'signin'});
|
||||
} catch (error) {
|
||||
this.signin.hasValidated.pushObject('password');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
yield this._authenticate();
|
||||
this.notifications.closeAlerts();
|
||||
this.args.close();
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (!error) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (error?.payload?.errors) {
|
||||
error.payload.errors.forEach((err) => {
|
||||
if (isVersionMismatchError(err)) {
|
||||
return this.notifications.showAPIError(error);
|
||||
}
|
||||
err.message = htmlSafe(err.context || err.message);
|
||||
});
|
||||
|
||||
this.signin.errors.add('password', 'Incorrect password');
|
||||
this.signin.hasValidated.pushObject('password');
|
||||
this.authenticationError = error.payload.errors[0].message;
|
||||
}
|
||||
|
||||
this.notifications.showAPIError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async _authenticate() {
|
||||
const authStrategy = 'authenticator:cookie';
|
||||
const {identification, password} = this.signin;
|
||||
|
||||
this.session.skipAuthSuccessHandler = true;
|
||||
|
||||
try {
|
||||
await this.session.authenticate(authStrategy, identification, password);
|
||||
} finally {
|
||||
this.session.skipAuthSuccessHandler = undefined;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
<header class="modal-header">
|
||||
<h1>Please re-authenticate</h1>
|
||||
</header>
|
||||
<a class="close" href="" role="button" title="Close" {{action "closeModal"}}>{{svg-jar "close"}}<span class="hidden">Close</span></a>
|
||||
|
||||
<div class="modal-body {{if this.authenticationError 'error'}}">
|
||||
|
||||
<form id="login" class="login-form" method="post" novalidate="novalidate" {{action "confirm" on="submit"}}>
|
||||
<GhValidationStatusContainer @class="password-wrap" @errors={{this.errors}} @property="password" @hasValidated={{this.hasValidated}}>
|
||||
<GhTextInput
|
||||
@class="password"
|
||||
@type="password"
|
||||
@placeholder="Password"
|
||||
@name="password"
|
||||
@value={{readonly this.password}}
|
||||
@input={{action (mut this.password) value="target.value"}} />
|
||||
</GhValidationStatusContainer>
|
||||
<div>
|
||||
<GhTaskButton @buttonText="Log in" @task={{this.reauthenticate}} @class="gh-btn gh-btn-black gh-btn-icon" @type="submit" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{#if this.authenticationError}}
|
||||
<p class="response">{{this.authenticationError}}</p>
|
||||
{{/if}}
|
||||
</div>
|
|
@ -1,78 +0,0 @@
|
|||
import ModalComponent from 'ghost-admin/components/modal-base';
|
||||
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
|
||||
import {htmlSafe} from '@ember/template';
|
||||
import {inject} from 'ghost-admin/decorators/inject';
|
||||
import {isVersionMismatchError} from 'ghost-admin/services/ajax';
|
||||
import {reads} from '@ember/object/computed';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
|
||||
export default ModalComponent.extend(ValidationEngine, {
|
||||
notifications: service(),
|
||||
session: service(),
|
||||
|
||||
validationType: 'signin',
|
||||
|
||||
authenticationError: null,
|
||||
|
||||
config: inject(),
|
||||
|
||||
identification: reads('session.user.email'),
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
this.reauthenticate.perform();
|
||||
}
|
||||
},
|
||||
|
||||
_authenticate() {
|
||||
let session = this.session;
|
||||
let authStrategy = 'authenticator:cookie';
|
||||
let identification = this.identification;
|
||||
let password = this.password;
|
||||
|
||||
session.set('skipAuthSuccessHandler', true);
|
||||
|
||||
this.toggleProperty('submitting');
|
||||
|
||||
return session.authenticate(authStrategy, identification, password).finally(() => {
|
||||
this.toggleProperty('submitting');
|
||||
session.set('skipAuthSuccessHandler', undefined);
|
||||
});
|
||||
},
|
||||
|
||||
_passwordConfirm() {
|
||||
// Manually trigger events for input fields, ensuring legacy compatibility with
|
||||
// browsers and password managers that don't send proper events on autofill
|
||||
const inputs = document.querySelectorAll('#login input');
|
||||
inputs.forEach(input => input.dispatchEvent(new Event('change')));
|
||||
|
||||
this.set('authenticationError', null);
|
||||
|
||||
return this.validate({property: 'signin'}).then(() => this._authenticate().then(() => {
|
||||
this.notifications.closeAlerts();
|
||||
this.send('closeModal');
|
||||
return true;
|
||||
}).catch((error) => {
|
||||
if (error && error.payload && error.payload.errors) {
|
||||
error.payload.errors.forEach((err) => {
|
||||
if (isVersionMismatchError(err)) {
|
||||
return this.notifications.showAPIError(error);
|
||||
}
|
||||
err.message = htmlSafe(err.context || err.message);
|
||||
});
|
||||
|
||||
this.errors.add('password', 'Incorrect password');
|
||||
this.hasValidated.pushObject('password');
|
||||
this.set('authenticationError', error.payload.errors[0].message);
|
||||
}
|
||||
}), () => {
|
||||
this.hasValidated.pushObject('password');
|
||||
return false;
|
||||
});
|
||||
},
|
||||
|
||||
reauthenticate: task(function* () {
|
||||
return yield this._passwordConfirm();
|
||||
}).drop()
|
||||
});
|
|
@ -4,6 +4,7 @@ import DeletePostModal from '../components/modals/delete-post';
|
|||
import DeleteSnippetModal from '../components/editor/modals/delete-snippet';
|
||||
import PostModel from 'ghost-admin/models/post';
|
||||
import PublishLimitModal from '../components/modals/limits/publish-limit';
|
||||
import ReAuthenticateModal from '../components/editor/modals/re-authenticate';
|
||||
import UpdateSnippetModal from '../components/editor/modals/update-snippet';
|
||||
import boundOneWay from 'ghost-admin/utils/bound-one-way';
|
||||
import classic from 'ember-classic-decorator';
|
||||
|
@ -112,7 +113,6 @@ export default class EditorController extends Controller {
|
|||
/* public properties -----------------------------------------------------*/
|
||||
|
||||
shouldFocusTitle = false;
|
||||
showReAuthenticateModal = false;
|
||||
showSettingsMenu = false;
|
||||
|
||||
/**
|
||||
|
@ -127,7 +127,6 @@ export default class EditorController extends Controller {
|
|||
|
||||
_leaveConfirmed = false;
|
||||
_previousTagNames = null; // set by setPost and _postSaved, used in hasDirtyAttributes
|
||||
_reAuthenticateModalToggle = false;
|
||||
|
||||
/* computed properties ---------------------------------------------------*/
|
||||
|
||||
|
@ -266,22 +265,6 @@ export default class EditorController extends Controller {
|
|||
}
|
||||
}
|
||||
|
||||
@action
|
||||
toggleReAuthenticateModal() {
|
||||
this._reAuthenticateModalToggle = true;
|
||||
|
||||
if (this.showReAuthenticateModal) {
|
||||
// closing, re-attempt save if needed
|
||||
if (this._reauthSave) {
|
||||
this.saveTask.perform(this._reauthSaveOptions);
|
||||
}
|
||||
|
||||
this._reauthSave = false;
|
||||
this._reauthSaveOptions = null;
|
||||
}
|
||||
this.toggleProperty('showReAuthenticateModal');
|
||||
}
|
||||
|
||||
@action
|
||||
openUpgradeModal(hostLimitError = {}) {
|
||||
this.modals.open(PublishLimitModal, {
|
||||
|
@ -467,15 +450,13 @@ export default class EditorController extends Controller {
|
|||
|
||||
return post;
|
||||
} catch (error) {
|
||||
if (!this.session.isAuthenticated && !this._reAuthenticateModalToggle) {
|
||||
this.toggleProperty('showReAuthenticateModal');
|
||||
}
|
||||
if (!this.session.isAuthenticated) {
|
||||
yield this.modals.open(ReAuthenticateModal);
|
||||
|
||||
this._reAuthenticateModalToggle = false;
|
||||
if (this.showReAuthenticateModal) {
|
||||
this._reauthSave = true;
|
||||
this._reauthSaveOptions = options;
|
||||
return;
|
||||
if (this.session.isAuthenticated) {
|
||||
this.saveTask.perform(options);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.set('post.status', prevStatus);
|
||||
|
|
|
@ -4,6 +4,7 @@ import DeletePostModal from '../components/modals/delete-post';
|
|||
import DeleteSnippetModal from '../components/editor/modals/delete-snippet';
|
||||
import PostModel from 'ghost-admin/models/post';
|
||||
import PublishLimitModal from '../components/modals/limits/publish-limit';
|
||||
import ReAuthenticateModal from '../components/editor/modals/re-authenticate';
|
||||
import UpdateSnippetModal from '../components/editor/modals/update-snippet';
|
||||
import boundOneWay from 'ghost-admin/utils/bound-one-way';
|
||||
import classic from 'ember-classic-decorator';
|
||||
|
@ -112,7 +113,6 @@ export default class LexicalEditorController extends Controller {
|
|||
/* public properties -----------------------------------------------------*/
|
||||
|
||||
shouldFocusTitle = false;
|
||||
showReAuthenticateModal = false;
|
||||
showSettingsMenu = false;
|
||||
|
||||
/**
|
||||
|
@ -127,7 +127,6 @@ export default class LexicalEditorController extends Controller {
|
|||
|
||||
_leaveConfirmed = false;
|
||||
_previousTagNames = null; // set by setPost and _postSaved, used in hasDirtyAttributes
|
||||
_reAuthenticateModalToggle = false;
|
||||
|
||||
/* computed properties ---------------------------------------------------*/
|
||||
|
||||
|
@ -266,22 +265,6 @@ export default class LexicalEditorController extends Controller {
|
|||
}
|
||||
}
|
||||
|
||||
@action
|
||||
toggleReAuthenticateModal() {
|
||||
this._reAuthenticateModalToggle = true;
|
||||
|
||||
if (this.showReAuthenticateModal) {
|
||||
// closing, re-attempt save if needed
|
||||
if (this._reauthSave) {
|
||||
this.saveTask.perform(this._reauthSaveOptions);
|
||||
}
|
||||
|
||||
this._reauthSave = false;
|
||||
this._reauthSaveOptions = null;
|
||||
}
|
||||
this.toggleProperty('showReAuthenticateModal');
|
||||
}
|
||||
|
||||
@action
|
||||
openUpgradeModal(hostLimitError = {}) {
|
||||
this.modals.open(PublishLimitModal, {
|
||||
|
@ -468,15 +451,13 @@ export default class LexicalEditorController extends Controller {
|
|||
|
||||
return post;
|
||||
} catch (error) {
|
||||
if (!this.session.isAuthenticated && !this._reAuthenticateModalToggle) {
|
||||
this.toggleProperty('showReAuthenticateModal');
|
||||
}
|
||||
if (!this.session.isAuthenticated) {
|
||||
yield this.modals.open(ReAuthenticateModal);
|
||||
|
||||
this._reAuthenticateModalToggle = false;
|
||||
if (this.showReAuthenticateModal) {
|
||||
this._reauthSave = true;
|
||||
this._reauthSaveOptions = options;
|
||||
return;
|
||||
if (this.session.isAuthenticated) {
|
||||
this.saveTask.perform(options);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.set('post.status', prevStatus);
|
||||
|
|
|
@ -38,7 +38,8 @@ export default AuthenticatedRoute.extend({
|
|||
},
|
||||
|
||||
authorizationFailed() {
|
||||
this.controller.send('toggleReAuthenticateModal');
|
||||
// noop - re-auth is handled by controller save
|
||||
return;
|
||||
},
|
||||
|
||||
willTransition(transition) {
|
||||
|
|
|
@ -120,12 +120,6 @@
|
|||
<span>{{svg-jar "sidemenu"}}</span>
|
||||
{{/if}}
|
||||
</button>
|
||||
|
||||
{{#if this.showReAuthenticateModal}}
|
||||
<GhFullscreenModal @modal="re-authenticate"
|
||||
@close={{this.toggleReAuthenticateModal}}
|
||||
@modifier="action wide" />
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{outlet}}
|
||||
|
|
|
@ -3,7 +3,7 @@ import windowProxy from 'ghost-admin/utils/window-proxy';
|
|||
import {Response} from 'miragejs';
|
||||
import {afterEach, beforeEach, describe, it} from 'mocha';
|
||||
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
|
||||
import {currentRouteName, currentURL, fillIn, findAll, triggerKeyEvent, visit} from '@ember/test-helpers';
|
||||
import {click, currentRouteName, currentURL, fillIn, findAll, triggerKeyEvent, visit, waitFor} from '@ember/test-helpers';
|
||||
import {expect} from 'chai';
|
||||
import {run} from '@ember/runloop';
|
||||
import {setupApplicationTest} from 'ember-mocha';
|
||||
|
@ -115,7 +115,6 @@ describe('Acceptance: Authentication', function () {
|
|||
});
|
||||
});
|
||||
|
||||
// TODO: re-enable once modal reappears correctly
|
||||
describe('editor', function () {
|
||||
let origDebounce = run.debounce;
|
||||
let origThrottle = run.throttle;
|
||||
|
@ -160,20 +159,23 @@ describe('Acceptance: Authentication', function () {
|
|||
});
|
||||
|
||||
// we shouldn't have a modal at this point
|
||||
expect(findAll('.modal-container #login').length, 'modal exists').to.equal(0);
|
||||
expect(findAll('[data-test-modal="re-authenticate"]').length, 'modal exists').to.equal(0);
|
||||
// we also shouldn't have any alerts
|
||||
expect(findAll('.gh-alert').length, 'no of alerts').to.equal(0);
|
||||
|
||||
// update the post
|
||||
testOn = 'edit';
|
||||
await fillIn('.__mobiledoc-editor', 'Edited post body');
|
||||
await triggerKeyEvent('.gh-editor-title', 'keydown', 83, {
|
||||
triggerKeyEvent('.gh-editor-title', 'keydown', 83, {
|
||||
metaKey: ctrlOrCmd === 'command',
|
||||
ctrlKey: ctrlOrCmd === 'ctrl'
|
||||
});
|
||||
|
||||
// we should see a re-auth modal
|
||||
expect(findAll('.fullscreen-modal #login').length, 'modal exists').to.equal(1);
|
||||
await waitFor('[data-test-modal="re-authenticate"]', {timeout: 100});
|
||||
|
||||
// close the modal so the modal promise is settled and we can continue
|
||||
await click('[data-test-modal="re-authenticate"] button[title="Close"]');
|
||||
});
|
||||
|
||||
// don't clobber debounce/throttle for future tests
|
||||
|
|
Loading…
Add table
Reference in a new issue