Browse Source

many updates from Soapbox

develop
Rob Colbert 2 years ago
parent
commit
0423f3f8a6
  1. 78
      app/controllers/email.js
  2. 19
      app/models/email-log.js
  3. 18
      app/models/email-verify.js
  4. 4
      app/models/user.js
  5. 84
      app/services/email.js
  6. 6
      app/services/user.js
  7. 16
      app/templates/common/html/footer.pug
  8. 3
      app/templates/common/html/header.pug
  9. 15
      app/templates/common/text/footer.pug
  10. 3
      app/templates/common/text/header.pug
  11. 3
      app/templates/html/user-email.pug
  12. 31
      app/templates/html/welcome.pug
  13. 106
      app/templates/layouts/html/system-message.pug
  14. 4
      app/templates/layouts/library.pug
  15. 10
      app/templates/layouts/text/system-message.pug
  16. 5
      app/templates/text/user-email.pug
  17. 14
      app/templates/text/welcome.pug
  18. 12
      app/views/email/verify-success.pug
  19. 2
      app/views/welcome/login.pug
  20. 5
      app/views/welcome/signup.pug
  21. 18
      config/limiter.js
  22. 17
      lib/site-common.js

78
app/controllers/email.js

@ -0,0 +1,78 @@
// email.js
// Copyright (C) 2021 Digital Telepresence, LLC
// License: Apache-2.0
'use strict';
const DTP_COMPONENT_NAME = 'email';
const express = require('express');
const { SiteController/*, SiteError*/ } = require('../../lib/site-lib');
class EmailController extends SiteController {
constructor (dtp) {
super(dtp, DTP_COMPONENT_NAME);
}
async start ( ) {
const { jobQueue: jobQueueService, limiter: limiterService } = this.dtp.services;
this.emailJobQueue = jobQueueService.getJobQueue('email', {
attempts: 3
});
const router = express.Router();
this.dtp.app.use('/email', router);
router.get(
'/verify',
limiterService.create(limiterService.config.email.getEmailVerify),
this.getEmailVerify.bind(this),
);
router.get(
'/opt-out',
limiterService.create(limiterService.config.email.getEmailOptOut),
this.getEmailOptOut.bind(this),
);
return router;
}
async getEmailOptOut (req, res, next) {
const { user: userService } = this.dtp.services;
try {
await userService.emailOptOut(req.query.u, req.query.c);
res.render('email/opt-out-success');
} catch (error) {
this.log.error('failed to opt-out from email', {
userId: req.query.t,
category: req.query.c,
error,
});
return next(error);
}
}
async getEmailVerify (req, res, next) {
const { email: emailService } = this.dtp.services;
try {
await emailService.verifyToken(req.query.t);
res.render('email/verify-success');
} catch (error) {
this.log.error('failed to verify email', { error });
return next(error);
}
}
}
module.exports = {
slug: 'email',
name: 'email',
create: async (dtp) => {
let controller = new EmailController(dtp);
return controller;
},
};

19
app/models/email-log.js

@ -0,0 +1,19 @@
// email-log.js
// Copyright (C) 2022 DTP Technologies, LLC
// All Rights Reserved
'use strict';
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const EmailLogSchema = new Schema({
created: { type: Date, default: Date.now, required: true, index: -1 },
from: { type: String, required: true, },
to: { type: String, required: true },
to_lc: { type: String, required: true, lowercase: true, index: 1 },
subject: { type: String },
messageId: { type: String },
});
module.exports = mongoose.model('EmailLog', EmailLogSchema);

18
app/models/email-verify.js

@ -0,0 +1,18 @@
// email-verify.js
// Copyright (C) 2021 Digital Telepresence, LLC
// License: Apache-2.0
'use strict';
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const EmailVerifySchema = new Schema({
created: { type: Date, default: Date.now, required: true, index: -1, expires: '30d' },
verified: { type: Date },
user: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' },
token: { type: String, required: true },
});
module.exports = mongoose.model('EmailVerify', EmailVerifySchema);

4
app/models/user.js

@ -23,8 +23,8 @@ const UserPermissionsSchema = new Schema({
});
const UserOptInSchema = new Schema({
system: { type: Boolean, default: true, requred: true },
marketing: { type: Boolean, default: true, requred: true },
system: { type: Boolean, default: true, required: true },
marketing: { type: Boolean, default: true, required: true },
});
const UserSchema = new Schema({

84
app/services/email.js

@ -4,19 +4,20 @@
'use strict';
const path = require('path');
const pug = require('pug');
const nodemailer = require('nodemailer');
const uuidv4 = require('uuid').v4;
const mongoose = require('mongoose');
const EmailBlacklist = mongoose.model('EmailBlacklist');
const EmailVerify = mongoose.model('EmailVerify');
const EmailLog = mongoose.model('EmailLog');
const disposableEmailDomains = require('disposable-email-provider-domains');
const emailValidator = require('email-validator');
const emailDomainCheck = require('email-domain-check');
const { SiteService } = require('../../lib/site-lib');
const { SiteService, SiteError } = require('../../lib/site-lib');
class EmailService extends SiteService {
@ -27,7 +28,8 @@ class EmailService extends SiteService {
async start ( ) {
await super.start();
if (process.env.DTP_EMAIL_ENABLED !== 'enabled') {
if (process.env.DTP_EMAIL_SERVICE !== 'enabled') {
this.log.info("DTP_EMAIL_SERVICE is disabled, the system can't send email and will not try.");
return;
}
@ -51,17 +53,35 @@ class EmailService extends SiteService {
this.templates = {
html: {
welcome: pug.compileFile(path.join(this.dtp.config.root, 'app', 'templates', 'html', 'welcome.pug')),
userEmail: this.loadAppTemplate('html', 'user-email.pug'),
welcome: this.loadAppTemplate('html', 'welcome.pug'),
},
text: {
welcome: pug.compileFile(path.join(this.dtp.config.root, 'app', 'templates', 'text', 'welcome.pug')),
userEmail: this.loadAppTemplate('text', 'user-email.pug'),
welcome: this.loadAppTemplate('text', 'welcome.pug'),
},
};
}
async renderTemplate (templateId, templateType, templateModel) {
this.log.debug('rendering email template', { templateId, templateType });
return this.templates[templateType][templateId](templateModel);
}
async send (message) {
const NOW = new Date();
this.log.info('sending email', { to: message.to, subject: message.subject });
await this.transport.sendMail(message);
const response = await this.transport.sendMail(message);
await EmailLog.create({
created: NOW,
from: message.from,
to: message.to,
to_lc: message.to.toLowerCase(),
subject: message.subject,
messageId: response.messageId,
});
}
async checkEmailAddress (emailAddress) {
@ -97,8 +117,52 @@ class EmailService extends SiteService {
return false;
}
async renderTemplate (templateId, templateType, message) {
return this.templates[templateType][templateId](message);
async createVerificationToken (user) {
const NOW = new Date();
const verify = new EmailVerify();
verify.created = NOW;
verify.user = user._id;
verify.token = uuidv4();
await verify.save();
this.log.info('created email verification token for user', { user: user._id });
return verify.toObject();
}
async verifyToken (token) {
const NOW = new Date();
const { user: userService } = this.dtp.services;
// fetch the token from the db
const emailVerify = await EmailVerify
.findOne({ token: token })
.populate(this.populateEmailVerify)
.lean();
// verify that the token is at least valid (it exists)
if (!emailVerify) {
this.log.error('email verify token not found', { token });
throw new SiteError(403, 'Email verification token is invalid');
}
// verify that it hasn't already been verified (user clicked link more than once)
if (emailVerify.verified) {
this.log.error('email verify token already claimed', { token });
throw new SiteError(403, 'Email verification token is invalid');
}
this.log.info('marking user email verified', { userId: emailVerify.user._id });
await userService.setEmailVerification(emailVerify.user, true);
await EmailVerify.updateOne({ _id: emailVerify._id }, { $set: { verified: NOW } });
}
async removeVerificationTokensForUser (user) {
this.log.info('removing all pending email address verification tokens for user', { user: user._id });
await EmailVerify.deleteMany({ user: user._id });
}
}

6
app/services/user.js

@ -107,7 +107,7 @@ class UserService {
user.optIn = {
system: true,
newsletter: false,
marketing: false,
};
this.log.info('creating new user account', { email: userDefinition.email });
@ -280,7 +280,7 @@ class UserService {
.select('+passwordSalt +password +flags')
.lean();
if (!user) {
throw new SiteError(404, 'Member account not found');
throw new SiteError(404, 'Member credentials are invalid');
}
const maskedPassword = crypto.maskPassword(
@ -288,7 +288,7 @@ class UserService {
account.password,
);
if (maskedPassword !== user.password) {
throw new SiteError(403, 'Account credentials do not match');
throw new SiteError(403, 'Member credentials are invalid');
}
// remove these critical fields from the user object

16
app/templates/common/html/footer.pug

@ -1,10 +1,8 @@
.common-footer
p This email was sent to #{user.email} because you selected to receive emails from LocalRedAction.com. You can #[a(href=`https://localredaction.com/opt-out/${voter._id}/email`) opt out] at any time to stop receiving these emails.
p You can request to stop receiving these emails in writing at:
address
div Local Red Action
div P.O. Box ########
div McKees Rocks, PA 15136
div USA
p This email was sent to #{recipient.email} because you selected to receive emails from #{site.name}. You can #[a(href=`https://${site.domain}/email/opt-out?u=${recipient._id}&c=marketing`) opt out] at any time to stop receiving these emails.
//- p You can request to stop receiving these emails in writing at:
//- address
//- div Digital Telepresence, LLC
//- div P.O. Box ########
//- div McKees Rocks, PA 15136
//- div USA

3
app/templates/common/html/header.pug

@ -1 +1,2 @@
.greeting Dear #{voter.name},
.common-title= emailTitle || `Greetings from ${site.name}!`
.common-slogan= site.description

15
app/templates/common/text/footer.pug

@ -1,9 +1,10 @@
|
| - - -
| This email was sent to #{user.email} because you selected to receive emails from LocalRedAction.com. Visit #{`https://localredaction.com/opt-out/${voter._id}/email`} to opt out and stop receiving these emails.
| This email was sent to #{recipient.email} because you selected to receive emails from #{site.name}. Visit #{`https://${site.domain}/email/opt-out?u=${recipient._id}&c=marketing`} to opt out and stop receiving these emails.
|
| You can request to stop receiving these emails in writing at:
|
| Local Red Action
| P.O. Box ########
| McKees Rocks, PA 15136
| USA
//- | You can request to stop receiving these emails in writing at:
//- |
//- | #{site.company}
//- | P.O. Box ########
//- | McKees Rocks, PA 15136
//- | USA

3
app/templates/common/text/header.pug

@ -1 +1,2 @@
| Dear #{voter.name},
| Dear #{recipient.displayName || recipient.username},
|

3
app/templates/html/user-email.pug

@ -0,0 +1,3 @@
extends ../layouts/html/system-message
block message-body
.content-message!= htmlMessage

31
app/templates/html/welcome.pug

@ -1,27 +1,4 @@
doctype html
html(lang='en')
head
meta(charset='UTF-8')
meta(name='viewport', content='width=device-width, initial-scale=1.0')
meta(name='description', content= pageDescription || siteDescription)
title= pageTitle ? `${pageTitle} | ${site.name}` : site.name
style(type="text/css").
html, body {
margin: 0;
padding: 0;
}
.greeting { font-size: 1.5em; margin-bottom: 16px; }
.message {}
body
include ../common/html/header
.message
p Welcome to #{service.name}! Please visit #[a(href=`https://localredaction.com/verify?t=${emailVerifyToken}`)= `https://localredaction.com/verify?t=${emailVerifyToken}`] to verify your email address.
p Thank you for supporting your local Republican committee and candidates!
include ../common/html/footer
extends ../layouts/html/system-message
block content
p Welcome to #{site.name}! Please visit #[a(href=`https://${site.domain}/email/verify?t=${emailVerifyToken}`)= `https://${site.domain}/email/verify?t=${emailVerifyToken}`] to verify your email address and enable all features on your new account.
p If you did not sign up for a new account at #{site.name}, please disregard this message.

106
app/templates/layouts/html/system-message.pug

@ -0,0 +1,106 @@
doctype html
html(lang='en')
head
meta(charset='UTF-8')
meta(name='viewport', content='width=device-width, initial-scale=1.0')
meta(name='description', content= pageDescription || siteDescription)
title= pageTitle ? `${pageTitle} | ${site.name}` : site.name
style(type="text/css").
html, body {
padding: 0;
margin: 0;
background-color: #ffffff;
color: #1a1a1a;
}
section {
padding: 20px 0;
background-color: #ffffff;
color: #1a1a1a;
}
section.section-muted {
background-color: #f8f8f8;
color: #2a2a2a;
}
.common-title {
max-width: 640px;
margin: 0 auto 8px auto;
font-size: 1.5em;
}
.common-greeting {
max-width: 640px;
margin: 0 auto 20px auto;
font-size: 1.1em;
}
.common-slogan {
max-width: 640px;
margin: 0 auto;
font-size: 1.1em;
font-style: italic;
}
.content-message {
max-width: 640px;
margin: 0 auto;
background: white;
color: black;
font-size: 14px;
}
.content-signature {
max-width: 640px;
margin: 0 auto;
background: white;
color: black;
font-size: 14px;
}
.common-footer {
max-width: 640px;
margin: 0 auto;
font-size: 10px;
}
.channel-icon {
border-radius: 8px;
}
.action-button {
padding: 6px 20px;
margin: 24px 0;
border: none;
border-radius: 20px;
outline: none;
background-color: #1093de;
color: #ffffff;
font-size: 16px;
font-weight: bold;
}
body
include ../library
section.section-muted
include ../../common/html/header
section
.common-greeting
div Dear #{recipient.displayName || recipient.username},
block message-body
.content-message
block content
.content-signature
p Thank you for your continued support!
p The #{site.name} team.
section.section-muted
include ../../common/html/footer

4
app/templates/layouts/library.pug

@ -0,0 +1,4 @@
-
function formatCount (count) {
return numeral(count).format((count > 1000) ? '0,0.0a' : '0,0');
}

10
app/templates/layouts/text/system-message.pug

@ -0,0 +1,10 @@
include ../library
include ../../common/text/header
|
block content
|
| Thank you for your continued support!
|
| The #{site.name} team.
|
include ../../common/text/footer

5
app/templates/text/user-email.pug

@ -0,0 +1,5 @@
extends ../layouts/text/system-message
block content
|
| #{textMessage}
|

14
app/templates/text/welcome.pug

@ -1,7 +1,7 @@
include ../common/text/header
|
| Welcome to #{service.name}! Please visit #[a(href=`https://localredaction.com/verify?t=${emailVerifyToken}`)= `https://localredaction.com/verify?t=${emailVerifyToken}`] to verify your email address.
|
| Thank you for supporting your local Republican committee and candidates!
|
include ../common/text/footer
extends ../layouts/text/system-message
block content
|
| Welcome to #{site.name}! Please visit #{`https://${site.domain}/email/verify?t=${emailVerifyToken}`} to verify your email address and enable all features on your new account.
|
| If you did not sign up for a new account at #{site.name}, please disregard this message.
|

12
app/views/email/verify-success.pug

@ -0,0 +1,12 @@
extends ../layouts/main
block content
section.uk-section.uk-section-default.uk-section-small
.uk-container
h1 Email Verification
p You have successfully verified your email address, and have unlocked additional features of the site.
a(href="/").uk-button.dtp-button-primary
span
i.fas.fa-home
span.uk-margin-small-left Home

2
app/views/welcome/login.pug

@ -12,7 +12,7 @@ block content
.uk-margin-small
div(uk-grid)
.uk-width-1-3
img(src=`/img/icon/${site.domainKey}.png`).responsive
img(src=`/img/icon/${site.domainKey}/icon-256x256.png`).responsive.uk-border-rounded
.uk-width-expand
if loginResult
div(uk-alert).uk-alert.uk-alert-danger= loginResult

5
app/views/welcome/signup.pug

@ -35,11 +35,6 @@ block content
input(id="passwordv", name="passwordv", type="password", placeholder="Verify password").uk-input
.uk-text-small.uk-text-muted.uk-margin-small-top(class="uk-visible@m") Please enter your password again to prove you're not an idiot.
.uk-margin
label
input(type="checkbox", checked).uk-checkbox
| Join #{site.name}'s Newsletter
section.uk-section.uk-section-secondary.uk-section
.uk-container.uk-container-small
.uk-margin-large

18
config/limiter.js

@ -6,7 +6,7 @@
const ONE_SECOND = 1000;
const ONE_MINUTE = ONE_SECOND * 60;
// const ONE_HOUR = ONE_MINUTE * 60;
const ONE_HOUR = ONE_MINUTE * 60;
module.exports = {
@ -70,6 +70,22 @@ module.exports = {
},
},
/*
* EmailController
*/
email: {
getEmailOptOut: {
total: 10,
expire: ONE_HOUR,
message: "You really don't need to do that this much.",
},
getEmailVerify: {
total: 10,
expire: ONE_HOUR,
message: "You really don't need to do that this much and can stop.",
},
},
/*
* HomeController
*/

17
lib/site-common.js

@ -4,12 +4,16 @@
'use strict';
const path = require('path');
const pug = require('pug');
const Events = require('events');
class SiteCommon extends Events {
constructor (dtp) {
super();
this.dtp = dtp;
this.appTemplateRoot = path.join(this.dtp.config.root, 'app', 'templates');
}
saveSession (req) {
@ -22,6 +26,19 @@ class SiteCommon extends Events {
});
});
}
isValidString (text) {
return text && (typeof text === 'string') && (text.length > 0);
}
loadAppTemplate (type, name) {
return pug.compileFile(path.join(this.appTemplateRoot, type, name));
}
loadViewTemplate (filename) {
const scriptFile = path.join(this.dtp.config.root, 'app', 'views', filename);
return pug.compileFile(scriptFile);
}
}
module.exports.SiteCommon = SiteCommon;
Loading…
Cancel
Save