mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 03:44:29 +03:00
Added sender email verification flow for newsletters
refs https://github.com/TryGhost/Team/issues/584 refs https://github.com/TryGhost/Team/issues/1498 - updated newsletter save routine in `edit-newsletter` modal to open an email confirmation modal if the API indicates one was sent - modal indicates that the previously set or default email will continue to be used until verified - response from API when saving looks like `{newsletters: [{...}], meta: {sent_email_verification: ['sender_name]}}` - added custom newsletter serializer and updated model so that the `meta` property returned in the response when saving posts is exposed - Ember Data only exposes meta on array-response find/query methods - https://github.com/emberjs/data/issues/2905 - added `/settings/members-email-labs/?verifyEmail=xyz` query param handling - opens email verification modal if param is set and instantly clears the query param to avoid problems with sticky params - when the modal opens it makes a `PUT /newsletters/verify-email/` request with the token in the body params, on the API side this works the same as a newsletter update request returning the fully updated newsletter record which is then pushed into the store - removed unused from/reply address code from `<Settings::MembersEmailLabs>` component and controller - setting the values now handled per-newsletter in the edit-newsletter modal - verifying email change is handled in the members-email-labs controller - fixed mirage not outputting pluralized root for "singular" endpoints such as POST/PUT requests to better match our API behaviour
This commit is contained in:
parent
cc6c0acf55
commit
e398557a75
@ -1,9 +1,13 @@
|
|||||||
import Component from '@glimmer/component';
|
import Component from '@glimmer/component';
|
||||||
|
import ConfirmNewsletterEmailModal from './edit-newsletter/confirm-newsletter-email';
|
||||||
import {action} from '@ember/object';
|
import {action} from '@ember/object';
|
||||||
|
import {inject as service} from '@ember/service';
|
||||||
import {task} from 'ember-concurrency';
|
import {task} from 'ember-concurrency';
|
||||||
import {tracked} from '@glimmer/tracking';
|
import {tracked} from '@glimmer/tracking';
|
||||||
|
|
||||||
export default class EditNewsletterModal extends Component {
|
export default class EditNewsletterModal extends Component {
|
||||||
|
@service modals;
|
||||||
|
|
||||||
static modalOptions = {
|
static modalOptions = {
|
||||||
className: 'fullscreen-modal-full-overlay fullscreen-modal-portal-settings'
|
className: 'fullscreen-modal-full-overlay fullscreen-modal-portal-settings'
|
||||||
};
|
};
|
||||||
@ -31,8 +35,17 @@ export default class EditNewsletterModal extends Component {
|
|||||||
@task
|
@task
|
||||||
*saveTask() {
|
*saveTask() {
|
||||||
try {
|
try {
|
||||||
|
const newEmail = this.args.data.newsletter.senderEmail;
|
||||||
|
|
||||||
const result = yield this.args.data.newsletter.save();
|
const result = yield this.args.data.newsletter.save();
|
||||||
|
|
||||||
|
if (result._meta?.sent_email_verification) {
|
||||||
|
yield this.modals.open(ConfirmNewsletterEmailModal, {
|
||||||
|
newEmail,
|
||||||
|
currentEmail: this.args.data.newsletter.senderEmail
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.args.data.afterSave?.(result);
|
this.args.data.afterSave?.(result);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
<div class="modal-content">
|
||||||
|
<header class="modal-header" data-test-modal="confirm-newsletter-email">
|
||||||
|
<h1>Confirm newsletter email address</h1>
|
||||||
|
</header>
|
||||||
|
<button type="button" class="close" role="button" title="Close" {{on "click" @close}}>{{svg-jar "close"}}<span class="hidden">Close</span></button>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>
|
||||||
|
We've sent a confirmation email to <strong>{{@data.newEmail}}</strong>.
|
||||||
|
Until the address has been verified newsletters will be sent from the
|
||||||
|
{{if @data.currentEmail "previous" "default"}} email address
|
||||||
|
({{full-email-address (or @data.currentEmail "noreply")}}).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="gh-btn"
|
||||||
|
{{on "click" @close}}
|
||||||
|
{{on-key "Enter"}}
|
||||||
|
>
|
||||||
|
<span>Ok</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,34 @@
|
|||||||
|
<div class="modal-content">
|
||||||
|
<header class="modal-header" data-test-modal="verify-newsletter-email">
|
||||||
|
<h1>Verifying newsletter email address</h1>
|
||||||
|
</header>
|
||||||
|
<button type="button" class="close" role="button" title="Close" {{on "click" @close}}>{{svg-jar "close"}}<span class="hidden">Close</span></button>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
{{#if this.verifyEmailTask.isRunning}}
|
||||||
|
<div class="flex justify-center flex-auto">
|
||||||
|
<div class="gh-loading-spinner"></div>
|
||||||
|
</div>
|
||||||
|
{{else if this.newsletter}}
|
||||||
|
<p>
|
||||||
|
Success! From address for newsletter
|
||||||
|
"<LinkTo @route="settings.members-email-labs.edit-newsletter" @model={{this.newsletter.id}}>{{this.newsletter.name}}</LinkTo>"
|
||||||
|
changed to <strong>{{this.newsletter.senderEmail}}</strong>
|
||||||
|
</p>
|
||||||
|
{{else if this.error}}
|
||||||
|
<p>Verification failed:</p>
|
||||||
|
<p>{{this.error}}</p>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="gh-btn"
|
||||||
|
{{on "click" @close}}
|
||||||
|
{{on-key "Enter"}}
|
||||||
|
>
|
||||||
|
<span>Ok</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,49 @@
|
|||||||
|
import Component from '@glimmer/component';
|
||||||
|
import {action} from '@ember/object';
|
||||||
|
import {inject as service} from '@ember/service';
|
||||||
|
import {task} from 'ember-concurrency';
|
||||||
|
import {tracked} from '@glimmer/tracking';
|
||||||
|
|
||||||
|
export default class VerifyNewsletterEmail extends Component {
|
||||||
|
@service ajax;
|
||||||
|
@service ghostPaths;
|
||||||
|
@service router;
|
||||||
|
@service store;
|
||||||
|
|
||||||
|
@tracked error = null;
|
||||||
|
@tracked newsletter = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.verifyEmailTask.perform(this.args.data.token);
|
||||||
|
|
||||||
|
this.router.on('routeDidChange', this.handleRouteChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
willDestroy() {
|
||||||
|
super.willDestroy(...arguments);
|
||||||
|
this.router.off('routeDidChange', this.handleRouteChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@task
|
||||||
|
*verifyEmailTask(token) {
|
||||||
|
try {
|
||||||
|
const url = this.ghostPaths.url.api('newsletters', 'verify-email');
|
||||||
|
|
||||||
|
const response = yield this.ajax.put(url, {data: {token}});
|
||||||
|
|
||||||
|
if (response.newsletters) {
|
||||||
|
this.store.pushPayload('newsletter', response);
|
||||||
|
const newsletter = this.store.peekRecord('newsletter', response.newsletters[0].id);
|
||||||
|
this.newsletter = newsletter;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.error = e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
handleRouteChange() {
|
||||||
|
this.args.close();
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
import Component from '@glimmer/component';
|
import Component from '@glimmer/component';
|
||||||
import {action} from '@ember/object';
|
import {action} from '@ember/object';
|
||||||
import {inject as service} from '@ember/service';
|
import {inject as service} from '@ember/service';
|
||||||
import {task} from 'ember-concurrency';
|
|
||||||
import {tracked} from '@glimmer/tracking';
|
import {tracked} from '@glimmer/tracking';
|
||||||
|
|
||||||
const US = {flag: '🇺🇸', name: 'US', baseUrl: 'https://api.mailgun.net/v3'};
|
const US = {flag: '🇺🇸', name: 'US', baseUrl: 'https://api.mailgun.net/v3'};
|
||||||
@ -9,8 +8,6 @@ const EU = {flag: '🇪🇺', name: 'EU', baseUrl: 'https://api.eu.mailgun.net/v
|
|||||||
|
|
||||||
export default class MembersEmailLabs extends Component {
|
export default class MembersEmailLabs extends Component {
|
||||||
@service config;
|
@service config;
|
||||||
@service ghostPaths;
|
|
||||||
@service ajax;
|
|
||||||
@service settings;
|
@service settings;
|
||||||
|
|
||||||
// set recipientsSelectValue as a static property because within this
|
// set recipientsSelectValue as a static property because within this
|
||||||
@ -19,41 +16,12 @@ export default class MembersEmailLabs extends Component {
|
|||||||
// from settings as it would equate to "none"
|
// from settings as it would equate to "none"
|
||||||
@tracked recipientsSelectValue = this._getDerivedRecipientsSelectValue();
|
@tracked recipientsSelectValue = this._getDerivedRecipientsSelectValue();
|
||||||
|
|
||||||
@tracked showFromAddressConfirmation = false;
|
|
||||||
|
|
||||||
mailgunRegions = [US, EU];
|
mailgunRegions = [US, EU];
|
||||||
|
|
||||||
replyAddresses = [
|
|
||||||
{
|
|
||||||
label: 'Newsletter email address (' + this.fromAddress + ')',
|
|
||||||
value: 'newsletter'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Support email address (' + this.supportAddress + ')',
|
|
||||||
value: 'support'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
get emailNewsletterEnabled() {
|
get emailNewsletterEnabled() {
|
||||||
return this.settings.get('editorDefaultEmailRecipients') !== 'disabled';
|
return this.settings.get('editorDefaultEmailRecipients') !== 'disabled';
|
||||||
}
|
}
|
||||||
|
|
||||||
get emailPreviewVisible() {
|
|
||||||
return this.recipientsSelectValue !== 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
get selectedReplyAddress() {
|
|
||||||
return this.replyAddresses.findBy('value', this.settings.get('membersReplyAddress'));
|
|
||||||
}
|
|
||||||
|
|
||||||
get disableUpdateFromAddressButton() {
|
|
||||||
const savedFromAddress = this.settings.get('membersFromAddress') || '';
|
|
||||||
if (!savedFromAddress.includes('@') && this.config.emailDomain) {
|
|
||||||
return !this.fromAddress || (this.fromAddress === `${savedFromAddress}@${this.config.emailDomain}`);
|
|
||||||
}
|
|
||||||
return !this.fromAddress || (this.fromAddress === savedFromAddress);
|
|
||||||
}
|
|
||||||
|
|
||||||
get mailgunRegion() {
|
get mailgunRegion() {
|
||||||
if (!this.settings.get('mailgunBaseUrl')) {
|
if (!this.settings.get('mailgunBaseUrl')) {
|
||||||
return US;
|
return US;
|
||||||
@ -72,11 +40,6 @@ export default class MembersEmailLabs extends Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
toggleFromAddressConfirmation() {
|
|
||||||
this.showFromAddressConfirmation = !this.showFromAddressConfirmation;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
setMailgunDomain(event) {
|
setMailgunDomain(event) {
|
||||||
this.settings.set('mailgunDomain', event.target.value);
|
this.settings.set('mailgunDomain', event.target.value);
|
||||||
@ -98,11 +61,6 @@ export default class MembersEmailLabs extends Component {
|
|||||||
this.settings.set('mailgunBaseUrl', region.baseUrl);
|
this.settings.set('mailgunBaseUrl', region.baseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
setFromAddress(fromAddress) {
|
|
||||||
this.setEmailAddress('fromAddress', fromAddress);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
toggleEmailTrackOpens(event) {
|
toggleEmailTrackOpens(event) {
|
||||||
if (event) {
|
if (event) {
|
||||||
@ -129,13 +87,6 @@ export default class MembersEmailLabs extends Component {
|
|||||||
this.recipientsSelectValue = this._getDerivedRecipientsSelectValue();
|
this.recipientsSelectValue = this._getDerivedRecipientsSelectValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
setReplyAddress(event) {
|
|
||||||
const newReplyAddress = event.value;
|
|
||||||
|
|
||||||
this.settings.set('membersReplyAddress', newReplyAddress);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
setDefaultEmailRecipients(value) {
|
setDefaultEmailRecipients(value) {
|
||||||
// Update the underlying setting properties to match the selected recipients option
|
// Update the underlying setting properties to match the selected recipients option
|
||||||
@ -169,24 +120,6 @@ export default class MembersEmailLabs extends Component {
|
|||||||
this.settings.set('editorDefaultEmailRecipientsFilter', filter);
|
this.settings.set('editorDefaultEmailRecipientsFilter', filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@task({drop: true})
|
|
||||||
*updateFromAddress() {
|
|
||||||
let url = this.ghostPaths.url.api('/settings/members/email');
|
|
||||||
try {
|
|
||||||
const response = yield this.ajax.post(url, {
|
|
||||||
data: {
|
|
||||||
email: this.fromAddress,
|
|
||||||
type: 'fromAddressUpdate'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.toggleFromAddressConfirmation();
|
|
||||||
return response;
|
|
||||||
} catch (e) {
|
|
||||||
// Failed to send email, retry
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_getDerivedRecipientsSelectValue() {
|
_getDerivedRecipientsSelectValue() {
|
||||||
const defaultEmailRecipients = this.settings.get('editorDefaultEmailRecipients');
|
const defaultEmailRecipients = this.settings.get('editorDefaultEmailRecipients');
|
||||||
const defaultEmailRecipientsFilter = this.settings.get('editorDefaultEmailRecipientsFilter');
|
const defaultEmailRecipientsFilter = this.settings.get('editorDefaultEmailRecipientsFilter');
|
||||||
|
@ -1,37 +1,14 @@
|
|||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
import {action} from '@ember/object';
|
|
||||||
import {inject as service} from '@ember/service';
|
import {inject as service} from '@ember/service';
|
||||||
import {task} from 'ember-concurrency';
|
import {task} from 'ember-concurrency';
|
||||||
import {tracked} from '@glimmer/tracking';
|
import {tracked} from '@glimmer/tracking';
|
||||||
|
|
||||||
export default class MembersEmailLabsController extends Controller {
|
export default class MembersEmailLabsController extends Controller {
|
||||||
@service config;
|
|
||||||
@service session;
|
|
||||||
@service settings;
|
@service settings;
|
||||||
|
|
||||||
// from/supportAddress are set here so that they can be reset to saved values on save
|
queryParams = ['verifyEmail'];
|
||||||
// to avoid it looking like they've been saved when they have a separate update process
|
|
||||||
@tracked fromAddress = '';
|
|
||||||
@tracked supportAddress = '';
|
|
||||||
|
|
||||||
@action
|
@tracked verifyEmail = null;
|
||||||
setEmailAddress(property, email) {
|
|
||||||
this[property] = email;
|
|
||||||
}
|
|
||||||
|
|
||||||
parseEmailAddress(address) {
|
|
||||||
const emailAddress = address || 'noreply';
|
|
||||||
// Adds default domain as site domain
|
|
||||||
if (emailAddress.indexOf('@') < 0 && this.config.emailDomain) {
|
|
||||||
return `${emailAddress}@${this.config.emailDomain}`;
|
|
||||||
}
|
|
||||||
return emailAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
resetEmailAddresses() {
|
|
||||||
this.fromAddress = this.parseEmailAddress(this.settings.get('membersFromAddress'));
|
|
||||||
this.supportAddress = this.parseEmailAddress(this.settings.get('membersSupportAddress'));
|
|
||||||
}
|
|
||||||
|
|
||||||
@task({drop: true})
|
@task({drop: true})
|
||||||
*saveSettings() {
|
*saveSettings() {
|
||||||
|
@ -27,4 +27,8 @@ export default class Newsletter extends Model.extend(ValidationEngine) {
|
|||||||
@attr({defaultValue: 'sans_serif'}) bodyFontCategory;
|
@attr({defaultValue: 'sans_serif'}) bodyFontCategory;
|
||||||
@attr() footerContent;
|
@attr() footerContent;
|
||||||
@attr({defaultValue: true}) showBadge;
|
@attr({defaultValue: true}) showBadge;
|
||||||
|
|
||||||
|
// HACK - not a real model attribute but a workaround for Ember Data not
|
||||||
|
// exposing meta from save responses
|
||||||
|
@attr _meta;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import AdminRoute from 'ghost-admin/routes/admin';
|
import AdminRoute from 'ghost-admin/routes/admin';
|
||||||
import ConfirmUnsavedChangesModal from '../../components/modals/confirm-unsaved-changes';
|
import ConfirmUnsavedChangesModal from '../../components/modals/confirm-unsaved-changes';
|
||||||
|
import VerifyNewsletterEmail from '../../components/modals/edit-newsletter/verify-newsletter-email';
|
||||||
import {action} from '@ember/object';
|
import {action} from '@ember/object';
|
||||||
import {inject as service} from '@ember/service';
|
import {inject as service} from '@ember/service';
|
||||||
|
|
||||||
@ -9,30 +10,37 @@ export default class MembersEmailLabsRoute extends AdminRoute {
|
|||||||
@service notifications;
|
@service notifications;
|
||||||
@service settings;
|
@service settings;
|
||||||
|
|
||||||
|
queryParams = {
|
||||||
|
verifyEmail: {
|
||||||
|
replace: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
confirmModal = null;
|
confirmModal = null;
|
||||||
hasConfirmed = false;
|
hasConfirmed = false;
|
||||||
|
|
||||||
beforeModel(transition) {
|
beforeModel() {
|
||||||
super.beforeModel(...arguments);
|
super.beforeModel(...arguments);
|
||||||
|
|
||||||
if (!this.feature.multipleNewsletters) {
|
if (!this.feature.multipleNewsletters) {
|
||||||
return this.transitionTo('settings.members-email');
|
return this.transitionTo('settings.members-email');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transition.to.queryParams?.fromAddressUpdate === 'success') {
|
|
||||||
this.notifications.showAlert(
|
|
||||||
`Newsletter email address has been updated`,
|
|
||||||
{type: 'success', key: 'members.settings.from-address.updated'}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model() {
|
model() {
|
||||||
return this.settings.reload();
|
return this.settings.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
setupController(controller) {
|
afterModel(model, transition) {
|
||||||
controller.resetEmailAddresses();
|
if (transition.to.queryParams.verifyEmail) {
|
||||||
|
this.modals.open(VerifyNewsletterEmail, {
|
||||||
|
token: transition.to.queryParams.verifyEmail
|
||||||
|
});
|
||||||
|
|
||||||
|
// clear query param so it doesn't linger and cause problems re-entering route
|
||||||
|
transition.abort();
|
||||||
|
return this.transitionTo('settings.members-email-labs', {queryParams: {verifyEmail: null}});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
27
ghost/admin/app/serializers/newsletter.js
Normal file
27
ghost/admin/app/serializers/newsletter.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/* eslint-disable camelcase */
|
||||||
|
import ApplicationSerializer from './application';
|
||||||
|
|
||||||
|
export default class MemberSerializer extends ApplicationSerializer {
|
||||||
|
// HACK: Ember Data doesn't expose `meta` properties consistently
|
||||||
|
// - https://github.com/emberjs/data/issues/2905
|
||||||
|
//
|
||||||
|
// We need the `meta` data returned when saving so we extract it and dump
|
||||||
|
// it onto the model as an attribute then delete it again when serializing.
|
||||||
|
normalizeResponse() {
|
||||||
|
const json = super.normalizeResponse(...arguments);
|
||||||
|
|
||||||
|
if (json.meta && json.data.attributes) {
|
||||||
|
json.data.attributes._meta = json.meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize() {
|
||||||
|
const json = super.serialize(...arguments);
|
||||||
|
|
||||||
|
delete json._meta;
|
||||||
|
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
}
|
@ -20,11 +20,7 @@
|
|||||||
|
|
||||||
<section class="view-container settings-debug">
|
<section class="view-container settings-debug">
|
||||||
<div class="gh-setting-liquid-section">
|
<div class="gh-setting-liquid-section">
|
||||||
<Settings::MembersEmailLabs
|
<Settings::MembersEmailLabs />
|
||||||
@fromAddress={{this.fromAddress}}
|
|
||||||
@supportAddress={{this.supportAddress}}
|
|
||||||
@setEmailAddress={{this.setEmailAddress}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
@ -1,8 +1,77 @@
|
|||||||
|
import {camelize} from '@ember/string';
|
||||||
import {paginatedResponse} from '../utils';
|
import {paginatedResponse} from '../utils';
|
||||||
|
|
||||||
export default function mockNewsletters(server) {
|
export default function mockNewsletters(server) {
|
||||||
server.post('/newsletters/');
|
|
||||||
server.get('/newsletters/', paginatedResponse('newsletters'));
|
server.get('/newsletters/', paginatedResponse('newsletters'));
|
||||||
server.get('/newsletters/:id/');
|
server.get('/newsletters/:id/');
|
||||||
server.put('/newsletters/:id/');
|
|
||||||
|
server.post('/newsletters/', function ({newsletters}) {
|
||||||
|
const attrs = this.normalizedRequestAttrs();
|
||||||
|
|
||||||
|
// sender email can't be set without verification
|
||||||
|
const senderEmail = attrs.senderEmail;
|
||||||
|
attrs.senderEmail = null;
|
||||||
|
|
||||||
|
const newsletter = newsletters.create(attrs);
|
||||||
|
|
||||||
|
// workaround for mirage output of meta
|
||||||
|
const collection = newsletters.where({id: newsletter.id});
|
||||||
|
|
||||||
|
if (senderEmail) {
|
||||||
|
collection.meta = {
|
||||||
|
sent_email_verification: ['sender_email']
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return collection;
|
||||||
|
});
|
||||||
|
|
||||||
|
server.put('/newsletters/:id/', function ({newsletters}, {params}) {
|
||||||
|
const attrs = this.normalizedRequestAttrs();
|
||||||
|
const newsletter = newsletters.find(params.id);
|
||||||
|
|
||||||
|
const previousSenderEmail = newsletter.senderEmail;
|
||||||
|
const newSenderEmail = attrs.senderEmail;
|
||||||
|
|
||||||
|
// sender email can't be changed without verification
|
||||||
|
if (newSenderEmail && newSenderEmail !== previousSenderEmail) {
|
||||||
|
attrs.senderEmail = previousSenderEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
newsletter.update(attrs);
|
||||||
|
|
||||||
|
// workaround for mirage output of meta
|
||||||
|
const collection = newsletters.where({id: newsletter.id});
|
||||||
|
|
||||||
|
if (newSenderEmail && newSenderEmail !== previousSenderEmail) {
|
||||||
|
collection.meta = {
|
||||||
|
sent_email_verification: ['sender_email']
|
||||||
|
};
|
||||||
|
|
||||||
|
const tokenData = {
|
||||||
|
id: newsletter.id,
|
||||||
|
email: newSenderEmail,
|
||||||
|
type: 'sender_email'
|
||||||
|
};
|
||||||
|
const token = btoa(JSON.stringify(tokenData));
|
||||||
|
const baseUrl = window.location.href.replace(window.location.hash, '');
|
||||||
|
const verifyUrl = `${baseUrl}settings/members-email-labs/?verifyEmail=${token}`;
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.warn('Verification email sent. Mocked verification URL:', verifyUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return collection;
|
||||||
|
});
|
||||||
|
|
||||||
|
// verify email update
|
||||||
|
server.put('/newsletters/verify-email/', function ({newsletters}, request) {
|
||||||
|
const requestBody = JSON.parse(request.requestBody);
|
||||||
|
const tokenData = JSON.parse(atob(requestBody.token));
|
||||||
|
|
||||||
|
const newsletter = newsletters.find(tokenData.id);
|
||||||
|
|
||||||
|
newsletter[camelize(tokenData.type)] = tokenData.email;
|
||||||
|
|
||||||
|
return newsletter.save();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -9,4 +9,30 @@ export default function (server) {
|
|||||||
server.create('integration', {name: 'Demo'});
|
server.create('integration', {name: 'Demo'});
|
||||||
|
|
||||||
server.createList('member', 125);
|
server.createList('member', 125);
|
||||||
|
|
||||||
|
// sites always have a default newsletter
|
||||||
|
server.create('newsletter', {
|
||||||
|
name: 'Site title',
|
||||||
|
slug: 'site-title',
|
||||||
|
description: 'Default newsletter created during setup',
|
||||||
|
|
||||||
|
senderName: 'Site title',
|
||||||
|
senderEmail: null,
|
||||||
|
senderReplyTo: 'newsletter',
|
||||||
|
|
||||||
|
status: 'active',
|
||||||
|
recipientFilter: null,
|
||||||
|
subscribeOnSignup: true,
|
||||||
|
sortOrder: 0,
|
||||||
|
|
||||||
|
headerImage: null,
|
||||||
|
showHeaderIcon: true,
|
||||||
|
showHeaderTitle: true,
|
||||||
|
titleFontCategory: 'sans_serif',
|
||||||
|
titleAlignment: 'center',
|
||||||
|
showFeatureImage: true,
|
||||||
|
bodyFontCategory: 'sans_serif',
|
||||||
|
footerContent: null,
|
||||||
|
showBadge: true
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -24,9 +24,7 @@ export default RestSerializer.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
serialize(object, request) {
|
serialize(object, request) {
|
||||||
// Ember expects pluralized responses for the post, user, and invite models,
|
if (this.isModel(object)) {
|
||||||
// and this shortcut will ensure that those models are pluralized
|
|
||||||
if (this.isModel(object) && ['post', 'user', 'invite'].includes(object.modelName)) {
|
|
||||||
object = new Collection(object.modelName, [object]);
|
object = new Collection(object.modelName, [object]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user