Fleshed out API, added simple email support.

This commit is contained in:
2018-07-04 08:17:12 +10:00
parent aa532e0fc8
commit 13982239d4
12 changed files with 343 additions and 7 deletions

26
common/Email.js Normal file
View File

@ -0,0 +1,26 @@
// Copyright (c) 2018 Dominic Masters
//
// MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module.exports = {
REGEX: /^[-a-z0-9~!$%^&*_=+}{\'?]+(\.[-a-z0-9~!$%^&*_=+}{\'?]+)*@([a-z0-9_][-a-z0-9_]*(\.[-a-z0-9_]+)*\.(aero|arpa|biz|com|coop|edu|gov|info|int|mil|museum|name|net|org|pro|travel|mobi|[a-z][a-z])|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}))(:[0-9]{1,5})?$/i
}

42
common/Forms.js Normal file
View File

@ -0,0 +1,42 @@
// Copyright (c) 2018 Dominic Masters
//
// MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
const Email = require('./Email');
module.exports = {
contact: {
name: {
maxLength: 64,
required: true
},
email: {
regex: Email.REGEX,
maxLength: 128,
required: true
},
message: {
maxLength: 4000,
required: true
}
}
};

View File

@ -38,6 +38,7 @@
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.4.1",
"node-sass": "^4.9.0",
"nodemailer": "^4.6.7",
"pg-promise": "^8.4.4",
"react": "^16.4.0",
"react-dom": "^16.4.0",
@ -48,6 +49,7 @@
"react-tap-event-plugin": "^3.0.3",
"react-transition-group": "^2.3.1",
"redux": "^4.0.0",
"sanitize-html": "^1.18.2",
"sass-loader": "^7.0.3",
"style-loader": "^0.21.0",
"uglifyjs-webpack-plugin": "^1.2.7",
@ -57,6 +59,7 @@
"devDependencies": {
"nodemon": "^1.17.5",
"react-hot-loader": "^4.3.3",
"webpack-cli": "^3.0.8",
"webpack-dev-server": "^3.1.4"
}
}

View File

@ -36,9 +36,10 @@ class API {
getHandlers() {return this.handlers;}
getServer() {return this.server;}
getApp() {return this.server.getApp();}
getApp() {return this.getServer().getApp();}
getExpress() {return this.getServer().getExpress();}
getConfig() {return this.getApp().getConfig();}
getEmail() {return this.getApp().getEmail();}
addHandler(handler) {this.handlers.push(handler);}

View File

@ -38,6 +38,7 @@ class APIHandler {
this.paths = paths;
}
getAPI() {return this.api;}
getMethods() {return this.methods;}
getPaths() {return this.paths;}

View File

@ -21,6 +21,8 @@
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
const Forms = require('./../../common/Forms');
class APIRequest {
constructor(handler, req, res) {
this.handler = handler;
@ -32,12 +34,14 @@ class APIRequest {
getRequest() {return this.req;}
getResponse() {return this.res;}
getHandleFunction() {return this.getHandler().handle;}
getFormData(name) {return Forms[name];}
//Some nice shorthands
getAPI() {return this.getHandler().getAPI();}
getConfig() {return this.getAPI().getConfig();}
getServer() {return this.getAPI().getServer();}
getExpress() {return this.getAPI().getExpress();}
getEmail() {return this.getAPI().getEmail();}
getApp() {return this.getAPI().getApp();}
//Our process method
@ -54,7 +58,8 @@ class APIRequest {
try {
response = await this.getHandleFunction()(this);
} catch(e) {
response = { ok: false, data: ( e.message || "Unknown Error Occured" ) };
console.error(e);
response = { ok: false, data: "An unknown error occured" };
}
}

View File

@ -0,0 +1,101 @@
// Copyright (c) 2018 Dominic Masters
//
// MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
const
APIHandler = require('./../../APIHandler'),
sanitizeHtml = require('sanitize-html')
;
const ERRORS = {
name: "Invalid name",
email: "Invalid email",
message: "Invalid message",
sending: "An unknown error occured"
};
module.exports = class Send extends APIHandler {
constructor(api) {
super(api, ['GET', 'POST'], '/contact/send');
}
async handle(request) {
let form = request.getFormData("contact");
let name,email,message;
if(form.name.required && !request.hasString('name', form.name.maxLength)) return {ok: false, data: ERRORS.name};
name = request.getString('name', form.name.maxLength);
if(form.email.required) {
if(!request.hasString('email', form.email.maxLength)) return { ok: false, data: ERRORS.email };
email = request.getString('email', form.email.maxLength);
if(!form.email.regex.test(email)) return { ok: false, data: ERRORS.email };
}
if(form.message.required && !request.hasString('message', form.message.maxLength)) return {ok: false, data: ERRORS.message};
message = request.getString('message', form.name.maxLength);
//Now let's create our message, we're gonna have to do some rudementry HTML...
let textMessage = '';
let htmlMessage = '';
//First the name
textMessage += 'Name: ' + name;
htmlMessage += '<p><strong>Name: </strong>' + sanitizeHtml(name) + '</p>';
//Now the response Email
textMessage += '\nEmail: ' + email;
htmlMessage += '<p><strong>Email: </strong>' + sanitizeHtml(email) + '</p>';
//Message!
textMessage += '\nMessage: ' + message;
htmlMessage += '<p><strong>Message:</strong></p>';
htmlMessage += '<p>' + sanitizeHtml(message) + '</p>';
htmlMessage += '<hr />';
htmlMessage += '<p>Reply: <a href="mailto:'+email+'">'+email+'</a></p>';
//Now we can send it!
try {
await request.getEmail().sendMailClean(
request.getEmail().getDestinationEmail(),
request.getEmail().getSourceName(),
request.getEmail().getSourceEmail(),
"domsPlace Contact Enquiry",
htmlMessage,
textMessage
);
return {
ok: true,
data: true
};
} catch(e) {
console.error('Failed to send contact message');
console.error(e);
return {
ok: false,
data: ERRORS.sending
};
}
}
}

View File

@ -26,7 +26,8 @@ const
path = require('path'),
ConfigurationManager = require('./../configuration/ConfigurationManager'),
DatabaseConnection = require('./../database/DatabaseConnection'),
Server = require('./../server/Server')
Server = require('./../server/Server'),
Email = require('./../email/Email')
;
//Constants
@ -42,6 +43,7 @@ class App {
getPublicDirectory() { return PUBLIC_PATH; }
getServer() { return this.server; }
getAPI() { return this.getServer().getAPI(); }
getEmail() {return this.email;}
//Primary Functions
async start() {
@ -68,6 +70,17 @@ class App {
throw new Error(e);
}
//Connect to our SMTP Host (For sending mail)
try {
console.log('Connecting to SMTP Server');
this.email = new Email(this);
await this.email.connect();
console.log('...Done');
} catch(e) {
console.error("Failed to setup emails!");
throw new Error(e);
}
//Now we need to start the server. This provides both a nice interface, as
//well as our API Handler (including 2auth callback urls)
try {

129
private/email/Email.js Normal file
View File

@ -0,0 +1,129 @@
// Copyright (c) 2018 Dominic Masters
//
// MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
const nodemailer = require('nodemailer');
class Email {
constructor(app) {
this.app = app;
}
getApp() {return this.app;}
getConfig() {return this.getApp().getConfig();}
getTransporter() {return this.transport;}
getDestinationName() {return this.getConfig().getValueOf("smtp.destination.name");}
getDestinationEmail() {return this.getConfig().getValueOf("smtp.destination.email");}
getSourceName() {return this.getConfig().getValueOf("smtp.source.name");}
getSourceEmail() {return this.getConfig().getValueOf("smtp.source.email");}
connect() {
if(!this.getConfig().getValueOf("smtp")) throw new Error("Missing SMTP Config");
if(!this.getConfig().getValueOf("smtp.host")) throw new Error("Missing SMTP Host Config");
if(!this.getConfig().getValueOf("smtp.username")) throw new Error("Missing SMTP Username Config");
if(!this.getConfig().getValueOf("smtp.password")) throw new Error("Missing SMTP Password Config");
//We require some info about the person who handles the mailing.
if(!this.getConfig().getValueOf("smtp.destination")) throw new Error("Missing SMTP Destination Config");
if(!this.getConfig().getValueOf("smtp.destination.name")) throw new Error("Missing SMTP Destination Name Config");
if(!this.getConfig().getValueOf("smtp.destination.email")) throw new Error("Missing SMTP Destination Email Config");
if(!this.getConfig().getValueOf("smtp.source")) throw new Error("Missing SMTP Source Config");
if(!this.getConfig().getValueOf("smtp.source.name")) throw new Error("Missing SMTP Source Name Config");
if(!this.getConfig().getValueOf("smtp.source.email")) throw new Error("Missing SMTP Source Email Config");
let ssl = false;
let port = 587;
if(this.getConfig().getValueOf("smtp.ssl")) {
ssl = true
port = 465;
}
port = this.getConfig().getValueOf("smtp.port") || port;
this.transport = nodemailer.createTransport({
host: this.getConfig().getValueOf("smtp.host"),
port: port,
secure: ssl,
auth: {
user: this.getConfig().getValueOf("smtp.username"),
pass: this.getConfig().getValueOf("smtp.password")
}
});
}
async sendMail(options) {
let o = {};
o.options = options;
o.email = this;
o.resolver = function(resolve, reject) {
this.resolve = resolve;
this.reject = reject;
console.log('Sending email to ' + this.options.to + '...');
this.email.getTransporter().sendMail(this.options, this.onEmailCallback);
}.bind(o);
o.onEmailCallback = function(error, info) {
this.error = error;
this.info = info;
if(error) {
return this.reject(error);
}
console.log('Email sent to ' + this.options.to + '!');
this.resolve(info);
}.bind(o);
let x = new Promise(o.resolver);
return await x;
}
async sendMailClean(tos, fromName, fromEmail, subject, html, text) {
//Create options
let options = {};
//TODO: Properly escape these emails & names, at the moment we're only using
//emails we assume to be safe (those in the config)
//From
if(typeof fromName === "string" && fromName.length) {
options.from = '"' +fromName+ '" <'+fromEmail+'>';
} else {
options.from = fromEmail;
}
//To (and CC)
if(typeof tos === "string") tos = [tos];
options.to = tos.join(', ');
//Subject
options.subject = subject || "Untitled";
//HTML Formatted emails
if(typeof html === "string" && html.length) options.html = html;
if(typeof text === "string" && text.length) options.text = text;
return await this.sendMail(options);
}
}
module.exports = Email;

View File

@ -135,6 +135,7 @@ class Server {
getLandingFile() {return path.join(this.app.getPublicDirectory(), LANDING_FILE);}
getExpress() {return this.express;}
getAPI() {return this.api;}
getApp() {return this.app;}
isRunning() {
if(typeof this.http !== typeof undefined) {
@ -235,7 +236,11 @@ class Server {
}
onWatchChange(error, stats) {
if(error) console.log(error);
if(error || (stats.compilation.errors && stats.compilation.errors.length)) {
console.error(error || stats.compilation.errors);
} else {
console.log("Server compiled!");
}
}
onGetRequest(req, res) {

View File

@ -62,9 +62,11 @@ module.exports = function(server, app) {
output.module = {
rules: [
{
test: /\.jsx?$/,
test: /\.jsx?$|\.js?$/,
exclude: /node_modules/,
loaders: ['babel-loader']
use: {
loader: 'babel-loader'
}
},
{
test: /\.scss$|\.css$/i,

View File

@ -28,7 +28,8 @@ import Input, { Form, InputGroup, TextArea, Label, ButtonGroup } from './../../i
import Language from './../../language/Language';
import ElementScrollFader from './../../animation/fade/ElementScrollFader';
import ContentBox from './../../content/ContentBox';
import { Title, Paragraph } from './../../typography/Typography'
import { Title, Paragraph } from './../../typography/Typography';
import Forms from './../../../common/Forms';
import Section, {
BodySection,
ClearSection,
@ -71,6 +72,9 @@ class ContactPage extends React.Component {
<Input
type="text"
placeholder={ Language.get("pages.contact.name.placeholder") }
required={ Forms.contact.name.required }
maxLength={ Forms.contact.name.maxLength }
/>
</InputGroup>
@ -79,6 +83,8 @@ class ContactPage extends React.Component {
<Input
type="email"
placeholder={ Language.get("pages.contact.email.placeholder") }
required={ Forms.contact.email.required }
maxLength={ Forms.contact.email.maxLength }
/>
</InputGroup>
@ -88,6 +94,8 @@ class ContactPage extends React.Component {
placeholder={ Language.get("pages.contact.message.placeholder") }
rows="8"
className="p-contact-page__message"
required={ Forms.contact.message.required }
maxLength={ Forms.contact.message.maxLength }
/>
</InputGroup>