mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-03 23:00:14 -05:00
Allowed updating from address domain for member emails
refs https://github.com/TryGhost/Ghost/issues/11414 Confirms if the fromAddress for sending member emails is valid and accessible using magic link flow, allowing owners to update full from address including domain change. - Extends member service to handle magic link generation and validation for email update - Updates existing setting endpoint to not directly update from address - Adds new endpoint to send magic link to new address - Adds new endpoint for validating the magic link when clicked and update the new email for from address - Adds new email template for from address update email
This commit is contained in:
parent
4200eaf1f7
commit
bca41e1877
10 changed files with 369 additions and 2 deletions
|
@ -1,5 +1,6 @@
|
|||
const Promise = require('bluebird');
|
||||
const _ = require('lodash');
|
||||
const validator = require('validator');
|
||||
const models = require('../../models');
|
||||
const routing = require('../../../frontend/services/routing');
|
||||
const {i18n} = require('../../lib/common');
|
||||
|
@ -80,6 +81,75 @@ module.exports = {
|
|||
}
|
||||
},
|
||||
|
||||
validateMembersFromEmail: {
|
||||
options: [
|
||||
'token'
|
||||
],
|
||||
permissions: false,
|
||||
validation: {
|
||||
options: {
|
||||
token: {
|
||||
required: true
|
||||
}
|
||||
}
|
||||
},
|
||||
async query(frame) {
|
||||
// This is something you have to do if you want to use the "framework" with access to the raw req/res
|
||||
frame.response = async function (req, res) {
|
||||
try {
|
||||
const updatedFromAddress = membersService.settings.getEmailFromToken({token: frame.options.token});
|
||||
if (updatedFromAddress) {
|
||||
let subscriptionSetting = settingsCache.get('members_subscription_settings', {resolve: false});
|
||||
const settingsValue = subscriptionSetting.value ? JSON.parse(subscriptionSetting.value) : {};
|
||||
settingsValue.fromAddress = updatedFromAddress;
|
||||
return models.Settings.edit({
|
||||
key: 'members_subscription_settings',
|
||||
value: JSON.stringify(settingsValue)
|
||||
}).then(() => {
|
||||
// Redirect to Ghost-Admin settings page
|
||||
const adminLink = membersService.settings.getAdminRedirectLink();
|
||||
res.redirect(adminLink);
|
||||
});
|
||||
} else {
|
||||
return Promise.reject(new BadRequestError({
|
||||
message: 'Invalid token!'
|
||||
}));
|
||||
}
|
||||
} catch (err) {
|
||||
return Promise.reject(new BadRequestError({
|
||||
err,
|
||||
message: 'Invalid token!'
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
updateMembersFromEmail: {
|
||||
permissions: {
|
||||
method: 'edit'
|
||||
},
|
||||
async query(frame) {
|
||||
const email = frame.data.from_address;
|
||||
if (typeof email !== 'string' || !validator.isEmail(email)) {
|
||||
throw new BadRequestError({
|
||||
message: i18n.t('errors.api.settings.invalidEmailReceived')
|
||||
});
|
||||
}
|
||||
try {
|
||||
// Send magic link to update fromAddress
|
||||
await membersService.settings.sendFromAddressUpdateMagicLink({
|
||||
email
|
||||
});
|
||||
} catch (err) {
|
||||
throw new BadRequestError({
|
||||
err,
|
||||
message: i18n.t('errors.mail.failedSendingEmail.error')
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
edit: {
|
||||
headers: {
|
||||
cacheInvalidate: true
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const _ = require('lodash');
|
||||
const url = require('./utils/url');
|
||||
const settingsCache = require('../../../../../services/settings/cache');
|
||||
|
||||
module.exports = {
|
||||
read(apiConfig, frame) {
|
||||
|
@ -55,6 +56,17 @@ module.exports = {
|
|||
const {apiKey = '', domain = '', baseUrl = '', provider = 'mailgun'} = setting.value ? JSON.parse(setting.value) : {};
|
||||
setting.value = JSON.stringify({apiKey, domain, baseUrl, provider});
|
||||
}
|
||||
|
||||
//CASE: Ensure we don't update fromAddress for member as that goes through magic link flow
|
||||
if (setting.key === 'members_subscription_settings') {
|
||||
const memberSubscriptionSettings = setting.value ? JSON.parse(setting.value) : {};
|
||||
|
||||
let subscriptionSettingCache = settingsCache.get('members_subscription_settings', {resolve: false});
|
||||
const settingsCacheValue = subscriptionSettingCache.value ? JSON.parse(subscriptionSettingCache.value) : {};
|
||||
memberSubscriptionSettings.fromAddress = settingsCacheValue.fromAddress;
|
||||
|
||||
setting.value = JSON.stringify(memberSubscriptionSettings);
|
||||
}
|
||||
});
|
||||
|
||||
// CASE: deprecated, won't accept
|
||||
|
|
|
@ -38,8 +38,13 @@ class MembersConfigProvider {
|
|||
*/
|
||||
getEmailFromAddress() {
|
||||
const subscriptionSettings = this._settingsCache.get('members_subscription_settings') || {};
|
||||
const fromAddress = subscriptionSettings.fromAddress || 'noreply';
|
||||
|
||||
return `${subscriptionSettings.fromAddress || 'noreply'}@${this._getDomain()}`;
|
||||
// Any fromAddress without domain uses site domain, like default setting `noreply`
|
||||
if (fromAddress.indexOf('@') < 0) {
|
||||
return `${fromAddress}@${this._getDomain()}`;
|
||||
}
|
||||
return fromAddress;
|
||||
}
|
||||
|
||||
getPublicPlans() {
|
||||
|
|
168
core/server/services/members/emails/updateEmail.js
Normal file
168
core/server/services/members/emails/updateEmail.js
Normal file
|
@ -0,0 +1,168 @@
|
|||
module.exports = ({siteTitle, email, url}) => `
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>📫 Confirm your subscription to ${siteTitle}</title>
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
padding: 10px !important;
|
||||
}
|
||||
table[class=body] .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
table[class=body] .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
.recipient-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
#MessageViewBody a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
}
|
||||
hr {
|
||||
border-width: 0;
|
||||
height: 0;
|
||||
margin-top: 34px;
|
||||
margin-bottom: 34px;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: #EEF5F8;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="" style="background-color: #F4F8FB; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: #F4F8FB;">
|
||||
<tr>
|
||||
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;"> </td>
|
||||
<td class="container" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto; max-width: 600px; padding: 10px; width: 600px;">
|
||||
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 600px; padding: 30px 20px;">
|
||||
|
||||
<!-- START CENTERED WHITE CONTAINER -->
|
||||
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">You're tap away from updating your email address!</span>
|
||||
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 8px;">
|
||||
|
||||
<!-- START MAIN CONTENT AREA -->
|
||||
<tr>
|
||||
<td class="wrapper" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 40px 50px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
||||
<tr>
|
||||
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 20px; color: #15212A; font-weight: bold; line-height: 25px; margin: 0; margin-bottom: 15px;">Hey there,</p>
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 32px;">Please confirm your email address with this link:</p>
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top; padding-bottom: 35px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top; background-color: #15212A; border-radius: 5px; text-align: center;"> <a href="${url}" target="_blank" style="display: inline-block; color: #ffffff; background-color: #15212A; border: solid 1px #15212A; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 16px; font-weight: normal; margin: 0; padding: 9px 22px 10px; text-transform: capitalize; border-color: #15212A;">Confirm email address</a> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 25px;">For your security, the link will expire in 10 minutes time.</p>
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 30px;">All the best!<br/>The team at ${siteTitle}</p>
|
||||
<hr/>
|
||||
<p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 5px;">You can also copy & paste this URL into your browser:</p>
|
||||
<p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; line-height: 21px; margin-top: 0; color: #738A94;">${url}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
</table>
|
||||
|
||||
<!-- START FOOTER -->
|
||||
<div class="footer" style="clear: both; Margin-top: 10px; text-align: center; width: 100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
||||
<tr>
|
||||
<td class="content-block" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; vertical-align: top; padding-bottom: 5px; padding-top: 15px; font-size: 13px; line-height: 21px; color: #738A94; text-align: center;">
|
||||
If you did not make this request, you can simply delete this message.<br/>This email address will not be used.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="content-block" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 13px; color: #738A94; text-align: center;">
|
||||
<span class="recipient-link" style="color: #738A94; font-size: 13px; text-align: center;">Sent to <a href="mailto:${email}" style="text-decoration: underline; color: #738A94; font-size: 13px; text-align: center;">${email}</a></span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!-- END FOOTER -->
|
||||
|
||||
<!-- END CENTERED WHITE CONTAINER -->
|
||||
</div>
|
||||
</td>
|
||||
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
|
@ -2,6 +2,7 @@ const MembersSSR = require('@tryghost/members-ssr');
|
|||
|
||||
const MembersConfigProvider = require('./config');
|
||||
const createMembersApiInstance = require('./api');
|
||||
const createMembersSettingsInstance = require('./settings');
|
||||
const {events} = require('../../lib/common');
|
||||
const logging = require('../../../shared/logging');
|
||||
const urlUtils = require('../../../shared/url-utils');
|
||||
|
@ -18,6 +19,7 @@ const membersConfig = new MembersConfigProvider({
|
|||
});
|
||||
|
||||
let membersApi;
|
||||
let membersSettings;
|
||||
|
||||
// Bind to events to automatically keep subscription info up-to-date from settings
|
||||
events.on('settings.edited', function updateSettingFromModel(settingModel) {
|
||||
|
@ -50,6 +52,13 @@ const membersService = {
|
|||
return membersApi;
|
||||
},
|
||||
|
||||
get settings() {
|
||||
if (!membersSettings) {
|
||||
membersSettings = createMembersSettingsInstance(membersConfig);
|
||||
}
|
||||
return membersSettings;
|
||||
},
|
||||
|
||||
ssr: MembersSSR({
|
||||
cookieSecure: urlUtils.isSSL(urlUtils.getSiteUrl()),
|
||||
cookieKeys: [settingsCache.get('theme_session_secret')],
|
||||
|
|
99
core/server/services/members/settings.js
Normal file
99
core/server/services/members/settings.js
Normal file
|
@ -0,0 +1,99 @@
|
|||
const MagicLink = require('@tryghost/magic-link');
|
||||
const {URL} = require('url');
|
||||
const path = require('path');
|
||||
const urlUtils = require('../../../shared/url-utils');
|
||||
const settingsCache = require('../settings/cache');
|
||||
const logging = require('../../../shared/logging');
|
||||
const mail = require('../mail');
|
||||
const updateEmailTemplate = require('./emails/updateEmail');
|
||||
|
||||
const ghostMailer = new mail.GhostMailer();
|
||||
|
||||
function createSettingsInstance(config) {
|
||||
const {transporter, getSubject, getText, getHTML, getSigninURL} = {
|
||||
transporter: {
|
||||
sendMail(message) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logging.warn(message.text);
|
||||
}
|
||||
let msg = Object.assign({
|
||||
subject: 'Update email address',
|
||||
forceTextContent: true
|
||||
}, message);
|
||||
|
||||
return ghostMailer.send(msg);
|
||||
}
|
||||
},
|
||||
getSubject() {
|
||||
const siteTitle = settingsCache.get('title');
|
||||
|
||||
return `📫 Confirm your email update for ${siteTitle}`;
|
||||
},
|
||||
getText(url, type, email) {
|
||||
const siteTitle = settingsCache.get('title');
|
||||
|
||||
return `
|
||||
Hey there,
|
||||
|
||||
You're one tap away from updating your email at ${siteTitle} — please confirm your email address with this link:
|
||||
|
||||
${url}
|
||||
|
||||
For your security, the link will expire in 10 minutes time.
|
||||
|
||||
All the best!
|
||||
The team at ${siteTitle}
|
||||
|
||||
---
|
||||
|
||||
Sent to ${email}
|
||||
If you did not make this request, you can simply delete this message. You will not be subscribed.
|
||||
`;
|
||||
},
|
||||
getHTML(url, type, email) {
|
||||
const siteTitle = settingsCache.get('title');
|
||||
return updateEmailTemplate({url, email, siteTitle});
|
||||
},
|
||||
getSigninURL(token, type) {
|
||||
const signinURL = new URL(getApiUrl({version: 'v3', type: 'admin'}));
|
||||
signinURL.pathname = path.join(signinURL.pathname, '/settings/members/email/');
|
||||
signinURL.searchParams.set('token', token);
|
||||
signinURL.searchParams.set('action', type);
|
||||
return signinURL.href;
|
||||
}
|
||||
};
|
||||
|
||||
const getApiUrl = ({version, type}) => {
|
||||
return urlUtils.urlFor('api', {version: version, versionType: type}, true);
|
||||
};
|
||||
|
||||
const magicLinkService = new MagicLink({
|
||||
transporter,
|
||||
secret: config.getAuthSecret(),
|
||||
getSigninURL,
|
||||
getText,
|
||||
getHTML,
|
||||
getSubject
|
||||
});
|
||||
|
||||
const sendFromAddressUpdateMagicLink = ({email, payload = {}}) => {
|
||||
return magicLinkService.sendMagicLink({email, payload, subject: email, type: 'updateFromAddress'});
|
||||
};
|
||||
|
||||
const getEmailFromToken = ({token}) => {
|
||||
return magicLinkService.getUserFromToken(token);
|
||||
};
|
||||
|
||||
const getAdminRedirectLink = () => {
|
||||
const adminUrl = urlUtils.urlFor('admin', true);
|
||||
return urlUtils.urlJoin(adminUrl, 'settings/labs/?fromAddressUpdate=success');
|
||||
};
|
||||
|
||||
return {
|
||||
sendFromAddressUpdateMagicLink,
|
||||
getEmailFromToken,
|
||||
getAdminRedirectLink
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = createSettingsInstance;
|
|
@ -351,6 +351,7 @@
|
|||
"settings": {
|
||||
"problemFindingSetting": "Problem finding setting: {key}",
|
||||
"accessCoreSettingFromExtReq": "Attempted to access core setting from external request",
|
||||
"invalidEmailReceived": "Please send a valid email",
|
||||
"activeThemeSetViaAPI": {
|
||||
"error": "Attempted to change active_theme via settings API",
|
||||
"help": "Please activate theme via the themes API endpoints instead"
|
||||
|
|
|
@ -61,6 +61,8 @@ module.exports = function apiRoutes() {
|
|||
router.get('/settings', mw.authAdminApi, http(apiCanary.settings.browse));
|
||||
router.get('/settings/:key', mw.authAdminApi, http(apiCanary.settings.read));
|
||||
router.put('/settings', mw.authAdminApi, http(apiCanary.settings.edit));
|
||||
router.get('/settings/members/email', http(apiCanary.settings.validateMembersFromEmail));
|
||||
router.post('/settings/members/email', mw.authAdminApi, http(apiCanary.settings.updateMembersFromEmail));
|
||||
|
||||
// ## Users
|
||||
router.get('/users', mw.authAdminApi, http(apiCanary.users.browse));
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
"@tryghost/kg-default-cards": "2.0.2",
|
||||
"@tryghost/kg-markdown-html-renderer": "2.0.0",
|
||||
"@tryghost/kg-mobiledoc-html-renderer": "3.0.0",
|
||||
"@tryghost/magic-link": "^0.4.8",
|
||||
"@tryghost/members-api": "0.20.0",
|
||||
"@tryghost/members-ssr": "0.8.0",
|
||||
"@tryghost/mw-session-from-token": "0.1.4",
|
||||
|
|
|
@ -478,7 +478,7 @@
|
|||
|
||||
"@tryghost/magic-link@^0.4.8":
|
||||
version "0.4.8"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/magic-link/-/magic-link-0.4.8.tgz#70cd6fa7aeb433a8bb09eefab68b35db6a8d85e8"
|
||||
resolved "https://registry.npmjs.org/@tryghost/magic-link/-/magic-link-0.4.8.tgz#70cd6fa7aeb433a8bb09eefab68b35db6a8d85e8"
|
||||
integrity sha512-WvaXzT0v0T6Au0HRQS5wcoeEqjKT+qHZi9Lo9yCOppxra7QqSywpxrv+SZI3GOQo8yjZ1moVcqF4T5YB0UH+dQ==
|
||||
dependencies:
|
||||
bluebird "^3.5.5"
|
||||
|
|
Loading…
Add table
Reference in a new issue