diff --git a/ghost/admin/app/components/modal-impersonate-member.js b/ghost/admin/app/components/modal-impersonate-member.js
new file mode 100644
index 0000000000..b52823c587
--- /dev/null
+++ b/ghost/admin/app/components/modal-impersonate-member.js
@@ -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()
+});
diff --git a/ghost/admin/app/controllers/member.js b/ghost/admin/app/controllers/member.js
index 066e1ee319..146a101678 100644
--- a/ghost/admin/app/controllers/member.js
+++ b/ghost/admin/app/controllers/member.js
@@ -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) {
diff --git a/ghost/admin/app/models/member.js b/ghost/admin/app/models/member.js
index 8e209f87e4..59da242910 100644
--- a/ghost/admin/app/models/member.js
+++ b/ghost/admin/app/models/member.js
@@ -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()
});
diff --git a/ghost/admin/app/styles/layouts/members.css b/ghost/admin/app/styles/layouts/members.css
index 57f44ffcad..26df007e73 100644
--- a/ghost/admin/app/styles/layouts/members.css
+++ b/ghost/admin/app/styles/layouts/members.css
@@ -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);
diff --git a/ghost/admin/app/styles/patterns/buttons.css b/ghost/admin/app/styles/patterns/buttons.css
index 69e23cf3b9..070bccc216 100644
--- a/ghost/admin/app/styles/patterns/buttons.css
+++ b/ghost/admin/app/styles/patterns/buttons.css
@@ -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;
diff --git a/ghost/admin/app/styles/patterns/forms.css b/ghost/admin/app/styles/patterns/forms.css
index f53b7bcc43..49f7edfc89 100644
--- a/ghost/admin/app/styles/patterns/forms.css
+++ b/ghost/admin/app/styles/patterns/forms.css
@@ -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
/* ---------------------------------------------------------- */
diff --git a/ghost/admin/app/styles/spirit/_animations.css b/ghost/admin/app/styles/spirit/_animations.css
index 1454c5887c..67e761a4a9 100644
--- a/ghost/admin/app/styles/spirit/_animations.css
+++ b/ghost/admin/app/styles/spirit/_animations.css
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/ghost/admin/app/templates/components/modal-impersonate-member.hbs b/ghost/admin/app/templates/components/modal-impersonate-member.hbs
new file mode 100644
index 0000000000..4bc127df6a
--- /dev/null
+++ b/ghost/admin/app/templates/components/modal-impersonate-member.hbs
@@ -0,0 +1,40 @@
+Impersonate
+
+ This is an authentication link to sign into {{this.config.blogTitle}} as {{this.member.email}}, you can send it to them if they need it, or use it to sign into their account for customer support. +
+ + +This link is only valid for the next 10 minutes
+