75bb53f065
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
129 lines
5.3 KiB
Handlebars
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"> 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}}
|