From c3376522ebcd36f46d92a6dd2bb032fb9cd27a01 Mon Sep 17 00:00:00 2001 From: cws Date: Sat, 1 Feb 2020 18:40:02 +0100 Subject: [PATCH] first drop --- .gitignore | 4 + README.md | 37 +++++++++ assets/css/demo.css | 53 ++++++++++++ assets/css/form.css | 56 +++++++++++++ assets/js/formProcessor.js | 89 +++++++++++++++++++++ demo.html | 35 ++++++++ ghost-admin/code-injection-page-footer.html | 2 + ghost-admin/code-injection-page-header.html | 1 + ghost-admin/contact-form.html | 13 +++ ghost-contact-svc.js | 58 ++++++++++++++ ghost-contact.service | 14 ++++ install.sh | 13 +++ test.sh | 8 ++ 13 files changed, 383 insertions(+) create mode 100644 .gitignore create mode 100644 assets/css/demo.css create mode 100644 assets/css/form.css create mode 100644 assets/js/formProcessor.js create mode 100644 demo.html create mode 100644 ghost-admin/code-injection-page-footer.html create mode 100644 ghost-admin/code-injection-page-header.html create mode 100644 ghost-admin/contact-form.html create mode 100644 ghost-contact-svc.js create mode 100644 ghost-contact.service create mode 100755 install.sh create mode 100755 test.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..04e5764 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +package.json +package-lock.json +assets/js/validate.min.js diff --git a/README.md b/README.md index 5a92214..aca0867 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,39 @@ # ghost-contact-form Contact Forms in Ghost — Without External Services + +## Prerequisites +- A supported version of Node.js +- npm to manage packages + +## Quickstart Install + +``` +$ git clone https://github.com/styxlab/ghost-contact-form.git +$ cd ghost-contact-form +$ sh install.sh +``` + +## Configure + +Adapt the `.env` file to your needs. The following variables must be defined. + +``` +SMTP_HOST = mail.server.com +SMTP_USER = user@server.com +SMTP_PASS = strong password +ALLOW_ORIGIN = https://your-blog.com +EMAIL_FROM = noreply@your-blog.com +EMAIL_TO = your@email.com +``` + +## Usage + +``` +$ node ghost-contact-svc.js +``` + +## Test Locally + +``` +$ http://lcoalhost:7000/v1/demo +``` diff --git a/assets/css/demo.css b/assets/css/demo.css new file mode 100644 index 0000000..a5d9905 --- /dev/null +++ b/assets/css/demo.css @@ -0,0 +1,53 @@ +body { + margin: 0px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 20px; + color: #333; + background-color: #fff; +} +h1 { + margin: 10px 0; + font-family: inherit; + font-weight: bold; + line-height: 20px; + color: inherit; + text-rendering: optimizelegibility; +} +h1 small { + font-weight: normal; + line-height: 1; + color: #999; +} +h1 { + line-height: 40px; +} +h1 { + font-size: 38.5px; +} +h1 small { + font-size: 24.5px; +} +body { + margin-top: 90px; +} +.header { + position: fixed; + top: 0; + left: 50%; + margin-left: -480px; + background-color: #fff; + border-bottom: 1px solid #ddd; + padding-top: 10px; + z-index: 10; +} +.footer { + color: #ddd; + font-size: 12px; + text-align: center; + margin-top: 20px; +} +.footer a { + color: #ccc; + text-decoration: underline; +} \ No newline at end of file diff --git a/assets/css/form.css b/assets/css/form.css new file mode 100644 index 0000000..3d3b30c --- /dev/null +++ b/assets/css/form.css @@ -0,0 +1,56 @@ +form,input,select,textarea,button { + font-family: avenir next,avenir,helvetica neue,helvetica,ubuntu,roboto,noto,segoe ui,arial,sans-serif; + display: block; + margin: 0 0 2rem 0; + padding: 1rem 0; + width: 100% !important; + align-items: flex-start; + box-sizing: border-box; +} +input,select,textarea { + color: darkslategray; + background-color: white; + background-clip: padding-box; + line-height: 1.5; + border-radius: .5rem; + padding: 1rem 4rem; + border: 1px solid #ced4da; + transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out; +} +select { + appearance: none; + -moz-appearance: none; + -webkit-appearance: none; +} +select:required:invalid { + color: silver; +} +input:focus, select:focus, textarea:focus { + border-color: royalblue; + box-shadow: none; + -webkit-box-shadow: none; +} +button { + margin-bottom: 0; + color: white; + background-color: royalblue; + border-color: royalblue; + user-select: none; + line-height: 1.5; + border-radius: .5rem; + border: 1px solid transparent; + transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out; +} +button:hover { + filter: brightness(80%); +} +input:placeholder { + color: silver; +} +option[value=""][disabled] { + display: none; + color: silver; +} +option { + color: black; +} \ No newline at end of file diff --git a/assets/js/formProcessor.js b/assets/js/formProcessor.js new file mode 100644 index 0000000..a368a00 --- /dev/null +++ b/assets/js/formProcessor.js @@ -0,0 +1,89 @@ +var formProcessor = (function(){ + + "use strict"; + + var constraints = { + name: { + presence: true, + length: { + minimum: 2, + maximum: 30, + message: "must be longer." + } + }, + email: { + presence: true, + email: true, + }, + subject: { + presence: true, + length: { + minimum: 1, + message: "must be selected." + } + }, + message: { + presence: true, + length: { + minimum: 10, + maxumum: 4000, + message: "must be longer." + } + }, + robot: { + presence: { + allowEmpty: true + }, + length: { + is: 0, + message: "must be filled out." + } + } + }; + + function formAlert(text) { + document.getElementById("responsemsg").innerHTML = "

" + text + "

"; + }; + function responseAlert(text) { + document.getElementById("serverresponse").innerHTML = "

" + text + "

"; + }; + + function sendData(data, url) { + formAlert("One second..."); + var postURL = (url || "https://api.your-blog.com/v1/contact"); + var http = new XMLHttpRequest(); + http.open("POST", postURL, true); + http.setRequestHeader("Content-Type", "application/json"); + data.source_url = window.location.href; + http.send(JSON.stringify(data)); + http.onload = function() { + formAlert("Thank you, your message has been sent!"); + if(url != undefined){ + responseAlert("Only the demo should display the server response: " + http.responseText); + } + document.getElementById("contact-form").reset(); + } + }; + + return ({ + process: function(url) { + var attributes = { + name: document.forms["contact-form"]["name"].value, + email: document.forms["contact-form"]["email"].value, + subject: document.forms["contact-form"]["subject"].value, + message: document.forms["contact-form"]["message"].value, + robot: document.forms["contact-form"]["_norobots"].value + }; + validate.async(attributes, constraints) + .then(function(success) { + //console.log("Success", success); + sendData(success, url); + }) + .catch(function(error) { + //console.log("ValidationError", error); + formAlert(Object.values(error)[0][0]); + }) + } + }); + +}()); diff --git a/demo.html b/demo.html new file mode 100644 index 0000000..1f52b98 --- /dev/null +++ b/demo.html @@ -0,0 +1,35 @@ + + + + + + + + + + + + + +
+

ghost-contact-form demo

+ +
+ + + + + + + + +
+ + +
+ + \ No newline at end of file diff --git a/ghost-admin/code-injection-page-footer.html b/ghost-admin/code-injection-page-footer.html new file mode 100644 index 0000000..02eb3a6 --- /dev/null +++ b/ghost-admin/code-injection-page-footer.html @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/ghost-admin/code-injection-page-header.html b/ghost-admin/code-injection-page-header.html new file mode 100644 index 0000000..72745f1 --- /dev/null +++ b/ghost-admin/code-injection-page-header.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ghost-admin/contact-form.html b/ghost-admin/contact-form.html new file mode 100644 index 0000000..cd68f8c --- /dev/null +++ b/ghost-admin/contact-form.html @@ -0,0 +1,13 @@ +
+ + + + + + + +
\ No newline at end of file diff --git a/ghost-contact-svc.js b/ghost-contact-svc.js new file mode 100644 index 0000000..3f50dc2 --- /dev/null +++ b/ghost-contact-svc.js @@ -0,0 +1,58 @@ +'use strict'; +var express = require('express'); +var cors = require('cors'); +var bodyParser = require("body-parser"); +var dotenv = require('dotenv').config(); +var nodemailer = require('nodemailer'); +var smtpTrans = require('nodemailer-smtp-transport'); +var validator = require("email-validator"); +var sanitize = require('sanitize-html'); + +var smtp = { "auth": {}, "port": 465, "secure": true, "tls": {"rejectUnauthorized": false}, "debug": false}; +smtp.host = process.env.SMTP_HOST; +smtp.auth.user = process.env.SMTP_USER; +smtp.auth.pass = process.env.SMTP_PASS; +var transporter = nodemailer.createTransport(smtpTrans(smtp)); + +var app = express(); +app.disable('x-powered-by'); +app.use(bodyParser.urlencoded({limit: '1mb', extended: false})); +app.use(bodyParser.json({limit: '1mb'})); +app.use(cors({origin: process.env.ALLOW_ORIGIN, + allowedHeaders: ['Content-Type', 'application/json; charset=utf-8', 'text/html; charset=utf-8']})); + +app.use('/v1/assets', express.static(__dirname + '/assets')); + +app.post('/v1/contact', function(req, res) { + if(validator.validate(req.body.email)) return sendEmail(req.body, res); + res.status(403).json({"validation": "no email"}); +}); + +app.get('/v1/demo', function(req, res) { + res.sendFile(__dirname + '/demo.html'); +}); + +app.listen(process.env.PORT || 7000, function(){ + console.log('Listening on http://localhost:' + (process.env.PORT || 7000)); +}); + +function sendEmail(data, res){ + var email = { "from": process.env.EMAIL_FROM, "to": process.env.EMAIL_TO}; + email.subject = 'MY BLOG - ' + (data.subject && data.subject.toUpperCase()); + + const output = ` +

Hello,

+

You got a new contact request.

+

Contact Details

+ +

Message:

+

${data.message}

+ ` + email.html = sanitize(output, { + allowedTags: sanitize.defaults.allowedTags.concat([ 'img' ]) + }); + transporter.sendMail(email, function(error, info){ + if(error) return res.json({"sendEmail": "failed"}); + res.json({"sendEmail": "ok"}); + }); +}; \ No newline at end of file diff --git a/ghost-contact.service b/ghost-contact.service new file mode 100644 index 0000000..90518be --- /dev/null +++ b/ghost-contact.service @@ -0,0 +1,14 @@ +[Unit] +Description=ghost-contact-svc microservice +After=network.target +StartLimitIntervalSec=0 + +[Service] +Type=simple +Restart=always +RestartSec=1 +User=joe +ExecStart=/usr/bin/env node /home/joe/ghost-contact-form/ghost-contact-svc.js + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..8e17a3c --- /dev/null +++ b/install.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +sudo npm -g install depcheck +npm -y init +deps=$(depcheck |cut -d: -f1 |tr -d '* ' | tail -n +2) + +for package in ${deps[@]}; do + echo 'Installing package: ' ${package} + npm install ${package} --save +done + +npm install validate.js --save +cp node_modules/validate.js/validate.min.js assets/js \ No newline at end of file diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..0281b59 --- /dev/null +++ b/test.sh @@ -0,0 +1,8 @@ +#!/bin/bash + + +contact="https://api.your-server.com/v1/contact" + +curl -v -X POST ${contact} -H "Content-Type: application/json" \ + -d '{"email": "test@example.com", "name": "John Izzo", "subject": "feedback", "message": "Production Light!"}' + \ No newline at end of file