Fleshed out API, added simple email support.
This commit is contained in:
26
common/Email.js
Normal file
26
common/Email.js
Normal 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
42
common/Forms.js
Normal 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
|
||||
}
|
||||
}
|
||||
};
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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);}
|
||||
|
||||
|
@ -38,6 +38,7 @@ class APIHandler {
|
||||
this.paths = paths;
|
||||
}
|
||||
|
||||
getAPI() {return this.api;}
|
||||
getMethods() {return this.methods;}
|
||||
getPaths() {return this.paths;}
|
||||
|
||||
|
@ -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" };
|
||||
}
|
||||
}
|
||||
|
||||
|
101
private/api/methods/contact/send.js
Normal file
101
private/api/methods/contact/send.js
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -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
129
private/email/Email.js
Normal 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;
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
||||
|
Reference in New Issue
Block a user