diff --git a/apps/portal/src/utils/api.js b/apps/portal/src/utils/api.js index e6478631c8..5ecc72430b 100644 --- a/apps/portal/src/utils/api.js +++ b/apps/portal/src/utils/api.js @@ -282,11 +282,17 @@ function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}) { } }, - signout() { + signout(all = false) { const url = endpointFor({type: 'members', resource: 'session'}); return makeRequest({ url, - method: 'DELETE' + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + all + }) }).then(function (res) { if (res.ok) { window.location.replace(siteUrl); diff --git a/ghost/admin/app/components/members/modals/logout-member.hbs b/ghost/admin/app/components/members/modals/logout-member.hbs new file mode 100644 index 0000000000..824fd5615a --- /dev/null +++ b/ghost/admin/app/components/members/modals/logout-member.hbs @@ -0,0 +1,30 @@ + diff --git a/ghost/admin/app/components/members/modals/logout-member.js b/ghost/admin/app/components/members/modals/logout-member.js new file mode 100644 index 0000000000..1fe9cd28db --- /dev/null +++ b/ghost/admin/app/components/members/modals/logout-member.js @@ -0,0 +1,31 @@ +import Component from '@glimmer/component'; +import {inject as service} from '@ember/service'; +import {task} from 'ember-concurrency'; + +export default class LogoutMemberModal extends Component { + @service notifications; + @service ajax; + @service ghostPaths; + + get member() { + return this.args.data.member; + } + + @task({drop: true}) + *logoutMemberTask() { + try { + const url = this.ghostPaths.url.api('/members/', this.member.id, '/sessions/'); + const options = {}; + yield this.ajax.delete(url, options); + + this.args.data.afterLogout?.(); + this.notifications.showNotification(`${this.member.name || this.member.email} has been successfully signed out from all devices.`, {type: 'success'}); + this.args.close(true); + return true; + } catch (e) { + this.notifications.showAPIError(e, {key: 'member.logout'}); + this.args.close(false); + throw e; + } + } +} diff --git a/ghost/admin/app/controllers/member.js b/ghost/admin/app/controllers/member.js index f64cf52edb..1ec80427ff 100644 --- a/ghost/admin/app/controllers/member.js +++ b/ghost/admin/app/controllers/member.js @@ -1,6 +1,7 @@ import Controller, {inject as controller} from '@ember/controller'; import DeleteMemberModal from '../components/members/modals/delete-member'; import EmberObject, {action, defineProperty} from '@ember/object'; +import LogoutMemberModal from '../components/members/modals/logout-member'; import boundOneWay from 'ghost-admin/utils/bound-one-way'; import moment from 'moment-timezone'; import {inject as service} from '@ember/service'; @@ -143,6 +144,16 @@ export default class MemberController extends Controller { }); } + @action + confirmLogoutMember() { + this.modals.open(LogoutMemberModal, { + member: this.member, + afterLogout: () => { + this.members.refreshData(); + } + }); + } + @action toggleImpersonateMemberModal() { this.showImpersonateMemberModal = !this.showImpersonateMemberModal; diff --git a/ghost/admin/app/models/member.js b/ghost/admin/app/models/member.js index 06945044ed..a965705df9 100644 --- a/ghost/admin/app/models/member.js +++ b/ghost/admin/app/models/member.js @@ -49,5 +49,10 @@ export default Model.extend(ValidationEngine, { let response = yield this.ajax.request(url); return response.member_signin_urls[0]; + }).drop(), + + logoutAllDevices: task(function* () { + let url = this.get('ghostPaths.url').api('members', this.id, 'signout'); + yield this.ajax.post(url); }).drop() }); diff --git a/ghost/admin/app/templates/member.hbs b/ghost/admin/app/templates/member.hbs index a5b7186ee1..3a7cf1aada 100644 --- a/ghost/admin/app/templates/member.hbs +++ b/ghost/admin/app/templates/member.hbs @@ -67,6 +67,16 @@ Impersonate +
  • + +