From a7997819721b70d01a9f6de4ac083c3db88882ea Mon Sep 17 00:00:00 2001 From: rob Date: Wed, 8 Dec 2021 14:44:13 -0500 Subject: [PATCH] Newsletter wip --- .gitignore | 1 + app/controllers/admin.js | 3 + app/controllers/admin/newsletter.js | 67 ++++++ app/controllers/admin/page.js | 45 ++++ app/controllers/admin/post.js | 45 ++++ app/controllers/image.js | 18 +- app/controllers/newsletter.js | 100 +++++++++ app/models/email-body.js | 15 ++ app/models/email.js | 20 ++ app/models/newsletter-recipient.js | 21 ++ app/models/newsletter.js | 31 +++ app/services/email.js | 38 ++-- app/services/image.js | 87 ++++---- app/services/markdown.js | 37 ++++ app/services/minio.js | 6 +- app/services/newsletter.js | 114 ++++++++++ app/views/admin/components/menu.pug | 18 ++ app/views/admin/layouts/main.pug | 2 +- app/views/admin/newsletter/editor.pug | 59 +++++ app/views/admin/newsletter/index.pug | 21 ++ app/views/index.pug | 2 +- app/views/newsletter/index.pug | 15 ++ app/workers/newsletter.js | 113 ++++++++++ client/js/site-app.js | 14 +- config/limiter.js | 16 ++ docs/services.md | 40 ++++ package.json | 2 +- yarn.lock | 303 ++------------------------ 28 files changed, 912 insertions(+), 341 deletions(-) create mode 100644 app/controllers/admin/newsletter.js create mode 100644 app/controllers/admin/page.js create mode 100644 app/controllers/admin/post.js create mode 100644 app/controllers/newsletter.js create mode 100644 app/models/email-body.js create mode 100644 app/models/email.js create mode 100644 app/models/newsletter-recipient.js create mode 100644 app/models/newsletter.js create mode 100644 app/services/markdown.js create mode 100644 app/services/newsletter.js create mode 100644 app/views/admin/newsletter/editor.pug create mode 100644 app/views/admin/newsletter/index.pug create mode 100644 app/views/newsletter/index.pug create mode 100644 app/workers/newsletter.js create mode 100644 docs/services.md diff --git a/.gitignore b/.gitignore index 9e9b214..f92418e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules dist +data/minio diff --git a/app/controllers/admin.js b/app/controllers/admin.js index bdc6165..119e201 100644 --- a/app/controllers/admin.js +++ b/app/controllers/admin.js @@ -46,6 +46,9 @@ class AdminController extends SiteController { router.use('/host',await this.loadChild(path.join(__dirname, 'admin', 'host'))); router.use('/job-queue',await this.loadChild(path.join(__dirname, 'admin', 'job-queue'))); + router.use('/newsletter',await this.loadChild(path.join(__dirname, 'admin', 'newsletter'))); + router.use('/page',await this.loadChild(path.join(__dirname, 'admin', 'page'))); + router.use('/post',await this.loadChild(path.join(__dirname, 'admin', 'post'))); router.use('/user', await this.loadChild(path.join(__dirname, 'admin', 'user'))); router.get('/', this.getHomeView.bind(this)); diff --git a/app/controllers/admin/newsletter.js b/app/controllers/admin/newsletter.js new file mode 100644 index 0000000..63bff38 --- /dev/null +++ b/app/controllers/admin/newsletter.js @@ -0,0 +1,67 @@ +// admin/newsletter.js +// Copyright (C) 2021 Digital Telepresence, LLC +// License: Apache-2.0 + +'use strict'; + +const DTP_COMPONENT_NAME = 'admin:newsletter'; +const express = require('express'); + +const { SiteController } = require('../../../lib/site-lib'); + +class NewsletterController extends SiteController { + + constructor (dtp) { + super(dtp, DTP_COMPONENT_NAME); + } + + async start ( ) { + const router = express.Router(); + router.use(async (req, res, next) => { + res.locals.currentView = 'admin'; + res.locals.adminView = 'newsletter'; + return next(); + }); + + router.param('newsletterId', this.populateNewsletterId.bind(this)); + + router.get('/compose', this.getComposer.bind(this)); + + router.get('/:newsletterId', this.getComposer.bind(this)); + + router.get('/', this.getIndex.bind(this)); + + return router; + } + + async populateNewsletterId (req, res, next, newsletterId) { + const { newsletter: newsletterService } = this.dtp.services; + try { + res.locals.newsletter = await newsletterService.getById(newsletterId); + return next(); + } catch (error) { + this.log.error('failed to populate newsletterId', { newsletterId, error }); + return next(error); + } + } + + async getComposer (req, res) { + res.render('admin/newsletter/editor'); + } + + async getIndex (req, res, next) { + const { newsletter: newsletterService } = this.dtp.services; + try { + res.locals.pagination = this.getPaginationParameters(req, 20); + res.locals.newsletters = await newsletterService.getNewsletters(res.locals.pagination); + res.render('admin/newsletter/index'); + } catch (error) { + return next(error); + } + } +} + +module.exports = async (dtp) => { + let controller = new NewsletterController(dtp); + return controller; +}; \ No newline at end of file diff --git a/app/controllers/admin/page.js b/app/controllers/admin/page.js new file mode 100644 index 0000000..21ff024 --- /dev/null +++ b/app/controllers/admin/page.js @@ -0,0 +1,45 @@ +// admin/page.js +// Copyright (C) 2021 Digital Telepresence, LLC +// License: Apache-2.0 + +'use strict'; + +const DTP_COMPONENT_NAME = 'admin:page'; +const express = require('express'); + +const { SiteController } = require('../../../lib/site-lib'); + +class PageController extends SiteController { + + constructor (dtp) { + super(dtp, DTP_COMPONENT_NAME); + } + + async start ( ) { + const router = express.Router(); + router.use(async (req, res, next) => { + res.locals.currentView = 'admin'; + res.locals.adminView = 'page'; + return next(); + }); + + router.param('pageId', this.populatePageId.bind(this)); + + router.get('/', this.getIndex.bind(this)); + + return router; + } + + async populatePageId (req, res, next/*, pageId*/) { + return next(); + } + + async getIndex (req, res) { + res.render('admin/page/index'); + } +} + +module.exports = async (dtp) => { + let controller = new PageController(dtp); + return controller; +}; \ No newline at end of file diff --git a/app/controllers/admin/post.js b/app/controllers/admin/post.js new file mode 100644 index 0000000..864a2e2 --- /dev/null +++ b/app/controllers/admin/post.js @@ -0,0 +1,45 @@ +// admin/post.js +// Copyright (C) 2021 Digital Telepresence, LLC +// License: Apache-2.0 + +'use strict'; + +const DTP_COMPONENT_NAME = 'admin:post'; +const express = require('express'); + +const { SiteController } = require('../../../lib/site-lib'); + +class PostController extends SiteController { + + constructor (dtp) { + super(dtp, DTP_COMPONENT_NAME); + } + + async start ( ) { + const router = express.Router(); + router.use(async (req, res, next) => { + res.locals.currentView = 'admin'; + res.locals.adminView = 'post'; + return next(); + }); + + router.param('postId', this.populatePostId.bind(this)); + + router.get('/', this.getIndex.bind(this)); + + return router; + } + + async populatePostId (req, res, next/*, postId*/) { + return next(); + } + + async getIndex (req, res) { + res.render('admin/post/index'); + } +} + +module.exports = async (dtp) => { + let controller = new PostController(dtp); + return controller; +}; \ No newline at end of file diff --git a/app/controllers/image.js b/app/controllers/image.js index 0940e63..0dcfb66 100644 --- a/app/controllers/image.js +++ b/app/controllers/image.js @@ -31,7 +31,7 @@ class ImageController extends SiteController { const imageUpload = multer({ dest: '/tmp/dtp-sites/upload/image', limits: { - fileSize: 1024 * 1000 * 5, + fileSize: 1024 * 1000 * 12, }, }); @@ -42,6 +42,11 @@ class ImageController extends SiteController { router.param('imageId', this.populateImage.bind(this)); + router.post('/tinymce', + imageUpload.single('file'), + this.postTinyMceImage.bind(this), + ); + router.post('/', limiterService.create(limiterService.config.image.postCreateImage), imageUpload.single('file'), @@ -66,6 +71,17 @@ class ImageController extends SiteController { } } + async postTinyMceImage (req, res, next) { + const { image: imageService } = this.dtp.services; + try { + res.locals.image = await imageService.create(req.user, req.body, req.file); + res.status(200).json({ location: `/image/${res.locals.image._id}` }); + } catch (error) { + this.log.error('failed to create image', { error }); + return next(error); + } + } + async postCreateImage (req, res, next) { const { image: imageService } = this.dtp.services; try { diff --git a/app/controllers/newsletter.js b/app/controllers/newsletter.js new file mode 100644 index 0000000..8676303 --- /dev/null +++ b/app/controllers/newsletter.js @@ -0,0 +1,100 @@ +// newsletter.js +// Copyright (C) 2021 Digital Telepresence, LLC +// License: Apache-2.0 + +'use strict'; + +const DTP_COMPONENT_NAME = 'newsletter'; + +const express = require('express'); +const multer = require('multer'); + +const { SiteController } = require('../../lib/site-lib'); + +class NewsletterController extends SiteController { + + constructor (dtp) { + super(dtp, DTP_COMPONENT_NAME); + } + + async start ( ) { + const { dtp } = this; + const { limiter: limiterService } = dtp.services; + + const upload = multer({ dest: '/tmp' }); + + const router = express.Router(); + dtp.app.use('/newsletter', router); + + router.use(async (req, res, next) => { + res.locals.currentView = DTP_COMPONENT_NAME; + return next(); + }); + + router.param('newsletterId', this.populateNewsletterId.bind(this)); + + router.post('/', upload.none(), this.postAddRecipient.bind(this)); + + router.get('/:newsletterId', + limiterService.create(limiterService.config.newsletter.getView), + this.getView.bind(this), + ); + + router.get('/', + limiterService.create(limiterService.config.newsletter.getIndex), + this.getIndex.bind(this), + ); + } + + async populateNewsletterId (req, res, next, newsletterId) { + const { newsletter: newsletterService } = this.dtp.services; + try { + res.locals.newsletter = await newsletterService.getById(newsletterId); + return next(); + } catch (error) { + this.log.error('failed to populate newsletterId', { newsletterId, error }); + return next(error); + } + } + + async postAddRecipient (req, res) { + const { newsletter: newsletterService, displayEngine: displayEngineService } = this.dtp.services; + try { + const displayList = displayEngineService.createDisplayList('add-recipient'); + await newsletterService.addRecipient(req.body.email); + displayList.showNotification( + 'You have been added to the newsletter. Please check your email and verify your email address.', + 'success', + 'bottom-center', + 10000, + ); + res.status(200).json({ success: true, displayList }); + } catch (error) { + this.log.error('failed to update account settings', { error }); + return res.status(error.statusCode || 500).json({ + success: false, + message: error.message, + }); + } + } + + async getView (req, res) { + res.render('newsletter/view'); + } + + async getIndex (req, res, next) { + const { newsletter: newsletterService } = this.dtp.services; + try { + res.locals.pagination = this.getPaginationParameters(req, 20); + res.locals.newsletters = await newsletterService.getNewsletters(res.locals.pagination); + res.render('newsletter/index'); + } catch (error) { + return next(error); + } + } +} + +module.exports = async (dtp) => { + let controller = new NewsletterController(dtp); + return controller; +}; diff --git a/app/models/email-body.js b/app/models/email-body.js new file mode 100644 index 0000000..3b973df --- /dev/null +++ b/app/models/email-body.js @@ -0,0 +1,15 @@ +// email-body.js +// Copyright (C) 2021 Digital Telepresence, LLC +// License: Apache-2.0 + +'use strict'; + +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const EmailBodySchema = new Schema({ + subject: { type: String, required: true }, + body: { type: String, required: true }, +}); + +module.exports = mongoose.model('EmailBody', EmailBodySchema); \ No newline at end of file diff --git a/app/models/email.js b/app/models/email.js new file mode 100644 index 0000000..6b8b446 --- /dev/null +++ b/app/models/email.js @@ -0,0 +1,20 @@ +// email.js +// Copyright (C) 2021 Digital Telepresence, LLC +// License: Apache-2.0 + +'use strict'; + +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const EmailSchema = new Schema({ + created: { type: Date, default: Date.now, required: true, index: -1 }, + token: { type: String, required: true, index: 1 }, + from: { type: String, required: true, }, + to: { type: String, required: true }, + to_lc: { type: String, required: true, lowercase: true, index: 1 }, + contentType: { type: String, enum: ['Newsletter'], required: true }, + content: { type: Schema.ObjectId, required: true, index: true, refPath: 'contentType' }, +}); + +module.exports = mongoose.model('Email', EmailSchema); \ No newline at end of file diff --git a/app/models/newsletter-recipient.js b/app/models/newsletter-recipient.js new file mode 100644 index 0000000..d434029 --- /dev/null +++ b/app/models/newsletter-recipient.js @@ -0,0 +1,21 @@ +// newsletter-recipient.js +// Copyright (C) 2021 Digital Telepresence, LLC +// License: Apache-2.0 + +'use strict'; + +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const NewsletterRecipientSchema = new Schema({ + created: { type: Date, default: Date.now, required: true, index: 1 }, + address: { type: String, required: true }, + address_lc: { type: String, required: true, lowercase: true, unique: true, index: 1 }, + flags: { + isVerified: { type: Boolean, default: false, required: true, index: 1 }, + isOptIn: { type: Boolean, default: false, required: true, index: 1 }, + isRejected: { type: Boolean, default: false, required: true, index: 1 }, + }, +}); + +module.exports = mongoose.model('NewsletterRecipient', NewsletterRecipientSchema); \ No newline at end of file diff --git a/app/models/newsletter.js b/app/models/newsletter.js new file mode 100644 index 0000000..f1e8bdd --- /dev/null +++ b/app/models/newsletter.js @@ -0,0 +1,31 @@ +// newsletter.js +// Copyright (C) 2021 Digital Telepresence, LLC +// License: Apache-2.0 + +'use strict'; + +const mongoose = require('mongoose'); + +const Schema = mongoose.Schema; + +const NEWSLETTER_STATUS_LIST = ['draft', 'published', 'archived']; + +const NewsletterSchema = new Schema({ + created: { type: Date, default: Date.now, required: true, index: -1 }, + author: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' }, + title: { type: String, required: true }, + summary: { type: String }, + content: { + html: { type: String, required: true, select: false, }, + text: { type: String, required: true, select: false, }, + }, + status: { + type: String, + enum: NEWSLETTER_STATUS_LIST, + default: 'draft', + required: true, + index: true, + }, +}); + +module.exports = mongoose.model('Newsletter', NewsletterSchema); \ No newline at end of file diff --git a/app/services/email.js b/app/services/email.js index eb4742e..13adde6 100644 --- a/app/services/email.js +++ b/app/services/email.js @@ -7,7 +7,7 @@ const path = require('path'); const pug = require('pug'); -const mailgun = require('mailgun-js'); +const nodemailer = require('nodemailer'); const mongoose = require('mongoose'); const EmailBlacklist = mongoose.model('EmailBlacklist'); @@ -27,13 +27,28 @@ class EmailService extends SiteService { async start ( ) { await super.start(); - if (process.env.DTP_MAILGUN_ENABLED === 'enabled') { - this.mg = mailgun({ - apiKey: process.env.MAILGUN_API_KEY, - domain: process.env.MAILGUN_DOMAIN, - }); + if (process.env.DTP_EMAIL_ENABLED !== 'enabled') { + return; } + const SMTP_PORT = parseInt(process.env.DTP_EMAIL_SMTP_PORT || '587', 10); + this.log.info('creating SMTP transport', { + host: process.env.DTP_EMAIL_SMTP_HOST, + port: SMTP_PORT, + }); + this.transport = nodemailer.createTransport({ + host: process.env.DTP_EMAIL_SMTP_HOST, + port: SMTP_PORT, + secure: process.env.DTP_EMAIL_SMTP_SECURE === 'enabled', + auth: { + user: process.env.DTP_EMAIL_SMTP_USER, + pass: process.env.DTP_EMAIL_SMTP_PASS, + }, + pool: true, + maxConnections: parseInt(process.env.DTP_EMAIL_SMTP_POOL_MAX_CONN || '5', 10), + maxMessages: parseInt(process.env.DTP_EMAIL_SMTP_POOL_MAX_MSGS || '5', 10), + }); + this.templates = { html: { welcome: pug.compileFile(path.join(this.dtp.config.root, 'app', 'templates', 'html', 'welcome.pug')), @@ -45,15 +60,8 @@ class EmailService extends SiteService { } async send (message) { - return new Promise((resolve, reject) => { - this.log.info('sending email', { to: message.to, subject: message.subject }); - this.mg.messages().send(message, async (error, body) => { - if (error) { - return reject(error); - } - resolve(body); - }); - }); + this.log.info('sending email', { to: message.to, subject: message.subject }); + await this.transport.sendMail(message); } async checkEmailAddress (emailAddress) { diff --git a/app/services/image.js b/app/services/image.js index f1c1423..dbb0f29 100644 --- a/app/services/image.js +++ b/app/services/image.js @@ -34,46 +34,53 @@ class ImageService extends SiteService { async create (owner, imageDefinition, file) { const NOW = new Date(); const { minio: minioService } = this.dtp.services; - - this.log.debug('processing uploaded image', { imageDefinition, file }); - - const sharpImage = await sharp(file.path); - const metadata = await sharpImage.metadata(); - - // create an Image model instance, but leave it here in application memory. - // we don't persist it to the db until MinIO accepts the binary data. - const image = new SiteImage(); - image.created = NOW; - image.owner = owner._id; - image.type = file.mimetype; - image.size = file.size; - image.file.bucket = process.env.MINIO_IMAGE_BUCKET; - image.metadata = this.makeImageMetadata(metadata); - - const imageId = image._id.toString(); - const ownerId = owner._id.toString(); - const fileKey = `/${ownerId.slice(0, 3)}/${ownerId}/${imageId.slice(0, 3)}/${imageId}`; - image.file.key = fileKey; - - // upload the image file to MinIO - const response = await minioService.uploadFile({ - bucket: image.file.bucket, - key: image.file.key, - filePath: file.path, - metadata: { - 'Content-Type': file.mimetype, - 'Content-Length': file.size, - }, - }); - - // store the eTag from MinIO in the Image model - image.file.etag = response.etag; - - // save the Image model to the db - await image.save(); - - this.log.info('processed uploaded image', { ownerId, imageId, fileKey }); - return image.toObject(); + try { + this.log.debug('processing uploaded image', { imageDefinition, file }); + + const sharpImage = await sharp(file.path); + const metadata = await sharpImage.metadata(); + + // create an Image model instance, but leave it here in application memory. + // we don't persist it to the db until MinIO accepts the binary data. + const image = new SiteImage(); + image.created = NOW; + image.owner = owner._id; + image.type = file.mimetype; + image.size = file.size; + image.file.bucket = process.env.MINIO_IMAGE_BUCKET; + image.metadata = this.makeImageMetadata(metadata); + + const imageId = image._id.toString(); + const ownerId = owner._id.toString(); + const fileKey = `/${ownerId.slice(0, 3)}/${ownerId}/${imageId.slice(0, 3)}/${imageId}`; + image.file.key = fileKey; + + // upload the image file to MinIO + const response = await minioService.uploadFile({ + bucket: image.file.bucket, + key: image.file.key, + filePath: file.path, + metadata: { + 'Content-Type': file.mimetype, + 'Content-Length': file.size, + }, + }); + + // store the eTag from MinIO in the Image model + image.file.etag = response.etag; + + // save the Image model to the db + await image.save(); + + this.log.info('processed uploaded image', { ownerId, imageId, fileKey }); + return image.toObject(); + } catch (error) { + this.log.error('failed to process image', { error }); + throw error; + } finally { + this.log.info('removing uploaded image from local file system', { file: file.path }); + await fs.promises.rm(file.path); + } } async getImageById (imageId) { diff --git a/app/services/markdown.js b/app/services/markdown.js new file mode 100644 index 0000000..376c746 --- /dev/null +++ b/app/services/markdown.js @@ -0,0 +1,37 @@ +// markdown.js +// Copyright (C) 2021 Digital Telepresence, LLC +// License: Apache-2.0 + +'use strict'; + +const fs = require('fs'); + +const { SiteService } = require('../../lib/site-lib'); + +const marked = require('marked'); + +class MarkdownService extends SiteService { + + constructor (dtp) { + super(dtp, module.exports); + } + + async start ( ) { + this.markedRenderer = new marked.Renderer(); + } + + async renderMarkdownFile (documentFile) { + const markdown = await fs.promises.readFile(documentFile, 'utf8'); + return this.renderMarkdown(markdown); + } + + async renderMarkdown (markdown) { + return marked(markdown, { renderer: this.markedRenderer }); + } +} + +module.exports = { + slug: 'markdown', + name: 'markdown', + create: (dtp) => { return new MarkdownService(dtp); }, +}; \ No newline at end of file diff --git a/app/services/minio.js b/app/services/minio.js index b523c5d..2aa1ebc 100644 --- a/app/services/minio.js +++ b/app/services/minio.js @@ -17,13 +17,15 @@ class MinioService extends SiteService { async start ( ) { await super.start(); - this.minio = new Minio.Client({ + const minioConfig = { endPoint: process.env.MINIO_ENDPOINT, port: parseInt(process.env.MINIO_PORT, 10), useSSL: (process.env.MINIO_USE_SSL === 'enabled'), accessKey: process.env.MINIO_ACCESS_KEY, secretKey: process.env.MINIO_SECRET_KEY, - }); + }; + this.log.debug('MinIO config', { minioConfig }); + this.minio = new Minio.Client(minioConfig); } async stop ( ) { diff --git a/app/services/newsletter.js b/app/services/newsletter.js new file mode 100644 index 0000000..bafb8c7 --- /dev/null +++ b/app/services/newsletter.js @@ -0,0 +1,114 @@ +// newsletter.js +// Copyright (C) 2021 Digital Telepresence, LLC +// License: Apache-2.0 + +'use strict'; + +const striptags = require('striptags'); + +const { SiteService } = require('../../lib/site-lib'); + +const mongoose = require('mongoose'); + +const Newsletter = mongoose.model('Newsletter'); +const NewsletterRecipient = mongoose.model('NewsletterRecipient'); + +class NewsletterService extends SiteService { + + constructor (dtp) { + super(dtp, module.exports); + + this.populateNewsletter = [ + { + path: 'author', + select: '_id username username_lc displayName picture', + }, + ]; + } + + async create (author, newsletterDefinition) { + const NOW = new Date(); + + const newsletter = new Newsletter(); + newsletter.created = NOW; + newsletter.author = author._id; + newsletter.title = striptags(newsletterDefinition.title.trim()); + newsletter.summary = striptags(newsletterDefinition.summary.trim()); + newsletter.content = newsletterDefinition.content.trim(); + newsletter.status = striptags(newsletterDefinition.status.trim().toLowerCase()); + + await newsletter.save(); + + return newsletter.toObject(); + } + + async update (newsletter, newsletterDefinition) { + const updateOp = { $set: { } }; + + if (newsletterDefinition.title) { + updateOp.$set.title = striptags(newsletterDefinition.title.trim()); + } + if (newsletterDefinition.summary) { + updateOp.$set.summary = striptags(newsletterDefinition.summary.trim()); + } + if (newsletterDefinition.content) { + updateOp.$set.content = newsletterDefinition.title.trim(); + } + if (newsletterDefinition.status) { + updateOp.$set.status = striptags(newsletterDefinition.status.trim()); + } + + if (Object.keys(updateOp.$set).length === 0) { + return; // no update to perform + } + + await Newsletter.updateOne( + { _id: newsletter._id }, + updateOp, + { upsert: true }, + ); + } + + async getNewsletters (pagination, status = ['published']) { + if (!Array.isArray(status)) { + status = [status]; + } + const newsletters = await Newsletter + .find({ status: { $in: status } }) + .sort({ created: -1 }) + .skip(pagination.skip) + .limit(pagination.cpp) + .lean(); + return newsletters; + } + + async getById (newsletterId) { + const newsletter = await Newsletter + .findById(newsletterId) + .select('+content') + .populate(this.populateNewsletter) + .lean(); + return newsletter; + } + + async addRecipient (emailAddress) { + const { email: emailService } = this.dtp.services; + const NOW = new Date(); + + await emailService.checkEmailAddress(emailAddress); + + const recipient = new NewsletterRecipient(); + recipient.created = NOW; + recipient.address = striptags(emailAddress.trim()); + recipient.address_lc = recipient.address.toLowerCase(); + await recipient.save(); + + return recipient.toObject(); + } +} + +module.exports = { + slug: 'newsletter', + name: 'newsletter', + create: (dtp) => { return new NewsletterService(dtp); }, +}; \ No newline at end of file diff --git a/app/views/admin/components/menu.pug b/app/views/admin/components/menu.pug index 811df24..562ae8a 100644 --- a/app/views/admin/components/menu.pug +++ b/app/views/admin/components/menu.pug @@ -9,6 +9,24 @@ ul.uk-nav.uk-nav-default li.uk-nav-divider + li(class={ 'uk-active': (adminView === 'post') }) + a(href="/admin/post") + span.nav-item-icon + i.fas.fa-pen + span.uk-margin-small-left Posts + li(class={ 'uk-active': (adminView === 'page') }) + a(href="/admin/page") + span.nav-item-icon + i.fas.fa-file + span.uk-margin-small-left Pages + li(class={ 'uk-active': (adminView === 'newsletter') }) + a(href="/admin/newsletter") + span.nav-item-icon + i.fas.fa-newspaper + span.uk-margin-small-left Newsletter + + li.uk-nav-divider + li(class={ 'uk-active': (adminView === 'host') }) a(href="/admin/host") span.nav-item-icon diff --git a/app/views/admin/layouts/main.pug b/app/views/admin/layouts/main.pug index 74b356b..1ce341c 100644 --- a/app/views/admin/layouts/main.pug +++ b/app/views/admin/layouts/main.pug @@ -4,7 +4,7 @@ block content-container block page-header section.uk-section.uk-section-header.uk-section-xsmall .uk-container - h1.uk-text-center DTP Sites Engine + h1.uk-text-center #{site.name} Admin block admin-layout diff --git a/app/views/admin/newsletter/editor.pug b/app/views/admin/newsletter/editor.pug new file mode 100644 index 0000000..1269717 --- /dev/null +++ b/app/views/admin/newsletter/editor.pug @@ -0,0 +1,59 @@ +extends ../layouts/main +block content + + - var actionUrl = newsletter ? `/admin/newsletter/${newsletter._id}` : `/admin/newsletter`; + + form(method="POST", action= actionUrl).uk-form + .uk-margin + label(for="title").uk-form-label.sr-only Newsletter title + input(id="title", name="title", type="text", placeholder= "Enter newsletter title", value= newsletter ? newsletter.title : undefined).uk-input + + .uk-margin + label(for="content-html").uk-form-label.sr-only Newsletter HTML body + textarea(id="content-html", name="content.html", rows="4").uk-textarea= newsletter ? newsletter.content.html : undefined + + .uk-margin + button(type="button", onclick="return dtp.app.copyHtmlToText(event, 'content-text');").uk-button.dtp-button-default Copy HTML to Text + + .uk-margin + label(for="content-text").uk-form-label.sr-only Newsletter text body + textarea(id="content-text", name="content.text", rows="4", placeholder= "Enter text-only version of newsletter.").uk-textarea= newsletter ? newsletter.content.text : undefined + + .uk-margin + label(for="summary").uk-form-label.sr-only Newsletter summary + textarea(id="summary", name="summary", rows="4", placeholder= "Enter newsletter summary (text only, no HTML)").uk-textarea= newsletter ? newsletter.summary : undefined + + button(type="submit").uk-button.dtp-button-primary= newsletter ? 'Update newsletter' : 'Save newsletter' + +block viewjs + script(src="/tinymce/tinymce.min.js") + script. + window.addEventListener('dtp-load', async ( ) => { + const toolbarItems = [ + 'undo redo', + 'formatselect', + 'bold italic backcolor', + 'alignleft aligncenter alignright alignjustify', + 'bullist numlist outdent indent removeformat', + 'link image', + 'help' + ]; + const pluginItems = [ + 'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'print', + 'preview', 'anchor', 'searchreplace', 'visualblocks', 'code', + 'fullscreen', 'insertdatetime', 'media', 'table', 'paste', 'code', + 'help', 'wordcount', + ] + + const editors = await tinymce.init({ + selector: 'textarea#content-html', + height: 500, + menubar: false, + plugins: pluginItems.join(' '), + toolbar: toolbarItems.join('|'), + branding: false, + images_upload_url: '/image/tinymce', + }); + + window.dtp.app.editor = editors[0]; + }); \ No newline at end of file diff --git a/app/views/admin/newsletter/index.pug b/app/views/admin/newsletter/index.pug new file mode 100644 index 0000000..b1e93a8 --- /dev/null +++ b/app/views/admin/newsletter/index.pug @@ -0,0 +1,21 @@ +extends ../layouts/main +block content + + .uk-margin + div(uk-grid) + .uk-width-expand + h1 Newsletters + .uk-width-auto + a(href="/admin/newsletter/compose").uk-button.dtp-button-primary + span + i.fas.fa-plus + span.uk-margin-small-left New Newsletter + + .uk-margin + if (Array.isArray(newsletters) && (newsletters.length > 0)) + ul.uk-list + each newsletter in newsletters + li + a(href=`/admin/newsletter/${newsletter._id}`)= newsletter.title + else + div There are no newsletters at this time. \ No newline at end of file diff --git a/app/views/index.pug b/app/views/index.pug index b8d965d..c2d64ca 100644 --- a/app/views/index.pug +++ b/app/views/index.pug @@ -62,7 +62,7 @@ block content .dtp-border-bottom.uk-margin h3.uk-heading-bullet Mailing List - form(method="post", action="/newsletter").uk-form + form(method="post", action="/newsletter", onsubmit="return dtp.app.submitForm(event, 'Subscribe to newsletter');").uk-form .uk-card.uk-card-secondary.uk-card-small .uk-card-body diff --git a/app/views/newsletter/index.pug b/app/views/newsletter/index.pug new file mode 100644 index 0000000..7e4cc10 --- /dev/null +++ b/app/views/newsletter/index.pug @@ -0,0 +1,15 @@ +extends ../layouts/main +block content + + section.uk-section.uk-section-default + .uk-container + + h1 #{site.name} Newsletters + + if Array.isArray(newsletters) && (newsletters.length > 0) + ul.uk-list + each newsletter of newsletters + li + a(href=`/newsletter/${newsletter._id}`).uk-link-reset= newsletter.title + else + div There are no newsletters at this time. Please check back later. \ No newline at end of file diff --git a/app/workers/newsletter.js b/app/workers/newsletter.js new file mode 100644 index 0000000..7e42dfe --- /dev/null +++ b/app/workers/newsletter.js @@ -0,0 +1,113 @@ +// newsletter.js +// Copyright (C) 2021 Digital Telepresence, LLC +// All Rights Reserved + +'use strict'; + +const DTP_COMPONENT_NAME = 'newsletter'; + +const path = require('path'); +require('dotenv').config({ path: path.resolve(__dirname, '..', '..', '.env') }); + +const mongoose = require('mongoose'); + +const { SitePlatform, SiteLog } = require(path.join(__dirname, '..', '..', 'lib', 'site-lib')); + +module.pkg = require(path.resolve(__dirname, '..', '..', 'package.json')); +module.config = { + componentName: DTP_COMPONENT_NAME, + root: path.resolve(__dirname, '..', '..'), +}; + +module.log = new SiteLog(module, module.config.componentName); + +module.sendNewsletter = async (job) => { + + module.log.info('newsletter email job received', { data: job.data }); + + const NewsletterRecipient = mongoose.model('NewsletterRecipient'); + + try { + /* + * Create one Bull Queue job per email to be delivered. + */ + await NewsletterRecipient + .find({ 'flags.isVerified': true, 'flags.isOptIn': true, 'flags.isRejected': false }) + .lean() + .cursor() + .eachAsync(async (recipient) => { + try { + const jobData = { + newsletterId: job.data.newsletterId, + recipient: recipient.address, + }; + const jobOptions = { + attempts: 3, + }; + await module.jobQueue.add('email-send', jobData, jobOptions); + } catch (error) { + module.log.error('failed to create newsletter email job'); + // but continue + } + }, { parallel: 4 }); + } catch (error) { + module.log('failed to send newsletter', { newsletterId: job.data.newsletterId, error }); + throw error; // throw error to Bull so it can report in job reports + } +}; + +module.sendNewsletterEmail = async (job) => { + const { newsletter: newsletterService, email: emailService } = module.services; + const { newsletterId, recipient } = job.data; + try { + + let newsletter = module.newsletters[newsletterId]; + + if (!newsletter) { + newsletter = await newsletterService.getById(newsletterId); + module.newsletters[newsletterId] = newsletter; //TODO: clean up memory leak of newsletter (remove when all emails are sent) + } + + if (!newsletter) { + throw new Error('newsletter not found'); + } + + const response = await emailService.send({ + from: 'demo@wherever.com', + to: recipient, + subject: newsletter.title, + html: newsletter.content.html, + text: newsletter.content.text, + }); + + job.log(`newsletter email sent: ${response}`); + } catch (error) { + module.error('failed to send newsletter email', { newsletterId, recipient, error }); + throw error; // throw error to Bull so it can report in job reports + } +}; + +(async ( ) => { + try { + /* + * Platform startup + */ + await SitePlatform.startPlatform(module); + + const { jobQueue: jobQueueService } = module.services; + + module.jobQueue = await jobQueueService.getJobQueue('newsletter', { + attempts: 3, + }); + module.jobQueue.process('email', module.sendNewsletter); + module.jobQueue.process('email-send', module.sendNewsletterEmail); + + /* + * Worker startup + */ + module.log.info(`${module.pkg.name} v${module.pkg.version} Newsletter worker started`); + } catch (error) { + module.log.error('failed to start Newsletter worker', { error }); + process.exit(-1); + } +})(); \ No newline at end of file diff --git a/client/js/site-app.js b/client/js/site-app.js index d9b43c8..22403ec 100644 --- a/client/js/site-app.js +++ b/client/js/site-app.js @@ -152,7 +152,13 @@ export default class DtpSiteApp extends DtpApp { }); if (!response.ok) { - throw new Error('Server error'); + let json; + try { + json = await response.json(); + } catch (error) { + throw new Error('Server error'); + } + throw new Error(json.message || 'Server error'); } await this.processResponse(response); @@ -163,6 +169,12 @@ export default class DtpSiteApp extends DtpApp { return; } + async copyHtmlToText (event, textContentId) { + const content = this.editor.getContent({ format: 'text' }); + const text = document.getElementById(textContentId); + text.value = content; + } + async selectImageFile (event) { event.preventDefault(); diff --git a/config/limiter.js b/config/limiter.js index ef75e8d..3b4c4db 100644 --- a/config/limiter.js +++ b/config/limiter.js @@ -119,6 +119,22 @@ module.exports = { } }, + /* + * NewsletterController + */ + newsletter: { + getView: { + total: 5, + expire: ONE_MINUTE, + message: 'You are reading newsletters too quickly', + }, + getIndex: { + total: 60, + expire: ONE_MINUTE, + message: 'You are fetching newsletters too quickly', + }, + }, + /* * UserController */ diff --git a/docs/services.md b/docs/services.md new file mode 100644 index 0000000..d69d9b5 --- /dev/null +++ b/docs/services.md @@ -0,0 +1,40 @@ +# DTP Sites: Services + +Services are common logic implemented in a centralized location made accessible to the rest of the application to perform tasks in a common way. They live in [app/services](app/services), and are scripts that export a specific structure that identifies the service and provides a way to create, start, and stop them. + +Services can't reference each other in their constructors, but they can reference each other in their `start` method with the caveat that the service you are referencing may not have had it's `start` method called, yet. DTP loads services in one loop, then starts them in a separate loop after they are loaded. + +All other service methods implemented can reference all other services with the assumption that they are started and ready to provide full service. + +```js +// myservice.js +// Copyright (C) 2021 Digital Telepresence, LLC +// License: Apache-2.0 + +'use strict'; + +const { SiteService } = require('../../lib/site-lib'); + +class MyService extends SiteService { + + constructor (dtp) { + super(dtp, module.exports); + } + + async start ( ) { + /* + * perform service initialization here + */ + } + + /* + * Implement service methods here + */ +} + +module.exports = { + slug: 'my-service', + name: 'myService', + create: (dtp) => { return new MyService(dtp); }, +}; +``` \ No newline at end of file diff --git a/package.json b/package.json index 080073d..952d093 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "ioredis": "^4.28.2", "jsdom": "^19.0.0", "libphonenumber-js": "^1.9.44", - "mailgun-js": "^0.22.0", "marked": "^4.0.6", "method-override": "^3.0.0", "minio": "^7.0.23", @@ -50,6 +49,7 @@ "morgan": "^1.10.0", "multer": "^1.4.3", "node-fetch": "2", + "nodemailer": "^6.7.2", "numeral": "^2.0.6", "otplib": "^12.0.1", "passport": "^0.5.0", diff --git a/yarn.lock b/yarn.lock index d66bce5..b76f5f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1265,13 +1265,6 @@ after@0.8.2: resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8= -agent-base@4, agent-base@^4.2.0, agent-base@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" - integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== - dependencies: - es6-promisify "^5.0.0" - agent-base@6: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -1279,13 +1272,6 @@ agent-base@6: dependencies: debug "4" -agent-base@~4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" - integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg== - dependencies: - es6-promisify "^5.0.0" - ajv-keywords@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" @@ -1547,13 +1533,6 @@ assign-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= -ast-types@0.x.x: - version "0.14.2" - resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.14.2.tgz#600b882df8583e3cd4f2df5fa20fa83759d4bdfd" - integrity sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA== - dependencies: - tslib "^2.0.1" - async-done@^1.2.0, async-done@^1.2.2: version "1.3.2" resolved "https://registry.yarnpkg.com/async-done/-/async-done-1.3.2.tgz#5e15aa729962a4b07414f528a88cdf18e0b290a2" @@ -1591,7 +1570,7 @@ async@1.5.2: resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= -async@^2.1.1, async@^2.6.1: +async@^2.1.1: version "2.6.3" resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== @@ -2301,7 +2280,7 @@ colors@^1.1.2, colors@^1.2.1: resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== -combined-stream@^1.0.6, combined-stream@^1.0.8: +combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -2574,11 +2553,6 @@ d@1, d@^1.0.1: es5-ext "^0.10.50" type "^1.0.1" -data-uri-to-buffer@1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz#77163ea9c20d8641b4707e8f18abdf9a78f34835" - integrity sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ== - data-urls@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.1.tgz#597fc2ae30f8bc4dbcf731fcd1b1954353afc6f8" @@ -2593,7 +2567,7 @@ date-now@^0.1.4: resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" integrity sha1-6vQ5/U1ISK105cx9vvIAZyueNFs= -debug@2, debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@~2.6.4: +debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@~2.6.4: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -2621,7 +2595,7 @@ debug@4.3.2: dependencies: ms "2.1.2" -debug@^3.1.0, debug@^3.2.6, debug@^3.2.7: +debug@^3.2.6, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== @@ -2730,15 +2704,6 @@ define-property@^2.0.2: is-descriptor "^1.0.2" isobject "^3.0.1" -degenerator@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-1.0.4.tgz#fcf490a37ece266464d9cc431ab98c5819ced095" - integrity sha1-/PSQo37OJmRk2cxDGrmMWBnO0JU= - dependencies: - ast-types "0.x.x" - escodegen "1.x.x" - esprima "3.x.x" - delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -3146,18 +3111,6 @@ es6-iterator@^2.0.1, es6-iterator@^2.0.3, es6-iterator@~2.0.3: es5-ext "^0.10.35" es6-symbol "^3.1.1" -es6-promise@^4.0.3: - version "4.2.8" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" - integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== - -es6-promisify@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" - integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= - dependencies: - es6-promise "^4.0.3" - es6-symbol@^3.1.1, es6-symbol@~3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" @@ -3196,18 +3149,6 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= -escodegen@1.x.x: - version "1.14.3" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" - integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== - dependencies: - esprima "^4.0.1" - estraverse "^4.2.0" - esutils "^2.0.2" - optionator "^0.8.1" - optionalDependencies: - source-map "~0.6.1" - escodegen@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" @@ -3228,11 +3169,6 @@ eslint-scope@5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -esprima@3.x.x: - version "3.1.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" - integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM= - esprima@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" @@ -3245,7 +3181,7 @@ esrecurse@^4.3.0: dependencies: estraverse "^5.2.0" -estraverse@^4.1.1, estraverse@^4.2.0: +estraverse@^4.1.1: version "4.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== @@ -3409,7 +3345,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@^3.0.0, extend@~3.0.2: +extend@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -3479,7 +3415,7 @@ feed@^4.2.2: dependencies: xml-js "^1.6.11" -file-uri-to-path@1, file-uri-to-path@1.0.0: +file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== @@ -3616,15 +3552,6 @@ foreach@^2.0.5: resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k= -form-data@^2.3.3: - version "2.5.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" - integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - form-data@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" @@ -3701,14 +3628,6 @@ fsevents@~2.3.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== -ftp@~0.3.10: - version "0.3.10" - resolved "https://registry.yarnpkg.com/ftp/-/ftp-0.3.10.tgz#9197d861ad8142f3e63d5a83bfe4c59f7330885d" - integrity sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0= - dependencies: - readable-stream "1.1.x" - xregexp "2.0.0" - function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -3802,18 +3721,6 @@ get-symbol-description@^1.0.0: call-bind "^1.0.2" get-intrinsic "^1.1.1" -get-uri@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-2.0.4.tgz#d4937ab819e218d4cb5ae18e4f5962bef169cc6a" - integrity sha512-v7LT/s8kVjs+Tx0ykk1I+H/rbpzkHvuIq87LmeXptcf5sNWm9uQiwjNAt94SJPA1zOlCntmnOlJvVWKmzsxG8Q== - dependencies: - data-uri-to-buffer "1" - debug "2" - extend "~3.0.2" - file-uri-to-path "1" - ftp "~0.3.10" - readable-stream "2" - get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" @@ -4236,14 +4143,6 @@ http-errors@~1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" -http-proxy-agent@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405" - integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg== - dependencies: - agent-base "4" - debug "3.1.0" - http-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" @@ -4262,14 +4161,6 @@ http-proxy@^1.18.1: follow-redirects "^1.0.0" requires-port "^1.0.0" -https-proxy-agent@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz#b8c286433e87602311b01c8ea34413d856a4af81" - integrity sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg== - dependencies: - agent-base "^4.3.0" - debug "^3.1.0" - https-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" @@ -4337,16 +4228,6 @@ indexof@0.0.1: resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10= -inflection@~1.12.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.12.0.tgz#a200935656d6f5f6bc4dc7502e1aecb703228416" - integrity sha1-ogCTVlbW9fa8TcdQLhrstwMihBY= - -inflection@~1.3.0: - version "1.3.8" - resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.3.8.tgz#cbd160da9f75b14c3cc63578d4f396784bf3014e" - integrity sha1-y9Fg2p91sUw8xjV41POWeEvzAU4= - inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -4420,11 +4301,6 @@ ip-address@^5.8.9: lodash "^4.17.15" sprintf-js "1.1.2" -ip@1.1.5, ip@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" - integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= - ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" @@ -4754,11 +4630,6 @@ is-shared-array-buffer@^1.0.1: resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6" integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== -is-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= - is-stream@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" @@ -5295,13 +5166,6 @@ lowercase-keys@^2.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== -lru-cache@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -5316,21 +5180,6 @@ magic-string@^0.25.0, magic-string@^0.25.7: dependencies: sourcemap-codec "^1.4.4" -mailgun-js@^0.22.0: - version "0.22.0" - resolved "https://registry.yarnpkg.com/mailgun-js/-/mailgun-js-0.22.0.tgz#128942b5e47a364a470791608852bf68c96b3a05" - integrity sha512-a2alg5nuTZA9Psa1pSEIEsbxr1Zrmqx4VkgGCQ30xVh0kIH7Bu57AYILo+0v8QLSdXtCyLaS+KVmdCrQo0uWFA== - dependencies: - async "^2.6.1" - debug "^4.1.0" - form-data "^2.3.3" - inflection "~1.12.0" - is-stream "^1.1.0" - path-proxy "~1.0.0" - promisify-call "^2.0.2" - proxy-agent "^3.0.3" - tsscmp "^1.0.6" - make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -5700,11 +5549,6 @@ neo-async@^2.6.2: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -netmask@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/netmask/-/netmask-1.0.6.tgz#20297e89d86f6f6400f250d9f4f6b4c1945fcd35" - integrity sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU= - next-tick@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" @@ -5734,6 +5578,11 @@ node-releases@^2.0.1: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== +nodemailer@^6.7.2: + version "6.7.2" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.2.tgz#44b2ad5f7ed71b7067f7a21c4fedabaec62b85e0" + integrity sha512-Dz7zVwlef4k5R71fdmxwR8Q39fiboGbu3xgswkzGwczUfjp873rVxt1O46+Fh0j1ORnAC6L9+heI8uUpO6DT7Q== + nodemon@^2.0.2: version "2.0.15" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.15.tgz#504516ce3b43d9dc9a955ccd9ec57550a31a8d4e" @@ -6026,31 +5875,6 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== -pac-proxy-agent@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-3.0.1.tgz#115b1e58f92576cac2eba718593ca7b0e37de2ad" - integrity sha512-44DUg21G/liUZ48dJpUSjZnFfZro/0K5JTyFYLBcmh9+T6Ooi4/i4efwUiEy0+4oQusCBqWdhv16XohIj1GqnQ== - dependencies: - agent-base "^4.2.0" - debug "^4.1.1" - get-uri "^2.0.0" - http-proxy-agent "^2.1.0" - https-proxy-agent "^3.0.0" - pac-resolver "^3.0.0" - raw-body "^2.2.0" - socks-proxy-agent "^4.0.1" - -pac-resolver@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-3.0.0.tgz#6aea30787db0a891704deb7800a722a7615a6f26" - integrity sha512-tcc38bsjuE3XZ5+4vP96OfhOugrX+JcnpUbhfuc4LuXBLQhoTthOstZeoQJBDnQUDYzYmdImKsbz0xSl1/9qeA== - dependencies: - co "^4.6.0" - degenerator "^1.0.4" - ip "^1.1.5" - netmask "^1.0.6" - thunkify "^2.1.2" - package-json@^6.3.0: version "6.5.0" resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" @@ -6169,13 +5993,6 @@ path-parse@^1.0.6: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-proxy@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/path-proxy/-/path-proxy-1.0.0.tgz#18e8a36859fc9d2f1a53b48dee138543c020de5e" - integrity sha1-GOijaFn8nS8aU7SN7hOFQ8Ag3l4= - dependencies: - inflection "~1.3.0" - path-root-regex@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/path-root-regex/-/path-root-regex-0.1.2.tgz#bfccdc8df5b12dc52c8b43ec38d18d72c04ba96d" @@ -6334,13 +6151,6 @@ promise@^7.0.1: dependencies: asap "~2.0.3" -promisify-call@^2.0.2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/promisify-call/-/promisify-call-2.0.4.tgz#d48c2d45652ccccd52801ddecbd533a6d4bd5fba" - integrity sha1-1IwtRWUszM1SgB3ey9UzptS9X7o= - dependencies: - with-callback "^1.0.2" - proxy-addr@~2.0.5: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -6349,25 +6159,6 @@ proxy-addr@~2.0.5: forwarded "0.2.0" ipaddr.js "1.9.1" -proxy-agent@^3.0.3: - version "3.1.1" - resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-3.1.1.tgz#7e04e06bf36afa624a1540be247b47c970bd3014" - integrity sha512-WudaR0eTsDx33O3EJE16PjBRZWcX8GqCEeERw1W3hZJgH/F2a46g7jty6UGty6NeJ4CKQy8ds2CJPMiyeqaTvw== - dependencies: - agent-base "^4.2.0" - debug "4" - http-proxy-agent "^2.1.0" - https-proxy-agent "^3.0.0" - lru-cache "^5.1.1" - pac-proxy-agent "^3.0.1" - proxy-from-env "^1.0.0" - socks-proxy-agent "^4.0.1" - -proxy-from-env@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" @@ -6580,7 +6371,7 @@ raw-body@2.4.0: iconv-lite "0.4.24" unpipe "1.0.0" -raw-body@^2.2.0, raw-body@^2.3.2: +raw-body@^2.3.2: version "2.4.2" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.2.tgz#baf3e9c21eebced59dd6533ac872b71f7b61cb32" integrity sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ== @@ -6654,7 +6445,16 @@ readable-stream@1.1.x: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: +"readable-stream@2 || 3", readable-stream@3, readable-stream@^3.1.1, readable-stream@^3.4.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -6667,15 +6467,6 @@ readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stre string_decoder "~1.1.1" util-deprecate "~1.0.1" -"readable-stream@2 || 3", readable-stream@3, readable-stream@^3.1.1, readable-stream@^3.4.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - readdirp@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" @@ -7281,11 +7072,6 @@ slug@^5.1.0: resolved "https://registry.yarnpkg.com/slug/-/slug-5.1.0.tgz#8a7e30ca1c3a6dc40cf74e269750913a865edb0b" integrity sha512-IS39jKR6m+puU8zWgH6ruwx1sfzFNJ6Ai5PKIlUqd0X8C3ca7PB49Cvm0uayqgEt1jgaojO2wWEsQJngnh7fDA== -smart-buffer@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" - integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== - snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -7414,22 +7200,6 @@ socket.io@^4.4.0: socket.io-adapter "~2.3.3" socket.io-parser "~4.0.4" -socks-proxy-agent@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz#3c8991f3145b2799e70e11bd5fbc8b1963116386" - integrity sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg== - dependencies: - agent-base "~4.2.1" - socks "~2.3.2" - -socks@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.3.3.tgz#01129f0a5d534d2b897712ed8aceab7ee65d78e3" - integrity sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA== - dependencies: - ip "1.1.5" - smart-buffer "^4.1.0" - source-list-map@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" @@ -7878,11 +7648,6 @@ through@^2.3.8: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= -thunkify@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/thunkify/-/thunkify-2.1.2.tgz#faa0e9d230c51acc95ca13a361ac05ca7e04553d" - integrity sha1-+qDp0jDFGsyVyhOjYawFyn4EVT0= - time-stamp@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3" @@ -8005,16 +7770,11 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= -tslib@^2.0.1, tslib@^2.3.0: +tslib@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== -tsscmp@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb" - integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA== - tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" @@ -8578,11 +8338,6 @@ widest-line@^3.1.0: dependencies: string-width "^4.0.0" -with-callback@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/with-callback/-/with-callback-1.0.2.tgz#a09629b9a920028d721404fb435bdcff5c91bc21" - integrity sha1-oJYpuakgAo1yFAT7Q1vc/1yRvCE= - with@^7.0.0: version "7.0.2" resolved "https://registry.yarnpkg.com/with/-/with-7.0.2.tgz#ccee3ad542d25538a7a7a80aad212b9828495bac" @@ -8870,11 +8625,6 @@ xmlhttprequest-ssl@~1.6.2: resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz#03b713873b01659dfa2c1c5d056065b27ddc2de6" integrity sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q== -xregexp@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943" - integrity sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM= - xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" @@ -8895,11 +8645,6 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== -yallist@^3.0.2: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"