Ghost/ghost/admin/app/templates/member.hbs
Simon Backx 75bb53f065
🔒 Added support for logging out members on all devices (#18935)
fixes https://github.com/TryGhost/Product/issues/3738
https://www.notion.so/ghost/Member-Session-Invalidation-13254316f2244c34bcbc65c101eb5cc4

- Adds the transient_id column to the members table. This defaults to
email, to keep it backwards compatible (not logging out all existing
sessions)
- Instead of using the email in the cookies, we now use the transient_id
- Updating the transient_id means invalidating all sessions of a member
- Adds an endpoint to the admin api to log out a member from all devices
- Added the `all` body property to the DELETE session endpoint in the
members API. Setting it to true will sign a member out from all devices.
- Adds a UI button in Admin to sign a member out from all devices
- Portal 'sign out of all devices' will not be added for now

Related changes (added because these areas were affected by the code
changes):
- Adds a serializer to member events / activity feed endpoints - all
member fields were returned here, so the transient_id would also be
returned - which is not needed and bloats the API response size
(`transient_id` is not a secret because the cookies are signed)
- Removed `loadMemberSession` from public settings browse (not used
anymore + bad pattern)

Performance tests on site with 50.000 members (on Macbook M1 Pro):
- Migrate: 6s (adding column 4s, setting to email is 1s, dropping
nullable: 1s)
- Rollback: 2s
2023-11-15 17:10:28 +01:00

129 lines
5.3 KiB
Handlebars

<section class="gh-canvas">
<GhCanvasHeader class="gh-canvas-header sticky gh-member-header">
<div class="flex flex-column">
{{#if this.fromAnalytics}}
<div class="gh-canvas-breadcrumb">
<LinkTo @route="posts">
Posts
</LinkTo>
{{svg-jar "arrow-right-small"}}
<LinkTo @route="posts.analytics" @models={{this.fromAnalytics}}>
Analytics
</LinkTo>
{{#unless this.directlyFromAnalytics}}
{{svg-jar "arrow-right-small"}}
<LinkTo @route="members" data-test-link="members-back" @query={{hash postAnalytics=this.postAnalytics}}>
Members
</LinkTo>
{{/unless}}
{{svg-jar "arrow-right-small"}} {{if this.member.isNew "New member" "Edit member"}}
</div>
{{else}}
<div class="gh-canvas-breadcrumb">
<LinkTo @route="members" data-test-link="members-back">
Members
</LinkTo>
{{svg-jar "arrow-right-small"}} {{if this.member.isNew "New member" "Edit member"}}
</div>
{{/if}}
<h2 class="gh-canvas-title" data-test-screen-title>
{{#if this.member.isNew}}
New<span class="gh-canvas-title-hide-for-mobile">&nbsp;member</span>
{{else}}
{{or this.member.name this.member.email}}
{{/if}}
</h2>
</div>
<section class="view-actions">
{{#if this.session.user.isAdmin}}
{{#unless this.member.isNew}}
<span class="dropdown">
<GhDropdownButton
@dropdownName="members-actions-menu"
@classNames="gh-btn gh-btn-icon icon-only gh-btn-action-icon"
@title="Members Actions"
data-test-button="member-actions"
>
<span>
{{svg-jar "settings"}}
<span class="hidden">Actions</span>
</span>
</GhDropdownButton>
<GhDropdown
@name="members-actions-menu"
@tagName="ul"
@classNames="gh-member-actions-menu dropdown-menu dropdown-triangle-top-right"
>
<li>
<button
class="mr2"
type="button"
{{on "click" this.toggleImpersonateMemberModal}}
data-test-button="impersonate"
>
<span>Impersonate</span>
</button>
</li>
<li>
<button
type="button"
class="mr2"
{{on "click" this.confirmLogoutMember}}
data-test-button="logout-member"
>
<span>Sign out of all devices</span>
</button>
</li>
<li>
<button
type="button"
class="mr2"
{{on "click" this.confirmDeleteMember}}
data-test-button="delete-member"
>
<span class="red">Delete member</span>
</button>
</li>
</GhDropdown>
</span>
{{/unless}}
{{/if}}
<GhTaskButton @class="gh-btn gh-btn-primary gh-btn-icon" @type="button" @task={{this.saveTask}} @data-test-button="save" />
</section>
</GhCanvasHeader>
<div>
<form class="member-basic-info-form">
<GhMemberSettingsForm
@member={{this.member}}
@scratchMember={{this.scratchMember}}
@setProperty={{this.setProperty}}
@onLabelEdit={{this.editLabel}}
@saveMember={{this.save}}
@isSaveRunning={{this.saveTask.isRunning}}
@isLoading={{this.isLoading}} />
</form>
</div>
</section>
{{#if this.showImpersonateMemberModal}}
<GhFullscreenModal
@modal="impersonate-member"
@model={{this.member}}
@close={{this.toggleImpersonateMemberModal}}
@modifier="action wide" />
{{/if}}
{{#if this.showLabelModal}}
<GhFullscreenModal
@modal="members-label-form"
@model={{this.labelModalData}}
@close={{this.toggleLabelModal}}
@modifier="action wide"
/>
{{/if}}