Added member impersonation (#1497)

refs b0ff1e7cac

- Adds "impersonate" button which would be triggering a popup window with "login url" that allows to log in as a member
This commit is contained in:
Naz 2020-02-27 11:50:15 +08:00 committed by GitHub
parent 4675fb911c
commit ff4fd2fc9a
10 changed files with 188 additions and 23 deletions

View File

@ -0,0 +1,33 @@
import ModalComponent from 'ghost-admin/components/modal-base';
import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard';
import {alias} from '@ember/object/computed';
import {inject as service} from '@ember/service';
import {task, timeout} from 'ember-concurrency';
export default ModalComponent.extend({
config: service(),
store: service(),
classNames: 'modal-impersonate-member',
signinUrl: null,
member: alias('model'),
didInsertElement() {
this._super(...arguments);
this._signinUrlUpdateTask.perform();
},
copySigninUrl: task(function* () {
copyTextToClipboard(this.get('signinUrl'));
yield timeout(1000);
return true;
}),
_signinUrlUpdateTask: task(function*() {
const memberSigninURL = yield this.member.fetchSigninUrl.perform();
this.set('signinUrl', memberSigninURL.url);
}).drop()
});

View File

@ -12,10 +12,14 @@ const SCRATCH_PROPS = ['name', 'email', 'note'];
export default Controller.extend({
members: controller(),
session: service(),
dropdown: service(),
notifications: service(),
router: service(),
store: service(),
showImpersonateMemberModal: false,
member: alias('model'),
scratchMember: computed('member', function () {
@ -39,6 +43,10 @@ export default Controller.extend({
this.toggleProperty('showDeleteMemberModal');
},
toggleImpersonateMemberModal() {
this.toggleProperty('showImpersonateMemberModal');
},
save() {
return this.save.perform();
},
@ -106,13 +114,12 @@ export default Controller.extend({
fetchMember: task(function* (memberId) {
this.set('isLoading', true);
yield this.store.findRecord('member', memberId, {
let member = yield this.store.findRecord('member', memberId, {
reload: true
}).then((member) => {
this.set('member', member);
this.set('isLoading', false);
return member;
});
this.set('member', member);
this.set('isLoading', false);
}),
_saveMemberProperty(propKey, newValue) {

View File

@ -1,5 +1,7 @@
import Model, {attr, hasMany} from '@ember-data/model';
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
export default Model.extend(ValidationEngine, {
validationType: 'member',
@ -12,6 +14,10 @@ export default Model.extend(ValidationEngine, {
subscribed: attr('boolean', {defaultValue: true}),
labels: hasMany('label', {embedded: 'always', async: false}),
comped: attr('boolean', {defaultValue: false}),
ghostPaths: service(),
ajax: service(),
// remove client-generated labels, which have `id: null`.
// Ember Data won't recognize/update them automatically
// when returned from the server with ids.
@ -22,5 +28,13 @@ export default Model.extend(ValidationEngine, {
labels.removeObjects(oldLabels);
oldLabels.invoke('deleteRecord');
}
},
fetchSigninUrl: task(function* () {
let url = this.get('ghostPaths.url').api('members', this.get('id'), 'signin_urls');
let response = yield this.ajax.request(url);
return response.member_signin_urls[0];
}).drop()
});

View File

@ -96,6 +96,10 @@ p.gh-members-list-email {
pointer-events: none;
}
.member-link-copied svg {
margin-right: 4px;
}
.members-header .gh-members-header-search {
margin-right: 12px;
border-right: 1px solid var(--lightgrey-d2);

View File

@ -279,6 +279,11 @@ fieldset[disabled] .gh-btn {
background: var(--white);
}
.gh-btn-white.gh-btn-green:hover,
.gh-btn-white.gh-btn-blue:hover {
border: none;
}
.gh-btn-white span {
height: 35px;
line-height: 35px;

View File

@ -656,6 +656,18 @@ textarea {
background: var(--input-bg-color);
}
.gh-input-group .gh-btn {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.gh-input-group .gh-btn span {
height: 36px;
line-height: 36px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
/* FFF: Fucking Firefox Fixes
/* ---------------------------------------------------------- */

View File

@ -284,3 +284,34 @@ Pop: Appear from bottom, disappear to bottom
width: 6px;
background: var(--darkgrey-l2);
}
/* Animated icons */
.animated-icon path {
stroke-dashoffset: 300;
stroke-dasharray: 300;
animation: icon-dash 3s ease-out forwards;
}
@keyframes icon-dash {
0% {
stroke-dashoffset: 300;
}
100% {
stroke-dashoffset: 0;
}
}
/* Fade in */
.fade-in {
opacity: 0;
animation: fade-in 3s ease-out forwards;
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1.0;
}
}

View File

@ -0,0 +1,40 @@
<header class="modal-header flex justify-center">
<h1 style="margin: 0;">Impersonate</h1>
</header>
{{!-- disable mouseDown so it doesn't trigger focus-out validations --}}
<a class="close" href title="Close" {{action "closeModal"}} {{action (optional this.noop) on="mouseDown"}}>
{{svg-jar "close"}}<span class="hidden">Close</span>
</a>
<div class="modal-body">
<div class="flex items-center justify-center mt4 mb4">
<GhMemberAvatar
@member={{this.member}}
@sizeClass={{if this.member.name 'f-headline fw4 lh-zero tracked-1' 'f-headline fw4 lh-zero tracked-1'}}
@containerClass="w25 h25 gh-member-detail-avatar" />
</div>
<p class="tc pl4 pr4">
This is an authentication link to sign into <strong>{{this.config.blogTitle}}</strong> as <strong>{{this.member.email}}</strong>, you can send it to them if they need it, or use it to sign into their account for customer support.
</p>
<fieldset>
<div class="gh-input-group">
<GhTextInput
@id="member-signin-url"
@name="member-signin-url"
@disabled={{true}}
@value={{readonly signinUrl}}
/>
<GhTaskButton
@buttonText="Copy link"
@task={{this.copySigninUrl}}
@successText="Link copied"
@class="gh-btn gh-btn-blue gh-btn-icon" />
</div>
</fieldset>
</div>
<div>
<p class="tc pt4 mb2">This link is only valid for the next <strong>10 minutes</strong></p>
</div>

View File

@ -1,26 +1,37 @@
<section class="gh-canvas">
<form class="mb10 member-basic-info-form">
<GhCanvasHeader class="gh-canvas-header">
<h2 class="gh-canvas-title" data-test-screen-title>
<LinkTo @route="members" data-test-link="members-back">Members</LinkTo>
<span>{{svg-jar "arrow-right"}}</span>
{{#if this.member.isNew}}
New member
{{else}}
{{or this.member.name this.member.email}}
{{/if}}
</h2>
<section class="view-actions">
<GhTaskButton @class="gh-btn gh-btn-blue gh-btn-icon" @type="button" @task={{this.save}} @data-test-button="save" />
</section>
</GhCanvasHeader>
<GhCanvasHeader class="gh-canvas-header">
<h2 class="gh-canvas-title" data-test-screen-title>
<LinkTo @route="members" data-test-link="members-back">Members</LinkTo>
<span>{{svg-jar "arrow-right"}}</span>
{{#if this.member.isNew}}
New member
{{else}}
{{or this.member.name this.member.email}}
{{/if}}
</h2>
<section class="view-actions">
{{#if this.session.user.isOwner}}
{{#unless this.member.isNew}}
<button
class="gh-btn gh-btn-white gh-btn-icon mr2"
{{on "click" (action "toggleImpersonateMemberModal")}}>
<span>Impersonate</span>
</button>
{{/unless}}
{{/if}}
<GhTaskButton @class="gh-btn gh-btn-blue gh-btn-icon" @type="button" @task={{this.save}} @data-test-button="save" />
</section>
</GhCanvasHeader>
<form class="mb10 member-basic-info-form">
<div class="flex items-center mb10 bt b--lightgrey-d1 pt8">
{{#if (or this.member.name this.member.email)}}
<GhMemberAvatar
@member={{this.member}}
@sizeClass={{if this.member.name 'f-subheadline fw4 lh-zero tracked-1' 'f-headline fw4 lh-zero tracked-1'}}
@containerClass="w18 h18 mr4 gh-member-detail-avatar"
@containerClass="w20 h20 mr4 gh-member-detail-avatar"
/>
{{else}}
<div class="flex items-center justify-center br-100 w18 h18 mr4 gh-new-member-avatar">
@ -78,3 +89,11 @@
@close={{action "toggleDeleteMemberModal"}}
@modifier="action wide" />
{{/if}}
{{#if this.showImpersonateMemberModal}}
<GhFullscreenModal
@modal="impersonate-member"
@model={{this.member}}
@close={{action "toggleImpersonateMemberModal"}}
@modifier="action wide" />
{{/if}}

View File

@ -134,7 +134,7 @@ describe('Acceptance: Members', function () {
// it navigates to the new member route
expect(currentURL(), 'new member URL').to.equal('/members/new');
// it displays the new member form
expect(find('.member-basic-info-form .gh-canvas-header h2').textContent, 'settings pane title')
expect(find('.gh-canvas-header h2').textContent, 'settings pane title')
.to.contain('New member');
// // all fields start blank