diff --git a/ghost/admin/app/authenticators/cookie.js b/ghost/admin/app/authenticators/cookie.js index 32b9370809..bdc05c8b00 100644 --- a/ghost/admin/app/authenticators/cookie.js +++ b/ghost/admin/app/authenticators/cookie.js @@ -11,11 +11,27 @@ export default Authenticator.extend({ return `${this.ghostPaths.apiRoot}/session`; }), + sessionVerifyEndpoint: computed('ghostPaths.apiRoot', function () { + return `${this.ghostPaths.apiRoot}/session/verify`; + }), + restore: function () { return RSVP.resolve(); }, - authenticate(identification, password) { + authenticate({identification, password, token}) { + if (token) { + const data = {token}; + const options = { + data, + contentType: 'application/json;charset=utf-8', + // ember-ajax will try and parse the response as JSON if not explicitly set + dataType: 'text' + }; + + return this.ajax.put(this.sessionVerifyEndpoint, options); + } + const data = {username: identification, password}; const options = { data, diff --git a/ghost/admin/app/components/editor/modals/re-authenticate.js b/ghost/admin/app/components/editor/modals/re-authenticate.js index a0d33d5e69..db68c63047 100644 --- a/ghost/admin/app/components/editor/modals/re-authenticate.js +++ b/ghost/admin/app/components/editor/modals/re-authenticate.js @@ -87,13 +87,12 @@ export default class ReAuthenticateModal extends Component { } async _authenticate() { - const authStrategy = 'authenticator:cookie'; const {identification, password} = this.signin; this.session.skipAuthSuccessHandler = true; try { - await this.session.authenticate(authStrategy, identification, password); + await this.session.authenticate('authenticator:cookie', {identification, password}); } finally { this.session.skipAuthSuccessHandler = undefined; } diff --git a/ghost/admin/app/controllers/reset.js b/ghost/admin/app/controllers/reset.js index 2ce6090ee8..40f7609c1f 100644 --- a/ghost/admin/app/controllers/reset.js +++ b/ghost/admin/app/controllers/reset.js @@ -66,7 +66,7 @@ export default class ResetController extends Controller.extend(ValidationEngine) resp.password_reset[0].message, {type: 'info', delayed: true, key: 'password.reset'} ); - this.session.authenticate('authenticator:cookie', email, newPassword); + this.session.authenticate('authenticator:cookie', {identification: email, password: newPassword}); return true; } catch (error) { this.notifications.showAPIError(error, {key: 'password.reset'}); diff --git a/ghost/admin/app/controllers/setup.js b/ghost/admin/app/controllers/setup.js index 7a63fe71a8..eec7e6e84b 100644 --- a/ghost/admin/app/controllers/setup.js +++ b/ghost/admin/app/controllers/setup.js @@ -50,12 +50,12 @@ export default class SetupController extends Controller.extend(ValidationEngine) }) setupTask; - @task(function* (authStrategy, authentication) { + @task(function* (authStrategy, {identification, password}) { // we don't want to redirect after sign-in during setup this.session.skipAuthSuccessHandler = true; try { - yield this.session.authenticate(authStrategy, ...authentication); + yield this.session.authenticate(authStrategy, {identification, password}); this.errors.remove('session'); @@ -118,7 +118,7 @@ export default class SetupController extends Controller.extend(ValidationEngine) // Don't call the success handler, otherwise we will be redirected to admin this.session.skipAuthSuccessHandler = true; - return this.session.authenticate('authenticator:cookie', data.email, data.password).then(() => { + return this.session.authenticate('authenticator:cookie', {identification: data.email, password: data.password}).then(() => { this.set('blogCreated', true); return this._afterAuthentication(result); }).catch((error) => { diff --git a/ghost/admin/app/controllers/signin-verify.js b/ghost/admin/app/controllers/signin-verify.js new file mode 100644 index 0000000000..4c292f7ed2 --- /dev/null +++ b/ghost/admin/app/controllers/signin-verify.js @@ -0,0 +1,36 @@ +import Controller from '@ember/controller'; +import {action} from '@ember/object'; +import {inject as service} from '@ember/service'; +import {task} from 'ember-concurrency'; +import {tracked} from '@glimmer/tracking'; + +export default class SigninVerifyController extends Controller { + @service ajax; + @service session; + + @tracked flowErrors = ''; + @tracked token = ''; + + @action + validateToken() { + return true; + } + + @action + handleTokenInput(event) { + this.token = event.target.value; + } + + @task + *verifyTokenTask() { + try { + yield this.session.authenticate('authenticator:cookie', {token: this.token}); + } catch (error) { + if (error && error.payload && error.payload.errors) { + this.flowErrors = error.payload.errors[0].message; + } else { + this.flowErrors = 'There was a problem with the verification token.'; + } + } + } +} diff --git a/ghost/admin/app/controllers/signin.js b/ghost/admin/app/controllers/signin.js index 1c4531cf3a..0cc8a6d1e3 100644 --- a/ghost/admin/app/controllers/signin.js +++ b/ghost/admin/app/controllers/signin.js @@ -7,6 +7,7 @@ import {action} from '@ember/object'; import {htmlSafe} from '@ember/template'; import {inject} from 'ghost-admin/decorators/inject'; import {isArray as isEmberArray} from '@ember/array'; +import {isForbiddenError} from 'ember-ajax/errors'; import {isVersionMismatchError} from 'ghost-admin/services/ajax'; import {inject as service} from '@ember/service'; import {task} from 'ember-concurrency'; @@ -18,6 +19,7 @@ export default class SigninController extends Controller.extend(ValidationEngine @service ajax; @service ghostPaths; @service notifications; + @service router; @service session; @service settings; @@ -49,12 +51,17 @@ export default class SigninController extends Controller.extend(ValidationEngine } @task({drop: true}) - *authenticateTask(authStrategy, authentication) { + *authenticateTask(authStrategy, {identification, password}) { try { return yield this.session - .authenticate(authStrategy, ...authentication) + .authenticate(authStrategy, {identification, password}) .then(() => true); // ensure task button transitions to "success" state } catch (error) { + if (isForbiddenError(error)) { + this.router.transitionTo('signin-verify'); + return; + } + if (isVersionMismatchError(error)) { return this.notifications.showAPIError(error); } @@ -100,8 +107,7 @@ export default class SigninController extends Controller.extend(ValidationEngine @task({drop: true}) *validateAndAuthenticateTask() { - let signin = this.signin; - let authStrategy = 'authenticator:cookie'; + const {identification, password} = this.signin; this.flowErrors = ''; @@ -111,7 +117,7 @@ export default class SigninController extends Controller.extend(ValidationEngine try { yield this.validate({property: 'signin'}); return yield this.authenticateTask - .perform(authStrategy, [signin.identification, signin.password]); + .perform('authenticator:cookie', {identification, password}); } catch (error) { this.flowErrors = 'Please fill out the form to sign in.'; } diff --git a/ghost/admin/app/router.js b/ghost/admin/app/router.js index 0a82fe0c87..092e55f0a7 100644 --- a/ghost/admin/app/router.js +++ b/ghost/admin/app/router.js @@ -15,6 +15,7 @@ Router.map(function () { this.route('setup.done', {path: '/setup/done'}); this.route('signin'); + this.route('signin-verify', {path: '/signin/verify'}); this.route('signout'); this.route('signup', {path: '/signup/:token'}); this.route('reset', {path: '/reset/:token'}); @@ -33,7 +34,7 @@ Router.map(function () { this.route('posts.analytics', {path: '/posts/analytics/:post_id'}); this.route('posts.mentions', {path: '/posts/analytics/:post_id/mentions'}); this.route('posts.debug', {path: '/posts/analytics/:post_id/debug'}); - + this.route('restore-posts', {path: '/restore'}); this.route('pages'); diff --git a/ghost/admin/app/templates/signin-verify.hbs b/ghost/admin/app/templates/signin-verify.hbs new file mode 100644 index 0000000000..cf02e3cf99 --- /dev/null +++ b/ghost/admin/app/templates/signin-verify.hbs @@ -0,0 +1,44 @@ +