mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -05:00
Merge branch 'master' into v3
This commit is contained in:
commit
78e16ddd3f
34 changed files with 868 additions and 807 deletions
|
@ -1 +1 @@
|
|||
Subproject commit 2e91cb09bf9ea348552eae7daede465f227cb4b3
|
||||
Subproject commit 1705a622d386bb034aecebeaa4857463c3c3c3a4
|
|
@ -60,30 +60,20 @@ const members = {
|
|||
}
|
||||
},
|
||||
permissions: true,
|
||||
query(frame) {
|
||||
// NOTE: Promise.resolve() is here for a reason! Method has to return an instance
|
||||
// of a Bluebird promise to allow reflection. If decided to be replaced
|
||||
// with something else, e.g: async/await, CSV export function
|
||||
// would need a deep rewrite (see failing tests if this line is removed)
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
return membersService.api.members.create(frame.data.members[0], {
|
||||
sendEmail: frame.options.send_email,
|
||||
emailType: frame.options.email_type
|
||||
});
|
||||
})
|
||||
.then((member) => {
|
||||
if (member) {
|
||||
return Promise.resolve(member);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.code && error.message.toLowerCase().indexOf('unique') !== -1) {
|
||||
return Promise.reject(new common.errors.ValidationError({message: common.i18n.t('errors.api.members.memberAlreadyExists')}));
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
async query(frame) {
|
||||
try {
|
||||
const member = await membersService.api.members.create(frame.data.members[0], {
|
||||
sendEmail: frame.options.send_email,
|
||||
emailType: frame.options.email_type
|
||||
});
|
||||
return member;
|
||||
} catch (error) {
|
||||
if (error.code && error.message.toLowerCase().indexOf('unique') !== -1) {
|
||||
throw new common.errors.ValidationError({message: common.i18n.t('errors.api.members.memberAlreadyExists')});
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -163,23 +153,24 @@ const members = {
|
|||
|
||||
return fsLib.readCSV({
|
||||
path: filePath,
|
||||
columnsToExtract: [{name: 'email', lookup: /email/i}, {name: 'name', lookup: /name/i}]
|
||||
columnsToExtract: [{name: 'email', lookup: /email/i}, {name: 'name', lookup: /name/i}, {name: 'note', lookup: /note/i}]
|
||||
}).then((result) => {
|
||||
return Promise.all(result.map((entry) => {
|
||||
const api = require('./index');
|
||||
|
||||
return api.members.add.query({
|
||||
return Promise.resolve(api.members.add.query({
|
||||
data: {
|
||||
members: [{
|
||||
email: entry.email,
|
||||
name: entry.name
|
||||
name: entry.name,
|
||||
note: entry.note
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
context: frame.options.context,
|
||||
options: {send_email: false}
|
||||
}
|
||||
}).reflect();
|
||||
})).reflect();
|
||||
})).each((inspection) => {
|
||||
if (inspection.isFulfilled()) {
|
||||
fulfilled = fulfilled + 1;
|
||||
|
|
|
@ -41,7 +41,7 @@ module.exports = {
|
|||
exportCSV(models, apiConfig, frame) {
|
||||
debug('exportCSV');
|
||||
|
||||
const fields = ['id', 'email', 'name', 'created_at', 'deleted_at'];
|
||||
const fields = ['id', 'email', 'name', 'note', 'created_at', 'deleted_at'];
|
||||
|
||||
function formatCSV(data) {
|
||||
let csv = `${fields.join(',')}\r\n`,
|
||||
|
|
|
@ -94,8 +94,6 @@ const post = (attrs, frame) => {
|
|||
if (attrs.og_description === '') {
|
||||
attrs.og_description = null;
|
||||
}
|
||||
|
||||
delete attrs.visibility;
|
||||
} else {
|
||||
delete attrs.page;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 191,
|
||||
"pattern": "^([^,]|$)"
|
||||
},
|
||||
|
@ -21,6 +20,11 @@
|
|||
"maxLength": 191,
|
||||
"pattern": "^([^,]|$)"
|
||||
},
|
||||
"note": {
|
||||
"type": "string",
|
||||
"minLength": 0,
|
||||
"maxLength": 2000
|
||||
},
|
||||
"id": {
|
||||
"strip": true
|
||||
},
|
||||
|
|
|
@ -67,10 +67,6 @@ module.exports = {
|
|||
return shared.pipeline(require('./settings'), localUtils);
|
||||
},
|
||||
|
||||
get members() {
|
||||
return shared.pipeline(require('./members'), localUtils);
|
||||
},
|
||||
|
||||
get images() {
|
||||
return shared.pipeline(require('./images'), localUtils);
|
||||
},
|
||||
|
|
|
@ -1,209 +0,0 @@
|
|||
// NOTE: We must not cache references to membersService.api
|
||||
// as it is a getter and may change during runtime.
|
||||
const Promise = require('bluebird');
|
||||
const membersService = require('../../services/members');
|
||||
const common = require('../../lib/common');
|
||||
const fsLib = require('../../lib/fs');
|
||||
|
||||
const members = {
|
||||
docName: 'members',
|
||||
browse: {
|
||||
options: [
|
||||
'limit',
|
||||
'fields',
|
||||
'filter',
|
||||
'order',
|
||||
'debug',
|
||||
'page'
|
||||
],
|
||||
permissions: true,
|
||||
validation: {},
|
||||
query(frame) {
|
||||
return membersService.api.members.list(frame.options);
|
||||
}
|
||||
},
|
||||
|
||||
read: {
|
||||
headers: {},
|
||||
data: [
|
||||
'id',
|
||||
'email'
|
||||
],
|
||||
validation: {},
|
||||
permissions: true,
|
||||
async query(frame) {
|
||||
const member = await membersService.api.members.get(frame.data, frame.options);
|
||||
if (!member) {
|
||||
throw new common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.members.memberNotFound')
|
||||
});
|
||||
}
|
||||
return member;
|
||||
}
|
||||
},
|
||||
|
||||
add: {
|
||||
statusCode: 201,
|
||||
headers: {},
|
||||
options: [
|
||||
'send_email',
|
||||
'email_type'
|
||||
],
|
||||
validation: {
|
||||
data: {
|
||||
email: {required: true}
|
||||
},
|
||||
options: {
|
||||
email_type: {
|
||||
values: ['signin', 'signup', 'subscribe']
|
||||
}
|
||||
}
|
||||
},
|
||||
permissions: true,
|
||||
query(frame) {
|
||||
// NOTE: Promise.resolve() is here for a reason! Method has to return an instance
|
||||
// of a Bluebird promise to allow reflection. If decided to be replaced
|
||||
// with something else, e.g: async/await, CSV export function
|
||||
// would need a deep rewrite (see failing tests if this line is removed)
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
return membersService.api.members.create(frame.data.members[0], {
|
||||
sendEmail: frame.options.send_email,
|
||||
emailType: frame.options.email_type
|
||||
});
|
||||
})
|
||||
.then((member) => {
|
||||
if (member) {
|
||||
return Promise.resolve(member);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.code && error.message.toLowerCase().indexOf('unique') !== -1) {
|
||||
return Promise.reject(new common.errors.ValidationError({message: common.i18n.t('errors.api.members.memberAlreadyExists')}));
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
edit: {
|
||||
statusCode: 200,
|
||||
headers: {},
|
||||
options: [
|
||||
'id'
|
||||
],
|
||||
validation: {
|
||||
options: {
|
||||
id: {
|
||||
required: true
|
||||
}
|
||||
}
|
||||
},
|
||||
permissions: true,
|
||||
async query(frame) {
|
||||
const member = await membersService.api.members.update(frame.data.members[0], frame.options);
|
||||
return member;
|
||||
}
|
||||
},
|
||||
|
||||
destroy: {
|
||||
statusCode: 204,
|
||||
headers: {},
|
||||
options: [
|
||||
'id'
|
||||
],
|
||||
validation: {
|
||||
options: {
|
||||
id: {
|
||||
required: true
|
||||
}
|
||||
}
|
||||
},
|
||||
permissions: true,
|
||||
async query(frame) {
|
||||
frame.options.require = true;
|
||||
await membersService.api.members.destroy(frame.options);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
exportCSV: {
|
||||
headers: {
|
||||
disposition: {
|
||||
type: 'csv',
|
||||
value() {
|
||||
const datetime = (new Date()).toJSON().substring(0, 10);
|
||||
return `members.${datetime}.csv`;
|
||||
}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
format: 'plain'
|
||||
},
|
||||
permissions: {
|
||||
method: 'browse'
|
||||
},
|
||||
validation: {},
|
||||
query(frame) {
|
||||
return membersService.api.members.list(frame.options);
|
||||
}
|
||||
},
|
||||
|
||||
importCSV: {
|
||||
statusCode: 201,
|
||||
permissions: {
|
||||
method: 'add'
|
||||
},
|
||||
async query(frame) {
|
||||
let filePath = frame.file.path,
|
||||
fulfilled = 0,
|
||||
invalid = 0,
|
||||
duplicates = 0;
|
||||
|
||||
return fsLib.readCSV({
|
||||
path: filePath,
|
||||
columnsToExtract: [{name: 'email', lookup: /email/i}, {name: 'name', lookup: /name/i}]
|
||||
}).then((result) => {
|
||||
return Promise.all(result.map((entry) => {
|
||||
const api = require('./index');
|
||||
|
||||
return api.members.add.query({
|
||||
data: {
|
||||
members: [{
|
||||
email: entry.email,
|
||||
name: entry.name
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
context: frame.options.context,
|
||||
options: {send_email: false}
|
||||
}
|
||||
}).reflect();
|
||||
})).each((inspection) => {
|
||||
if (inspection.isFulfilled()) {
|
||||
fulfilled = fulfilled + 1;
|
||||
} else {
|
||||
if (inspection.reason() instanceof common.errors.ValidationError) {
|
||||
duplicates = duplicates + 1;
|
||||
} else {
|
||||
invalid = invalid + 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}).then(() => {
|
||||
return {
|
||||
meta: {
|
||||
stats: {
|
||||
imported: fulfilled,
|
||||
duplicates: duplicates,
|
||||
invalid: invalid
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = members;
|
|
@ -1,77 +0,0 @@
|
|||
const common = require('../../../../../lib/common');
|
||||
const debug = require('ghost-ignition').debug('api:v2:utils:serializers:output:members');
|
||||
|
||||
module.exports = {
|
||||
browse(data, apiConfig, frame) {
|
||||
debug('browse');
|
||||
|
||||
frame.response = data;
|
||||
},
|
||||
|
||||
add(data, apiConfig, frame) {
|
||||
debug('add');
|
||||
|
||||
frame.response = {
|
||||
members: [data]
|
||||
};
|
||||
},
|
||||
|
||||
edit(data, apiConfig, frame) {
|
||||
debug('edit');
|
||||
|
||||
frame.response = {
|
||||
members: [data]
|
||||
};
|
||||
},
|
||||
|
||||
read(data, apiConfig, frame) {
|
||||
debug('read');
|
||||
|
||||
if (!data) {
|
||||
return Promise.reject(new common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.members.memberNotFound')
|
||||
}));
|
||||
}
|
||||
|
||||
frame.response = {
|
||||
members: [data]
|
||||
};
|
||||
},
|
||||
|
||||
exportCSV(models, apiConfig, frame) {
|
||||
debug('exportCSV');
|
||||
|
||||
const fields = ['id', 'email', 'name', 'created_at', 'deleted_at'];
|
||||
|
||||
function formatCSV(data) {
|
||||
let csv = `${fields.join(',')}\r\n`,
|
||||
entry,
|
||||
field,
|
||||
j,
|
||||
i;
|
||||
|
||||
for (j = 0; j < data.length; j = j + 1) {
|
||||
entry = data[j];
|
||||
|
||||
for (i = 0; i < fields.length; i = i + 1) {
|
||||
field = fields[i];
|
||||
csv += entry[field] !== null ? entry[field] : '';
|
||||
if (i !== fields.length - 1) {
|
||||
csv += ',';
|
||||
}
|
||||
}
|
||||
csv += '\r\n';
|
||||
}
|
||||
|
||||
return csv;
|
||||
}
|
||||
|
||||
frame.response = formatCSV(models.members);
|
||||
},
|
||||
|
||||
importCSV(data, apiConfig, frame) {
|
||||
debug('importCSV');
|
||||
|
||||
frame.response = data;
|
||||
}
|
||||
};
|
|
@ -1,15 +0,0 @@
|
|||
const jsonSchema = require('../utils/json-schema');
|
||||
|
||||
module.exports = {
|
||||
add(apiConfig, frame) {
|
||||
const schema = require('./schemas/members-add');
|
||||
const definitions = require('./schemas/members');
|
||||
return jsonSchema.validate(schema, definitions, frame.data);
|
||||
},
|
||||
|
||||
edit(apiConfig, frame) {
|
||||
const schema = require('./schemas/members-edit');
|
||||
const definitions = require('./schemas/members');
|
||||
return jsonSchema.validate(schema, definitions, frame.data);
|
||||
}
|
||||
};
|
|
@ -1,22 +0,0 @@
|
|||
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "members.add",
|
||||
"title": "members.add",
|
||||
"description": "Schema for members.add",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"members": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"allOf": [{"$ref": "members#/definitions/member"}],
|
||||
"required": ["email"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["members"]
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "members.edit",
|
||||
"title": "members.edit",
|
||||
"description": "Schema for members.edit",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"members": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"allOf": [{"$ref": "members#/definitions/member"}]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["members"]
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "members",
|
||||
"title": "members",
|
||||
"description": "Base members definitions",
|
||||
"definitions": {
|
||||
"member": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 191,
|
||||
"pattern": "^([^,]|$)"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 191,
|
||||
"pattern": "^([^,]|$)"
|
||||
},
|
||||
"id": {
|
||||
"strip": true
|
||||
},
|
||||
"created_at": {
|
||||
"strip": true
|
||||
},
|
||||
"created_by": {
|
||||
"strip": true
|
||||
},
|
||||
"updated_at": {
|
||||
"strip": true
|
||||
},
|
||||
"updated_by": {
|
||||
"strip": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
const commands = require('../../../schema').commands;
|
||||
|
||||
module.exports = {
|
||||
|
||||
up: commands.createColumnMigration({
|
||||
table: 'members',
|
||||
column: 'note',
|
||||
dbIsInCorrectState(hasColumn) {
|
||||
return hasColumn === true;
|
||||
},
|
||||
operation: commands.addColumn,
|
||||
operationVerb: 'Adding'
|
||||
}),
|
||||
|
||||
down: commands.createColumnMigration({
|
||||
table: 'members',
|
||||
column: 'note',
|
||||
dbIsInCorrectState(hasColumn) {
|
||||
return hasColumn === false;
|
||||
},
|
||||
operation: commands.dropColumn,
|
||||
operationVerb: 'Dropping'
|
||||
}),
|
||||
|
||||
config: {
|
||||
transaction: true
|
||||
}
|
||||
};
|
|
@ -194,11 +194,14 @@
|
|||
"members_session_secret": {
|
||||
"defaultValue": null
|
||||
},
|
||||
"members_email_auth_secret": {
|
||||
"defaultValue": null
|
||||
},
|
||||
"default_content_visibility": {
|
||||
"defaultValue": "public"
|
||||
},
|
||||
"members_subscription_settings": {
|
||||
"defaultValue": "{\"isPaid\":false,\"fromAddress\":\"noreply\",\"requirePaymentForSignup\":false,\"paymentProcessors\":[{\"adapter\":\"stripe\",\"config\":{\"secret_token\":\"\",\"public_token\":\"\",\"product\":{\"name\":\"Ghost Subscription\"},\"plans\":[{\"name\":\"Monthly\",\"currency\":\"usd\",\"interval\":\"month\",\"amount\":\"\"},{\"name\":\"Yearly\",\"currency\":\"usd\",\"interval\":\"year\",\"amount\":\"\"}]}}]}"
|
||||
"defaultValue": "{\"isPaid\":false,\"fromAddress\":\"noreply\",\"allowSelfSignup\":true,\"paymentProcessors\":[{\"adapter\":\"stripe\",\"config\":{\"secret_token\":\"\",\"public_token\":\"\",\"product\":{\"name\":\"Ghost Subscription\"},\"plans\":[{\"name\":\"Monthly\",\"currency\":\"usd\",\"interval\":\"month\",\"amount\":\"\"},{\"name\":\"Yearly\",\"currency\":\"usd\",\"interval\":\"year\",\"amount\":\"\"}]}}]}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -323,6 +323,7 @@ module.exports = {
|
|||
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
||||
email: {type: 'string', maxlength: 191, nullable: false, unique: true, validations: {isEmail: true}},
|
||||
name: {type: 'string', maxlength: 191, nullable: true},
|
||||
note: {type: 'string', maxlength: 2000, nullable: true},
|
||||
created_at: {type: 'dateTime', nullable: false},
|
||||
created_by: {type: 'string', maxlength: 24, nullable: false},
|
||||
updated_at: {type: 'dateTime', nullable: true},
|
||||
|
|
44
core/server/lib/mobiledoc/cards/_markdown.js
Normal file
44
core/server/lib/mobiledoc/cards/_markdown.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
// this is a function so that when it's aliased across multiple cards we do not
|
||||
// end up modifying the object by reference
|
||||
module.exports = function markdownCardDefinition() {
|
||||
return {
|
||||
name: 'markdown',
|
||||
type: 'dom',
|
||||
config: {
|
||||
commentWrapper: true
|
||||
},
|
||||
|
||||
render: function (opts) {
|
||||
let converters = require('../converters');
|
||||
let payload = opts.payload;
|
||||
let version = opts.options && opts.options.version || 2;
|
||||
// convert markdown to HTML ready for insertion into dom
|
||||
let html = converters.markdownConverter.render(payload.markdown || '');
|
||||
|
||||
if (!html) {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Ghost 1.0's markdown-only renderer wrapped cards. Remove in Ghost 3.0
|
||||
*/
|
||||
if (version === 1) {
|
||||
html = `<div class="kg-card-markdown">${html}</div>`;
|
||||
}
|
||||
|
||||
// use the SimpleDOM document to create a raw HTML section.
|
||||
// avoids parsing/rendering of potentially broken or unsupported HTML
|
||||
return opts.env.dom.createRawHTMLSection(html);
|
||||
},
|
||||
|
||||
absoluteToRelative(urlUtils, payload, options) {
|
||||
payload.markdown = payload.markdown && urlUtils.markdownAbsoluteToRelative(payload.markdown, options);
|
||||
return payload;
|
||||
},
|
||||
|
||||
relativeToAbsolute(urlUtils, payload, options) {
|
||||
payload.markdown = payload.markdown && urlUtils.markdownRelativeToAbsolute(payload.markdown, options);
|
||||
return payload;
|
||||
}
|
||||
};
|
||||
};
|
|
@ -1,8 +1,9 @@
|
|||
// this card is just an alias of the `markdown` card which is necessary because
|
||||
// our markdown-only editor was using the `card-markdown` card name
|
||||
const markdownCard = require('./markdown');
|
||||
const markdownCard = require('./_markdown');
|
||||
const createCard = require('../create-card');
|
||||
|
||||
module.exports = createCard(
|
||||
Object.assign({}, markdownCard, {name: 'card-markdown'})
|
||||
);
|
||||
const v1CompatMarkdownCard = markdownCard();
|
||||
v1CompatMarkdownCard.name = 'card-markdown';
|
||||
|
||||
module.exports = createCard(v1CompatMarkdownCard);
|
||||
|
|
|
@ -1,33 +1,6 @@
|
|||
const markdownCard = require('./_markdown');
|
||||
const createCard = require('../create-card');
|
||||
|
||||
module.exports = createCard({
|
||||
name: 'markdown',
|
||||
type: 'dom',
|
||||
config: {
|
||||
commentWrapper: true
|
||||
},
|
||||
render: function (opts) {
|
||||
let converters = require('../converters');
|
||||
let payload = opts.payload;
|
||||
// convert markdown to HTML ready for insertion into dom
|
||||
let html = converters.markdownConverter.render(payload.markdown || '');
|
||||
|
||||
if (!html) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// use the SimpleDOM document to create a raw HTML section.
|
||||
// avoids parsing/rendering of potentially broken or unsupported HTML
|
||||
return opts.env.dom.createRawHTMLSection(html);
|
||||
},
|
||||
|
||||
absoluteToRelative(urlUtils, payload, options) {
|
||||
payload.markdown = payload.markdown && urlUtils.markdownAbsoluteToRelative(payload.markdown, options);
|
||||
return payload;
|
||||
},
|
||||
|
||||
relativeToAbsolute(urlUtils, payload, options) {
|
||||
payload.markdown = payload.markdown && urlUtils.markdownRelativeToAbsolute(payload.markdown, options);
|
||||
return payload;
|
||||
}
|
||||
});
|
||||
// uses the _markdown card definition function so that the card definition
|
||||
// can be re-used in aliased cards
|
||||
module.exports = createCard(markdownCard());
|
||||
|
|
|
@ -1,24 +1,7 @@
|
|||
const ghostBookshelf = require('./base');
|
||||
|
||||
const Member = ghostBookshelf.Model.extend({
|
||||
tableName: 'members',
|
||||
|
||||
relationships: ['stripe_customers'],
|
||||
relationshipBelongsTo: {
|
||||
stripe_customers: 'members_stripe_customers'
|
||||
},
|
||||
|
||||
permittedAttributes(...args) {
|
||||
return ghostBookshelf.Model.prototype.permittedAttributes.apply(this, args).concat(this.relationships);
|
||||
},
|
||||
|
||||
stripe_customers() {
|
||||
return this.hasMany('MemberStripeCustomer', 'member_id');
|
||||
}
|
||||
}, {
|
||||
permittedOptions(...args) {
|
||||
return ghostBookshelf.Model.permittedOptions.apply(this, args).concat(['withRelated']);
|
||||
}
|
||||
tableName: 'members'
|
||||
});
|
||||
|
||||
const Members = ghostBookshelf.Collection.extend({
|
||||
|
|
|
@ -37,7 +37,8 @@ function parseDefaultSettings() {
|
|||
members_session_secret: () => crypto.randomBytes(32).toString('hex'),
|
||||
theme_session_secret: () => crypto.randomBytes(32).toString('hex'),
|
||||
members_public_key: () => getMembersKey('public'),
|
||||
members_private_key: () => getMembersKey('private')
|
||||
members_private_key: () => getMembersKey('private'),
|
||||
members_email_auth_secret: () => crypto.randomBytes(64).toString('hex')
|
||||
};
|
||||
|
||||
_.each(defaultSettingsInCategories, function each(settings, categoryName) {
|
||||
|
|
|
@ -36,10 +36,12 @@ function getFromAddress(requestedFromAddress) {
|
|||
}
|
||||
|
||||
function createMessage(message) {
|
||||
const encoding = 'base64';
|
||||
const generateTextFromHTML = !message.forceTextContent;
|
||||
return Object.assign({}, message, {
|
||||
from: getFromAddress(),
|
||||
generateTextFromHTML: true,
|
||||
encoding: 'base64'
|
||||
from: getFromAddress(message.from),
|
||||
generateTextFromHTML,
|
||||
encoding
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
const crypto = require('crypto');
|
||||
const {URL} = require('url');
|
||||
const settingsCache = require('../settings/cache');
|
||||
const urlUtils = require('../../lib/url-utils');
|
||||
|
@ -6,26 +7,30 @@ const common = require('../../lib/common');
|
|||
const ghostVersion = require('../../lib/ghost-version');
|
||||
const mail = require('../mail');
|
||||
const models = require('../../models');
|
||||
const signinEmail = require('./emails/signin');
|
||||
const signupEmail = require('./emails/signup');
|
||||
const subscribeEmail = require('./emails/subscribe');
|
||||
|
||||
function createMember({email, name}) {
|
||||
return models.Member.add({
|
||||
async function createMember({email, name, note}, options = {}) {
|
||||
const model = await models.Member.add({
|
||||
email,
|
||||
name
|
||||
}).then((member) => {
|
||||
return member.toJSON();
|
||||
name: name || null,
|
||||
note: note || null
|
||||
});
|
||||
const member = model.toJSON(options);
|
||||
return member;
|
||||
}
|
||||
|
||||
function getMember(data, options = {}) {
|
||||
async function getMember(data, options = {}) {
|
||||
if (!data.email && !data.id) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return models.Member.findOne(data, options).then((model) => {
|
||||
if (!model) {
|
||||
return null;
|
||||
}
|
||||
return model.toJSON(options);
|
||||
});
|
||||
const model = await models.Member.findOne(data, options);
|
||||
if (!model) {
|
||||
return null;
|
||||
}
|
||||
const member = model.toJSON(options);
|
||||
return member;
|
||||
}
|
||||
|
||||
async function setMetadata(module, metadata) {
|
||||
|
@ -70,8 +75,14 @@ async function getMetadata(module, member) {
|
|||
};
|
||||
}
|
||||
|
||||
function updateMember({name}, options) {
|
||||
return models.Member.edit({name}, options);
|
||||
async function updateMember({name, note}, options = {}) {
|
||||
const model = await models.Member.edit({
|
||||
name: name || null,
|
||||
note: note || null
|
||||
}, options);
|
||||
|
||||
const member = model.toJSON(options);
|
||||
return member;
|
||||
}
|
||||
|
||||
function deleteMember(options) {
|
||||
|
@ -146,9 +157,30 @@ function getStripePaymentConfig() {
|
|||
};
|
||||
}
|
||||
|
||||
function getRequirePaymentSetting() {
|
||||
function getAuthSecret() {
|
||||
const hexSecret = settingsCache.get('members_email_auth_secret');
|
||||
if (!hexSecret) {
|
||||
common.logging.warn('Could not find members_email_auth_secret, using dynamically generated secret');
|
||||
return crypto.randomBytes(64);
|
||||
}
|
||||
const secret = Buffer.from(hexSecret, 'hex');
|
||||
if (secret.length < 64) {
|
||||
common.logging.warn('members_email_auth_secret not large enough (64 bytes), using dynamically generated secret');
|
||||
return crypto.randomBytes(64);
|
||||
}
|
||||
return secret;
|
||||
}
|
||||
|
||||
function getAllowSelfSignup() {
|
||||
const subscriptionSettings = settingsCache.get('members_subscription_settings');
|
||||
return !!subscriptionSettings.requirePaymentForSignup;
|
||||
return subscriptionSettings.allowSelfSignup;
|
||||
}
|
||||
|
||||
// NOTE: the function is an exact duplicate of one in GhostMailer should be extracted
|
||||
// into a common lib once it needs to be reused anywhere else again
|
||||
function getDomain() {
|
||||
const domain = urlUtils.urlFor('home', true).match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i'));
|
||||
return domain && domain[1];
|
||||
}
|
||||
|
||||
module.exports = createApiInstance;
|
||||
|
@ -167,7 +199,8 @@ function createApiInstance() {
|
|||
signinURL.searchParams.set('action', type);
|
||||
return signinURL.href;
|
||||
},
|
||||
allowSelfSignup: !getRequirePaymentSetting()
|
||||
allowSelfSignup: getAllowSelfSignup(),
|
||||
secret: getAuthSecret()
|
||||
},
|
||||
mail: {
|
||||
transporter: {
|
||||
|
@ -175,29 +208,102 @@ function createApiInstance() {
|
|||
if (process.env.NODE_ENV !== 'production') {
|
||||
common.logging.warn(message.text);
|
||||
}
|
||||
return ghostMailer.send(Object.assign({subject: 'Signin'}, message));
|
||||
let msg = Object.assign({
|
||||
subject: 'Signin',
|
||||
forceTextContent: true
|
||||
}, message);
|
||||
const subscriptionSettings = settingsCache.get('members_subscription_settings');
|
||||
|
||||
if (subscriptionSettings && subscriptionSettings.fromAddress) {
|
||||
let from = `${subscriptionSettings.fromAddress}@${getDomain()}`;
|
||||
msg = Object.assign({from: from}, msg);
|
||||
}
|
||||
|
||||
return ghostMailer.send(msg);
|
||||
}
|
||||
},
|
||||
getText(url, type) {
|
||||
getSubject(type) {
|
||||
const siteTitle = settingsCache.get('title');
|
||||
switch (type) {
|
||||
case 'subscribe':
|
||||
return `Click here to confirm your subscription ${url}`;
|
||||
return `📫 Confirm your subscription to ${siteTitle}`;
|
||||
case 'signup':
|
||||
return `Click here to confirm your email address and sign up ${url}`;
|
||||
return `🙌 Complete your sign up to ${siteTitle}!`;
|
||||
case 'signin':
|
||||
default:
|
||||
return `Click here to sign in ${url}`;
|
||||
return `🔑 Secure sign in link for ${siteTitle}`;
|
||||
}
|
||||
},
|
||||
getHTML(url, type) {
|
||||
getText(url, type, email) {
|
||||
const siteTitle = settingsCache.get('title');
|
||||
switch (type) {
|
||||
case 'subscribe':
|
||||
return `<a href="${url}">Click here to confirm your subscription</a>`;
|
||||
return `
|
||||
Hey there,
|
||||
|
||||
You're one tap away from subscribing to ${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.
|
||||
`;
|
||||
case 'signup':
|
||||
return `<a href="${url}">Click here to confirm your email address and sign up</a>`;
|
||||
return `
|
||||
Hey there!
|
||||
|
||||
Thanks for signing up for ${siteTitle} — use this link to complete the sign up process and be automatically signed in:
|
||||
|
||||
${url}
|
||||
|
||||
For your security, the link will expire in 10 minutes time.
|
||||
|
||||
See you soon!
|
||||
The team at ${siteTitle}
|
||||
|
||||
---
|
||||
|
||||
Sent to ${email}
|
||||
If you did not make this request, you can simply delete this message. You will not be signed up, and no account will be created for you.
|
||||
`;
|
||||
case 'signin':
|
||||
default:
|
||||
return `<a href="${url}">Click here to sign in</a>`;
|
||||
return `
|
||||
Hey there,
|
||||
|
||||
Welcome back! Use this link to securely sign in to your ${siteTitle} account:
|
||||
|
||||
${url}
|
||||
|
||||
For your security, the link will expire in 10 minutes time.
|
||||
|
||||
See you soon!
|
||||
The team at ${siteTitle}
|
||||
|
||||
---
|
||||
|
||||
Sent to ${email}
|
||||
If you did not make this request, you can safely ignore this email.
|
||||
`;
|
||||
}
|
||||
},
|
||||
getHTML(url, type, email) {
|
||||
const siteTitle = settingsCache.get('title');
|
||||
switch (type) {
|
||||
case 'subscribe':
|
||||
return subscribeEmail({url, email, siteTitle});
|
||||
case 'signup':
|
||||
return signupEmail({url, email, siteTitle});
|
||||
case 'signin':
|
||||
default:
|
||||
return signinEmail({url, email, siteTitle});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
169
core/server/services/members/emails/signin.js
Normal file
169
core/server/services/members/emails/signin.js
Normal file
|
@ -0,0 +1,169 @@
|
|||
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>🔑 Secure sign in link for ${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;">Welcome back to ${siteTitle}!</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; font-weight: bold; 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; Margin-bottom: 30px;">Welcome back! Use this link to securely sign in to your ${siteTitle} account:</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: 30px;">
|
||||
<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; border-color: #15212A;">Sign in to ${siteTitle}</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; Margin-bottom: 30px;">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; Margin-bottom: 30px;">See you soon!<br/>The team at ${siteTitle}</p>
|
||||
<hr/>
|
||||
<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; Margin-bottom: 30px;">You can also copy & paste this URL into your browser:</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: 12px; 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 powered-by" 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: 15px; font-size: 12px; color: #738A94; text-align: center;">
|
||||
If you did not make this request, you can safely ignore this email.
|
||||
</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: 15px; font-size: 12px; color: #738A94; text-align: center;">
|
||||
<span class="recipient-link" style="color: #738A94; font-size: 12px; text-align: center;">Sent to <a href="mailto:${email}" style="text-decoration: underline; color: #738A94; font-size: 12px; 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>
|
||||
`;
|
169
core/server/services/members/emails/signup.js
Normal file
169
core/server/services/members/emails/signup.js
Normal file
|
@ -0,0 +1,169 @@
|
|||
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>🙌 Complete your sign up 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;">Thanks for signing up for ${siteTitle}!</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; font-weight: bold; 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; Margin-bottom: 30px;">Thanks for signing up for ${siteTitle} — use this link to complete the sign up process and be automatically signed in:</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: 30px;">
|
||||
<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; border-color: #15212A;">Activate my account</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; Margin-bottom: 30px;">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; Margin-bottom: 30px;">See you soon!<br/>The team at ${siteTitle}</p>
|
||||
<hr/>
|
||||
<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; Margin-bottom: 30px;">You can also copy & paste this URL into your browser:</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: 12px; 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: 10px; padding-top: 15px; font-size: 12px; color: #738A94; text-align: center;">
|
||||
If you did not make this request, you can simply delete this message. <br/>You will not be signed up, and no account will be created for you.
|
||||
</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: 15px; font-size: 12px; color: #738A94; text-align: center;">
|
||||
<span class="recipient-link" style="color: #738A94; font-size: 12px; text-align: center;">Sent to <a href="mailto:${email}" style="text-decoration: underline; color: #738A94; font-size: 12px; 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>
|
||||
`;
|
169
core/server/services/members/emails/subscribe.js
Normal file
169
core/server/services/members/emails/subscribe.js
Normal file
|
@ -0,0 +1,169 @@
|
|||
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 one tap away from subscribing to ${siteTitle}!</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; font-weight: bold; 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; Margin-bottom: 30px;">You're one tap away from subscribing to ${siteTitle} — 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: 30px;">
|
||||
<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; border-color: #15212A;">Yes, I want to subscribe</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; Margin-bottom: 30px;">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; Margin-bottom: 30px;">All the best!<br/>The team at ${siteTitle}</p>
|
||||
<hr/>
|
||||
<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: 14px; font-weight: normal; margin: 0; Margin-bottom: 30px;">You can also copy & paste this URL into your browser:</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: 12px; 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: 10px; padding-top: 15px; font-size: 12px; color: #738A94; text-align: center;">
|
||||
If you did not make this request, you can simply delete this message.<br/>You will not be subscribed.
|
||||
</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: 15px; font-size: 12px; color: #738A94; text-align: center;">
|
||||
<span class="recipient-link" style="color: #738A94; font-size: 12px; text-align: center;">Sent to <a href="mailto:${email}" style="text-decoration: underline; color: #738A94; font-size: 12px; 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>
|
||||
`;
|
|
@ -83,23 +83,6 @@ module.exports = function apiRoutes() {
|
|||
router.put('/tags/:id', mw.authAdminApi, http(apiv2.tags.edit));
|
||||
router.del('/tags/:id', mw.authAdminApi, http(apiv2.tags.destroy));
|
||||
|
||||
// ## Members
|
||||
router.get('/members', shared.middlewares.labs.members, mw.authAdminApi, http(apiv2.members.browse));
|
||||
router.post('/members', shared.middlewares.labs.members, mw.authAdminApi, http(apiv2.members.add));
|
||||
|
||||
router.get('/members/csv', shared.middlewares.labs.members, mw.authAdminApi, http(apiv2.members.exportCSV));
|
||||
router.post('/members/csv',
|
||||
shared.middlewares.labs.members,
|
||||
mw.authAdminApi,
|
||||
upload.single('membersfile'),
|
||||
shared.middlewares.validation.upload({type: 'members'}),
|
||||
http(apiv2.members.importCSV)
|
||||
);
|
||||
|
||||
router.get('/members/:id', shared.middlewares.labs.members, mw.authAdminApi, http(apiv2.members.read));
|
||||
router.put('/members/:id', shared.middlewares.labs.members, mw.authAdminApi, http(apiv2.members.edit));
|
||||
router.del('/members/:id', shared.middlewares.labs.members, mw.authAdminApi, http(apiv2.members.destroy));
|
||||
|
||||
// ## Roles
|
||||
router.get('/roles/', mw.authAdminApi, http(apiv2.roles.browse));
|
||||
|
||||
|
|
|
@ -225,7 +225,7 @@ describe('Members API', function () {
|
|||
.then((res) => {
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
res.headers['content-disposition'].should.match(/Attachment;\sfilename="members/);
|
||||
res.text.should.match(/id,email,name,created_at,deleted_at/);
|
||||
res.text.should.match(/id,email,name,note,created_at,deleted_at/);
|
||||
res.text.should.match(/member1@test.com/);
|
||||
res.text.should.match(/Mr Egg/);
|
||||
});
|
||||
|
|
|
@ -20,7 +20,7 @@ const expectedProperties = {
|
|||
// and always returns computed properties: url, primary_tag, primary_author
|
||||
.concat('url')
|
||||
// canary API doesn't return unused fields
|
||||
.without('locale', 'visibility')
|
||||
.without('locale')
|
||||
// These fields aren't useful as they always have known values
|
||||
.without('status')
|
||||
// @TODO: https://github.com/TryGhost/Ghost/issues/10335
|
||||
|
|
|
@ -1,255 +0,0 @@
|
|||
const path = require('path');
|
||||
const should = require('should');
|
||||
const supertest = require('supertest');
|
||||
const sinon = require('sinon');
|
||||
const testUtils = require('../../../../utils');
|
||||
const localUtils = require('./utils');
|
||||
const config = require('../../../../../server/config');
|
||||
const labs = require('../../../../../server/services/labs');
|
||||
|
||||
const ghost = testUtils.startGhost;
|
||||
|
||||
let request;
|
||||
|
||||
describe('Members API', function () {
|
||||
before(function () {
|
||||
sinon.stub(labs, 'isSet').withArgs('members').returns(true);
|
||||
});
|
||||
|
||||
after(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
before(function () {
|
||||
return ghost()
|
||||
.then(function () {
|
||||
request = supertest.agent(config.get('url'));
|
||||
})
|
||||
.then(function () {
|
||||
return localUtils.doAuth(request, 'member');
|
||||
});
|
||||
});
|
||||
|
||||
it('Can browse', function () {
|
||||
return request
|
||||
.get(localUtils.API.getApiQuery('members/'))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
const jsonResponse = res.body;
|
||||
should.exist(jsonResponse);
|
||||
should.exist(jsonResponse.members);
|
||||
jsonResponse.members.should.have.length(1);
|
||||
localUtils.API.checkResponse(jsonResponse.members[0], 'member');
|
||||
|
||||
testUtils.API.isISO8601(jsonResponse.members[0].created_at).should.be.true();
|
||||
jsonResponse.members[0].created_at.should.be.an.instanceof(String);
|
||||
|
||||
jsonResponse.meta.pagination.should.have.property('page', 1);
|
||||
jsonResponse.meta.pagination.should.have.property('limit', 15);
|
||||
jsonResponse.meta.pagination.should.have.property('pages', 1);
|
||||
jsonResponse.meta.pagination.should.have.property('total', 1);
|
||||
jsonResponse.meta.pagination.should.have.property('next', null);
|
||||
jsonResponse.meta.pagination.should.have.property('prev', null);
|
||||
});
|
||||
});
|
||||
|
||||
it('Can read', function () {
|
||||
return request
|
||||
.get(localUtils.API.getApiQuery(`members/${testUtils.DataGenerator.Content.members[0].id}/`))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
const jsonResponse = res.body;
|
||||
should.exist(jsonResponse);
|
||||
should.exist(jsonResponse.members);
|
||||
jsonResponse.members.should.have.length(1);
|
||||
localUtils.API.checkResponse(jsonResponse.members[0], 'member', 'stripe');
|
||||
});
|
||||
});
|
||||
|
||||
it('Can add', function () {
|
||||
const member = {
|
||||
name: 'test',
|
||||
email: 'memberTestAdd@test.com'
|
||||
};
|
||||
|
||||
return request
|
||||
.post(localUtils.API.getApiQuery(`members/`))
|
||||
.send({members: [member]})
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(201)
|
||||
.then((res) => {
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
const jsonResponse = res.body;
|
||||
should.exist(jsonResponse);
|
||||
should.exist(jsonResponse.members);
|
||||
jsonResponse.members.should.have.length(1);
|
||||
|
||||
jsonResponse.members[0].name.should.equal(member.name);
|
||||
jsonResponse.members[0].email.should.equal(member.email);
|
||||
})
|
||||
.then(() => {
|
||||
return request
|
||||
.post(localUtils.API.getApiQuery(`members/`))
|
||||
.send({members: [member]})
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(422);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should fail when passing incorrect email_type query parameter', function () {
|
||||
const member = {
|
||||
name: 'test',
|
||||
email: 'memberTestAdd@test.com'
|
||||
};
|
||||
|
||||
return request
|
||||
.post(localUtils.API.getApiQuery(`members/?send_email=true&email_type=lel`))
|
||||
.send({members: [member]})
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(422);
|
||||
});
|
||||
|
||||
it('Can edit by id', function () {
|
||||
const memberToChange = {
|
||||
name: 'change me',
|
||||
email: 'member2Change@test.com'
|
||||
};
|
||||
|
||||
const memberChanged = {
|
||||
name: 'changed',
|
||||
email: 'cantChangeMe@test.com'
|
||||
};
|
||||
|
||||
return request
|
||||
.post(localUtils.API.getApiQuery(`members/`))
|
||||
.send({members: [memberToChange]})
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(201)
|
||||
.then((res) => {
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
const jsonResponse = res.body;
|
||||
should.exist(jsonResponse);
|
||||
should.exist(jsonResponse.members);
|
||||
jsonResponse.members.should.have.length(1);
|
||||
|
||||
return jsonResponse.members[0];
|
||||
})
|
||||
.then((newMember) => {
|
||||
return request
|
||||
.put(localUtils.API.getApiQuery(`members/${newMember.id}/`))
|
||||
.send({members: [memberChanged]})
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
|
||||
const jsonResponse = res.body;
|
||||
|
||||
should.exist(jsonResponse);
|
||||
should.exist(jsonResponse.members);
|
||||
jsonResponse.members.should.have.length(1);
|
||||
localUtils.API.checkResponse(jsonResponse.members[0], 'member');
|
||||
jsonResponse.members[0].name.should.equal(memberChanged.name);
|
||||
jsonResponse.members[0].email.should.not.equal(memberChanged.email);
|
||||
jsonResponse.members[0].email.should.equal(memberToChange.email);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Can destroy', function () {
|
||||
const member = {
|
||||
name: 'test',
|
||||
email: 'memberTestDestroy@test.com'
|
||||
};
|
||||
|
||||
return request
|
||||
.post(localUtils.API.getApiQuery(`members/`))
|
||||
.send({members: [member]})
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(201)
|
||||
.then((res) => {
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
|
||||
const jsonResponse = res.body;
|
||||
|
||||
should.exist(jsonResponse);
|
||||
should.exist(jsonResponse.members);
|
||||
|
||||
return jsonResponse.members[0];
|
||||
})
|
||||
.then((newMember) => {
|
||||
return request
|
||||
.delete(localUtils.API.getApiQuery(`members/${newMember.id}`))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(204)
|
||||
.then(() => newMember);
|
||||
})
|
||||
.then((newMember) => {
|
||||
return request
|
||||
.get(localUtils.API.getApiQuery(`members/${newMember.id}/`))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
it('Can export CSV', function () {
|
||||
return request
|
||||
.get(localUtils.API.getApiQuery(`members/csv/`))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /text\/csv/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
res.headers['content-disposition'].should.match(/Attachment;\sfilename="members/);
|
||||
res.text.should.match(/id,email,name,created_at,deleted_at/);
|
||||
res.text.should.match(/member1@test.com/);
|
||||
res.text.should.match(/Mr Egg/);
|
||||
});
|
||||
});
|
||||
|
||||
it('Can import CSV', function () {
|
||||
return request
|
||||
.post(localUtils.API.getApiQuery(`members/csv/`))
|
||||
.attach('membersfile', path.join(__dirname, '/../../../../utils/fixtures/csv/valid-members-import.csv'))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(201)
|
||||
.then((res) => {
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
const jsonResponse = res.body;
|
||||
|
||||
should.exist(jsonResponse);
|
||||
should.exist(jsonResponse.meta);
|
||||
should.exist(jsonResponse.meta.stats);
|
||||
|
||||
jsonResponse.meta.stats.imported.should.equal(2);
|
||||
jsonResponse.meta.stats.duplicates.should.equal(0);
|
||||
jsonResponse.meta.stats.invalid.should.equal(0);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -19,7 +19,7 @@ var should = require('should'),
|
|||
*/
|
||||
describe('DB version integrity', function () {
|
||||
// Only these variables should need updating
|
||||
const currentSchemaHash = 'bf8ffd57c6d35998dc0b17ef9d34f4cc';
|
||||
const currentSchemaHash = '09ab28b8a18dc7f7f4fb5dbd5ccc10ac';
|
||||
const currentFixturesHash = '9e3a7f71cab98f3fb8504d1f234b503d';
|
||||
|
||||
// If this test is failing, then it is likely a change has been made that requires a DB version bump,
|
||||
|
|
83
core/test/unit/lib/mobiledoc/cards/card-markdown_spec.js
Normal file
83
core/test/unit/lib/mobiledoc/cards/card-markdown_spec.js
Normal file
|
@ -0,0 +1,83 @@
|
|||
const should = require('should');
|
||||
const card = require('../../../../../server/lib/mobiledoc/cards/card-markdown');
|
||||
const SimpleDom = require('simple-dom');
|
||||
const serializer = new SimpleDom.HTMLSerializer(SimpleDom.voidMap);
|
||||
|
||||
describe('Markdown card (v1 compatibility card)', function () {
|
||||
it('renders', function () {
|
||||
let opts = {
|
||||
env: {
|
||||
dom: new SimpleDom.Document()
|
||||
},
|
||||
payload: {
|
||||
markdown: '#HEADING\r\n- list\r\n- items'
|
||||
}
|
||||
};
|
||||
|
||||
serializer.serialize(card.render(opts)).should.eql('<!--kg-card-begin: markdown--><h1 id="heading">HEADING</h1>\n<ul>\n<li>list</li>\n<li>items</li>\n</ul>\n<!--kg-card-end: markdown-->');
|
||||
});
|
||||
|
||||
it('Accepts invalid HTML in markdown', function () {
|
||||
let opts = {
|
||||
env: {
|
||||
dom: new SimpleDom.Document()
|
||||
},
|
||||
payload: {
|
||||
markdown: '#HEADING\r\n<h2>Heading 2>'
|
||||
}
|
||||
};
|
||||
|
||||
serializer.serialize(card.render(opts)).should.eql('<!--kg-card-begin: markdown--><h1 id="heading">HEADING</h1>\n<h2>Heading 2><!--kg-card-end: markdown-->');
|
||||
});
|
||||
|
||||
it('Renders nothing when payload is undefined', function () {
|
||||
let opts = {
|
||||
env: {
|
||||
dom: new SimpleDom.Document()
|
||||
},
|
||||
payload: {
|
||||
markdown: undefined
|
||||
}
|
||||
};
|
||||
|
||||
serializer.serialize(card.render(opts)).should.eql('');
|
||||
});
|
||||
|
||||
it('[deprecated] version 1', function () {
|
||||
let opts = {
|
||||
env: {
|
||||
dom: new SimpleDom.Document()
|
||||
},
|
||||
payload: {
|
||||
markdown: '#HEADING\r\n- list\r\n- items'
|
||||
},
|
||||
options: {
|
||||
version: 1
|
||||
}
|
||||
};
|
||||
|
||||
serializer.serialize(card.render(opts)).should.eql('<!--kg-card-begin: markdown--><div class="kg-card-markdown"><h1 id="heading">HEADING</h1>\n<ul>\n<li>list</li>\n<li>items</li>\n</ul>\n</div><!--kg-card-end: markdown-->');
|
||||
});
|
||||
|
||||
it('transforms urls absolute to relative', function () {
|
||||
let payload = {
|
||||
markdown: 'A link to [an internal post](http://127.0.0.1:2369/post)'
|
||||
};
|
||||
|
||||
const transformed = card.absoluteToRelative(payload, {});
|
||||
|
||||
transformed.markdown
|
||||
.should.equal('A link to [an internal post](/post)');
|
||||
});
|
||||
|
||||
it('transforms urls relative to absolute', function () {
|
||||
let payload = {
|
||||
markdown: 'A link to [an internal post](/post)'
|
||||
};
|
||||
|
||||
const transformed = card.relativeToAbsolute(payload, {});
|
||||
|
||||
transformed.markdown
|
||||
.should.equal('A link to [an internal post](http://127.0.0.1:2369/post)');
|
||||
});
|
||||
});
|
|
@ -113,17 +113,15 @@ describe('Unit: models/settings', function () {
|
|||
|
||||
return models.Settings.populateDefaults()
|
||||
.then(() => {
|
||||
eventSpy.callCount.should.equal(80);
|
||||
eventSpy.callCount.should.equal(82);
|
||||
const eventsEmitted = eventSpy.args.map(args => args[0]);
|
||||
const checkEventEmitted = event => should.ok(eventsEmitted.includes(event), `${event} event should be emitted`);
|
||||
|
||||
eventSpy.args[1][0].should.equal('settings.db_hash.added');
|
||||
eventSpy.args[1][1].attributes.type.should.equal('core');
|
||||
checkEventEmitted('settings.db_hash.added');
|
||||
checkEventEmitted('settings.description.added');
|
||||
|
||||
eventSpy.args[13][0].should.equal('settings.description.added');
|
||||
eventSpy.args[13][1].attributes.type.should.equal('blog');
|
||||
eventSpy.args[13][1].attributes.value.should.equal('The professional publishing platform');
|
||||
|
||||
eventSpy.args[77][0].should.equal('settings.default_content_visibility.added');
|
||||
eventSpy.args[79][0].should.equal('settings.members_subscription_settings.added');
|
||||
checkEventEmitted('settings.default_content_visibility.added');
|
||||
checkEventEmitted('settings.members_subscription_settings.added');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -137,7 +135,7 @@ describe('Unit: models/settings', function () {
|
|||
|
||||
return models.Settings.populateDefaults()
|
||||
.then(() => {
|
||||
eventSpy.callCount.should.equal(78);
|
||||
eventSpy.callCount.should.equal(80);
|
||||
|
||||
eventSpy.args[13][0].should.equal('settings.logo.added');
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "ghost",
|
||||
"version": "2.34.0",
|
||||
"version": "2.35.0",
|
||||
"description": "The professional publishing platform",
|
||||
"author": "Ghost Foundation",
|
||||
"homepage": "https://ghost.org",
|
||||
|
@ -41,11 +41,11 @@
|
|||
"dependencies": {
|
||||
"@nexes/nql": "0.3.0",
|
||||
"@tryghost/helpers": "1.1.15",
|
||||
"@tryghost/members-api": "0.8.0",
|
||||
"@tryghost/members-api": "0.8.2",
|
||||
"@tryghost/members-ssr": "0.7.0",
|
||||
"@tryghost/social-urls": "0.1.2",
|
||||
"@tryghost/string": "^0.1.3",
|
||||
"@tryghost/url-utils": "0.6.5",
|
||||
"@tryghost/url-utils": "0.6.6",
|
||||
"ajv": "6.10.2",
|
||||
"amperize": "0.6.0",
|
||||
"analytics-node": "3.3.0",
|
||||
|
|
26
yarn.lock
26
yarn.lock
|
@ -227,22 +227,22 @@
|
|||
dependencies:
|
||||
"@tryghost/kg-clean-basic-html" "^0.1.3"
|
||||
|
||||
"@tryghost/magic-link@^0.2.1":
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/magic-link/-/magic-link-0.2.1.tgz#c729bf5d2fe7fa1330eccbba51ba3579834784fc"
|
||||
integrity sha512-bqlZndOXwU3b9FXvMtHIep1EradDnsfQ+4vvINQ+QsCOWKH1EDbPhjYS9f2G0xNx8BVyG4e1eMxZ5lBhJ6lBCA==
|
||||
"@tryghost/magic-link@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/magic-link/-/magic-link-0.3.0.tgz#52e4326efba5756a39d219990d8fcbcbd3ba813a"
|
||||
integrity sha512-Ahd4KG8Hz/zndRjx0MQt6cY8Lq8TL3UQEXbNIhEO1zOzgyq7FKGpBoSfaVzYb6aSx1pAG/EP60VuBKetHBmUSw==
|
||||
dependencies:
|
||||
bluebird "^3.5.5"
|
||||
ghost-ignition "^3.1.0"
|
||||
jsonwebtoken "^8.5.1"
|
||||
lodash "^4.17.15"
|
||||
|
||||
"@tryghost/members-api@0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/members-api/-/members-api-0.8.0.tgz#e1f0b67371b6b61f6cf4f64e5b62c92ba3777c2a"
|
||||
integrity sha512-THPd9HUyqo1WdroWFH8X+KcNfyy586dVSOYRQWIjF+URoYHmlrf2AlxBrdHMq5R8mQVCKIO0t3pmZwRnafucVA==
|
||||
"@tryghost/members-api@0.8.2":
|
||||
version "0.8.2"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/members-api/-/members-api-0.8.2.tgz#2d72cb80909c571fb2e6c9de868d438b5319e54d"
|
||||
integrity sha512-maqhvyNpvw0kWG20UpSG0637YXteO5cc/ts+KWiSk4oIDuaH1sycgNXQ98q4Z+MAKzMye0iGoom7NcXQOFh8/Q==
|
||||
dependencies:
|
||||
"@tryghost/magic-link" "^0.2.1"
|
||||
"@tryghost/magic-link" "^0.3.0"
|
||||
bluebird "^3.5.4"
|
||||
body-parser "^1.19.0"
|
||||
cookies "^0.7.3"
|
||||
|
@ -297,10 +297,10 @@
|
|||
dependencies:
|
||||
unidecode "^0.1.8"
|
||||
|
||||
"@tryghost/url-utils@0.6.5":
|
||||
version "0.6.5"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/url-utils/-/url-utils-0.6.5.tgz#a393e67e60daf265fd5d3e4ff6423bd0573bcd10"
|
||||
integrity sha512-Ah6gmFo79a4p/lpzGC/EMwKgw5jEiURJAtaj3RSAiOaDF8VIAdjg7G4zO6CY8ShnMCCo7xizIGUMETHePRBgVA==
|
||||
"@tryghost/url-utils@0.6.6":
|
||||
version "0.6.6"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/url-utils/-/url-utils-0.6.6.tgz#45fe815419df325cc07571c39739b04296a1cc09"
|
||||
integrity sha512-U3JHMK5c2OVpuXHoJAOMjomYOVoyCY3ZGMYJ8yyby7eq2mMAHIBbwWsg4IkKuYKQ+/hGxK7DLGS7A3xb/Rxxbg==
|
||||
dependencies:
|
||||
cheerio "0.22.0"
|
||||
moment "2.24.0"
|
||||
|
|
Loading…
Add table
Reference in a new issue