This commit is contained in:
James
2020-11-21 10:03:18 -05:00
13 changed files with 354 additions and 65 deletions

View File

@@ -104,8 +104,8 @@ module.exports = {
},
graphQL: {
maxComplexity: 1000,
mutations: {},
queries: {},
mutations: {}, // TODO: needs typing
queries: {}, // TODO: needs typing
disablePlaygroundInProduction: true,
},
rateLimit: {

View File

@@ -12,8 +12,7 @@ payload.init({
mongoURL: 'mongodb://localhost/payload',
express: expressApp,
onInit: () => {
console.log('Payload is initialized');
// console.log('Payload is initialized');
console.log('Payload Demo Initialized');
},
});

View File

@@ -3,11 +3,12 @@
".git",
"node_modules",
"node_modules/**/node_modules",
"src/client"
"src/client",
"src/**/*.spec.ts"
],
"watch": [
"src/",
"src/**/*.ts",
"demo/"
],
"ext": "js,json"
"ext": "ts,js,json"
}

View File

@@ -1,37 +1,47 @@
import nodemailer from 'nodemailer';
import nodemailer, { Transporter } from 'nodemailer';
import { PayloadEmailOptions } from '../types';
import { InvalidConfiguration } from '../errors';
import mockHandler from './mockHandler';
import Logger from '../utilities/logger';
const logger = Logger();
async function buildEmail() {
if (!this.config.email.transport || this.config.email.transport === 'mock') {
this.logger.info('E-mail configured with mock configuration');
const mockAccount = await mockHandler(this.config.email);
if (this.config.email.transport === 'mock') {
export default async function buildEmail(emailConfig: PayloadEmailOptions) {
if (!emailConfig.transport || emailConfig.transport === 'mock') {
const mockAccount = await mockHandler(emailConfig);
// Only log mock credentials if was explicitly set in config
if (emailConfig.transport === 'mock') {
const { account: { web, user, pass } } = mockAccount;
this.logger.info(`Log into mock email provider at ${web}`);
this.logger.info(`Mock email account username: ${user}`);
this.logger.info(`Mock email account password: ${pass}`);
logger.info('E-mail configured with mock configuration');
logger.info(`Log into mock email provider at ${web}`);
logger.info(`Mock email account username: ${user}`);
logger.info(`Mock email account password: ${pass}`);
}
return mockAccount;
}
const email = { ...this.config.email };
const email = { ...emailConfig };
if (this.config.email.transport) {
email.transport = this.config.email.transport;
if (!email.fromName || !email.fromAddress) {
throw new InvalidConfiguration('Email fromName and fromAddress must be configured when transport is configured');
}
if (this.config.email.transportOptions) {
email.transport = nodemailer.createTransport(this.config.email.transportOptions);
let transport: Transporter;
// TODO: Is this ever populated when not using 'mock'?
if (emailConfig.transport) {
transport = emailConfig.transport;
} else if (emailConfig.transportOptions) {
transport = nodemailer.createTransport(emailConfig.transportOptions);
}
try {
await email.transport.verify();
await transport.verify();
email.transport = transport;
} catch (err) {
this.logger.error('There is an error with the email configuration you have provided.', err);
logger.error(
"There is an error with the email configuration you have provided.",
err
);
}
return email;
}
export default buildEmail;

View File

@@ -1,15 +1,17 @@
import nodemailer from 'nodemailer';
import nodemailer, { TestAccount, Transporter } from 'nodemailer';
import { PayloadEmailOptions } from '../types';
import { MockEmailHandler } from './types';
const mockEmailHandler = async (emailConfig) => {
const mockEmailHandler = async (emailConfig: PayloadEmailOptions): Promise<MockEmailHandler> => {
const testAccount = await nodemailer.createTestAccount();
const smtpOptions = {
...emailConfig,
host: 'smtp.ethereal.email',
host: "smtp.ethereal.email",
port: 587,
secure: false,
fromName: emailConfig.fromName || 'Payload CMS',
fromAddress: emailConfig.fromAddress || 'info@payloadcms.com',
fromName: emailConfig.fromName || "Payload CMS",
fromAddress: emailConfig.fromAddress || "info@payloadcms.com",
auth: {
user: testAccount.user,
pass: testAccount.pass,

3
src/email/types.ts Normal file
View File

@@ -0,0 +1,3 @@
import { TestAccount, Transporter } from 'nodemailer';
export type MockEmailHandler = { account: TestAccount; transport: Transporter };

View File

@@ -5,7 +5,7 @@ import httpStatus from 'http-status';
* @extends Error
*/
class ExtendableError extends Error {
constructor(message, status, data, isPublic) {
constructor(message: string, status: number, data: any, isPublic: boolean) {
super(message);
this.name = this.constructor.name;
this.message = message;
@@ -29,7 +29,7 @@ class APIError extends ExtendableError {
* @param {object} data - response data to be returned.
* @param {boolean} isPublic - Whether the message should be visible to user or not.
*/
constructor(message, status = httpStatus.INTERNAL_SERVER_ERROR, data, isPublic = false) {
constructor(message: string, status: number = httpStatus.INTERNAL_SERVER_ERROR, data: any, isPublic = false) {
super(message, status, data, isPublic);
}
}

View File

@@ -2,7 +2,7 @@ import httpStatus from 'http-status';
import APIError from './APIError';
class InvalidConfiguration extends APIError {
constructor(message, results) {
constructor(message, results?) {
super(message, httpStatus.INTERNAL_SERVER_ERROR, results);
}
}

View File

@@ -1,5 +1,19 @@
import express from 'express';
import crypto from 'crypto';
import { Router } from 'express';
import {
PayloadConfig,
PayloadCollection,
PayloadInitOptions,
PayloadLogger,
CreateOptions,
FindOptions,
FindGlobalOptions,
UpdateGlobalOptions,
FindByIDOptions,
UpdateOptions,
DeleteOptions,
} from './types';
import Logger from './utilities/logger';
import bindOperations from './init/bindOperations';
import bindRequestHandlers from './init/bindRequestHandlers';
@@ -23,21 +37,27 @@ import performFieldOperations from './fields/performFieldOperations';
import localOperations from './collections/operations/local';
import localGlobalOperations from './globals/operations/local';
import { encrypt, decrypt } from './auth/crypto';
import { TestAccount } from 'nodemailer';
import { MockEmailHandler } from './email/types';
require('es6-promise').polyfill();
require('isomorphic-fetch');
const logger = Logger();
class Payload {
logger: any;
config: PayloadConfig;
collections: PayloadCollection[] = [];
logger: PayloadLogger;
router: Router;
email: any;
init(options) {
this.logger = logger;
init(options: PayloadInitOptions) {
this.logger = Logger();
this.logger.info('Starting Payload...');
if (!options.secret) {
throw new Error('Error: missing secret key. A secret key is needed to secure Payload.');
throw new Error(
'Error: missing secret key. A secret key is needed to secure Payload.'
);
}
if (!options.mongoURL) {
@@ -51,14 +71,18 @@ class Payload {
...config,
email,
license: options.license,
secret: crypto.createHash('sha256').update(options.secret).digest('hex').slice(0, 32),
secret: crypto
.createHash('sha256')
.update(options.secret)
.digest('hex')
.slice(0, 32),
mongoURL: options.mongoURL,
local: options.local,
});
if (typeof this.config.paths === 'undefined') this.config.paths = {};
this.collections = {};
// this.collections = {};
bindOperations(this);
bindRequestHandlers(this);
@@ -70,7 +94,7 @@ class Payload {
this.initCollections = initCollections.bind(this);
this.initGlobals = initGlobals.bind(this);
this.initGraphQLPlayground = initGraphQLPlayground.bind(this);
this.buildEmail = buildEmail.bind(this);
// this.buildEmail = buildEmail.bind(this);
this.sendEmail = this.sendEmail.bind(this);
this.getMockEmailCredentials = this.getMockEmailCredentials.bind(this);
this.initStatic = initStatic.bind(this);
@@ -97,7 +121,7 @@ class Payload {
}
// Configure email service
this.email = this.buildEmail();
this.email = buildEmail(this.config.email);
// Initialize collections & globals
this.initCollections();
@@ -114,7 +138,8 @@ class Payload {
// If not initializing locally, set up HTTP routing
if (!this.config.local) {
this.express = options.express;
if (this.config.rateLimit && this.config.rateLimit.trustProxy) this.express.set('trust proxy', 1);
if (this.config.rateLimit && this.config.rateLimit.trustProxy)
this.express.set('trust proxy', 1);
this.initAdmin();
@@ -125,7 +150,7 @@ class Payload {
this.router.use(
this.config.routes.graphQL,
identifyAPI('GraphQL'),
(req, res) => graphQLHandler.init(req, res)(req, res),
(req, res) => graphQLHandler.init(req, res)(req, res)
);
this.initGraphQLPlayground();
@@ -145,54 +170,54 @@ class Payload {
if (typeof options.onInit === 'function') options.onInit();
}
async sendEmail(message) {
async sendEmail(message: string) {
const email = await this.email;
const result = email.transport.sendMail(message);
return result;
}
async getMockEmailCredentials() {
const email = await this.email;
async getMockEmailCredentials(): Promise<TestAccount> {
const email = await this.email as MockEmailHandler;
return email.account;
}
async create(options) {
async create(options: CreateOptions) {
let { create } = localOperations;
create = create.bind(this);
return create(options);
}
async find(options) {
async find(options: FindOptions) {
let { find } = localOperations;
find = find.bind(this);
return find(options);
}
async findGlobal(options) {
async findGlobal(options: FindGlobalOptions) {
let { findOne } = localGlobalOperations;
findOne = findOne.bind(this);
return findOne(options);
}
async updateGlobal(options) {
async updateGlobal(options: UpdateGlobalOptions) {
let { update } = localGlobalOperations;
update = update.bind(this);
return update(options);
}
async findByID(options) {
async findByID(options: FindByIDOptions) {
let { findByID } = localOperations;
findByID = findByID.bind(this);
return findByID(options);
}
async update(options) {
async update(options: UpdateOptions) {
let { update } = localOperations;
update = update.bind(this);
return update(options);
}
async delete(options) {
async delete(options: DeleteOptions) {
let { delete: deleteOperation } = localOperations;
deleteOperation = deleteOperation.bind(this);
return deleteOperation(options);

View File

@@ -3,8 +3,9 @@ import * as payloadSchema from './payload.schema.json';
import * as collectionSchema from './collection.schema.json';
import InvalidSchema from '../errors/InvalidSchema';
import { PayloadConfig } from '../types';
const validateSchema = (config) => {
const validateSchema = (config: PayloadConfig) => {
const ajv = new Ajv({ useDefaults: true });
const validate = ajv.addSchema(collectionSchema, '/collection.schema.json')
.compile(payloadSchema);

251
src/types/index.ts Normal file
View File

@@ -0,0 +1,251 @@
// TODO: Split out init options from config types into own file
import { Express, Request } from 'express';
import { Transporter } from 'nodemailer';
import SMTPConnection from 'nodemailer/lib/smtp-connection';
import { Logger } from 'pino';
type MockEmailTransport = {
transport?: 'mock';
fromName?: string;
fromAddress?: string;
};
type ValidEmailTransport = {
transport: Transporter;
transportOptions?: SMTPConnection.Options;
fromName: string;
fromAddress: string;
};
export type PayloadEmailOptions = ValidEmailTransport | MockEmailTransport;
export type PayloadInitOptions = {
express?: Express;
mongoURL: string;
secret: string;
license?: string;
email?: PayloadEmailOptions;
local?: boolean; // I have no idea what this is
onInit?: () => void;
};
export type Document = {
id: string;
};
export type CreateOptions = {
collection: string;
data: any;
};
export type FindOptions = {
collection: string;
where?: { [key: string]: any };
};
export type FindResponse = {
docs: Document[];
totalDocs: number;
limit: number;
totalPages: number;
page: number;
pagingCounter: number;
hasPrevPage: boolean;
hasNextPage: boolean;
prevPage: number | null;
nextPage: number | null;
};
export type FindGlobalOptions = {
global: string;
};
export type UpdateGlobalOptions = {
global: string;
data: any;
};
export type FindByIDOptions = {
collection: string;
id: string;
};
export type UpdateOptions = {
collection: string;
id: string;
data: any;
};
export type DeleteOptions = {
collection: string;
id: string;
};
export type ForgotPasswordOptions = {
collection: string;
generateEmailHTML?: (token: string) => string;
expiration: Date;
data: any;
};
export type SendEmailOptions = {
from: string;
to: string;
subject: string;
html: string;
};
export type MockEmailCredentials = {
user: string;
pass: string;
web: string;
};
export type PayloadField = {
name: string;
label: string;
type:
| 'number'
| 'text'
| 'email'
| 'textarea'
| 'richText'
| 'code'
| 'radio'
| 'checkbox'
| 'date'
| 'upload'
| 'relationship'
| 'row'
| 'array'
| 'group'
| 'select'
| 'blocks';
localized?: boolean;
fields?: PayloadField[];
admin?: {
position?: string;
width?: string;
style?: Object;
};
};
export type PayloadCollectionHook = (...args: any[]) => any | void;
export type PayloadAccess = (args?: any) => boolean;
export type PayloadCollection = {
slug: string;
labels?: {
singular: string;
plural: string;
};
admin?: {
useAsTitle?: string;
defaultColumns?: string[];
components?: any;
};
hooks?: {
beforeOperation?: PayloadCollectionHook[];
beforeValidate?: PayloadCollectionHook[];
beforeChange?: PayloadCollectionHook[];
afterChange?: PayloadCollectionHook[];
beforeRead?: PayloadCollectionHook[];
afterRead?: PayloadCollectionHook[];
beforeDelete?: PayloadCollectionHook[];
afterDelete?: PayloadCollectionHook[];
};
access?: {
create?: PayloadAccess;
read?: PayloadAccess;
update?: PayloadAccess;
delete?: PayloadAccess;
admin?: PayloadAccess;
};
auth?: {
tokenExpiration?: number;
verify?:
| boolean
| { generateEmailHTML: string; generateEmailSubject: string };
maxLoginAttempts?: number;
lockTime?: number;
useAPIKey?: boolean;
cookies?:
| {
secure?: boolean;
sameSite?: string;
domain?: string | undefined;
}
| boolean;
};
fields: PayloadField[];
};
export type PayloadGlobal = {
slug: string;
label: string;
access?: {
create?: PayloadAccess;
read?: PayloadAccess;
update?: PayloadAccess;
delete?: PayloadAccess;
admin?: PayloadAccess;
};
fields: PayloadField[];
};
export type PayloadConfig = {
admin?: {
user?: string;
meta?: {
titleSuffix?: string;
ogImage?: string;
favicon?: string;
};
disable?: boolean;
};
collections?: PayloadCollection[];
globals?: PayloadGlobal[];
serverURL?: string;
cookiePrefix?: string;
csrf?: string[];
cors?: string[];
publicENV: { [key: string]: string };
routes?: {
api?: string;
admin?: string;
graphQL?: string;
graphQLPlayground?: string;
};
email?: PayloadEmailOptions;
local?: boolean;
defaultDepth?: number;
maxDepth?: number;
rateLimit?: {
window?: number;
max?: number;
trustProxy?: boolean;
skip?: (req: Request) => boolean; // TODO: Type join Request w/ PayloadRequest
};
upload?: {
limits?: {
fileSize?: number;
};
};
localization?: {
locales: string[];
};
defaultLocale?: string;
fallback?: boolean;
graphQL?: {
mutations?: Object;
queries?: Object;
maxComplexity?: number;
disablePlaygroundInProduction?: boolean;
};
components: { [key: string]: JSX.Element | (() => JSX.Element) };
paths?: { [key: string]: string };
hooks?: {
afterError?: () => void;
};
webpack?: (config: any) => any;
serverModules?: string[];
};
export type PayloadLogger = Logger;

View File

@@ -1,6 +1,7 @@
import falsey from 'falsey';
import pino from 'pino';
import memoize from 'micro-memoize';
import { PayloadLogger } from '../types';
export default memoize((name = 'payload') => pino({
name,
@@ -9,4 +10,4 @@ export default memoize((name = 'payload') => pino({
ignore: 'pid,hostname',
translateTime: 'HH:MM:ss',
},
}));
}) as PayloadLogger);

View File

@@ -1,3 +1,4 @@
import { PayloadConfig } from '../types';
import defaultUser from '../auth/default';
import sanitizeCollection from '../collections/sanitize';
import { InvalidConfiguration } from '../errors';
@@ -5,7 +6,7 @@ import sanitizeGlobals from '../globals/sanitize';
import validateSchema from '../schema/validateSchema';
import checkDuplicateCollections from './checkDuplicateCollections';
const sanitizeConfig = (config) => {
const sanitizeConfig = (config: PayloadConfig) => {
const sanitizedConfig = validateSchema({ ...config });
// TODO: remove default values from sanitize in favor of assigning in the schema within validateSchema and use https://www.npmjs.com/package/ajv#coercing-data-types where needed
@@ -42,12 +43,7 @@ const sanitizeConfig = (config) => {
}
sanitizedConfig.email = config.email || {};
// TODO: This should likely be moved to the payload.schema.json
if (sanitizedConfig.email.transports) {
if (!sanitizedConfig.email.email.fromName || !sanitizedConfig.email.email.fromAddress) {
throw new InvalidConfiguration('Email fromName and fromAddress must be configured when transport is configured');
}
}
// if (!sanitizedConfig.email.transport) sanitizedConfig.email.transport = 'mock';
sanitizedConfig.graphQL = config.graphQL || {};
sanitizedConfig.graphQL.maxComplexity = (sanitizedConfig.graphQL && sanitizedConfig.graphQL.maxComplexity) ? sanitizedConfig.graphQL.maxComplexity : 1000;