Ghost/ghost/admin/app/models/member.js
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

59 lines
1.9 KiB
JavaScript

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',
name: attr('string'),
email: attr('string'),
note: attr('string'),
status: attr('string'),
createdAtUTC: attr('moment-utc'),
lastSeenAtUTC: attr('moment-utc'),
subscriptions: attr('member-subscription'),
attribution: attr(),
subscribed: attr('boolean', {defaultValue: true}),
comped: attr('boolean', {defaultValue: false}),
geolocation: attr('json-string'),
emailCount: attr('number', {defaultValue: 0}),
emailOpenedCount: attr('number', {defaultValue: 0}),
emailOpenRate: attr('number'),
avatarImage: attr('string'),
tiers: attr('member-tier'),
newsletters: hasMany('newsletter', {embedded: 'always', async: false}),
emailSuppression: attr(),
labels: hasMany('label', {embedded: 'always', async: 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.
// https://github.com/emberjs/data/issues/1829
updateLabels() {
let labels = this.labels;
let oldLabels = labels.filterBy('id', null);
labels.removeObjects(oldLabels);
oldLabels.invoke('deleteRecord');
},
fetchSigninUrl: task(function* () {
let url = this.get('ghostPaths.url').api('members', this.id, 'signin_urls');
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()
});