first drop
This commit is contained in:
parent
b27fcef9e7
commit
c3376522eb
13 changed files with 383 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
node_modules
|
||||||
|
package.json
|
||||||
|
package-lock.json
|
||||||
|
assets/js/validate.min.js
|
||||||
37
README.md
37
README.md
|
|
@ -1,2 +1,39 @@
|
||||||
# ghost-contact-form
|
# ghost-contact-form
|
||||||
Contact Forms in Ghost — Without External Services
|
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
|
||||||
|
```
|
||||||
|
|
|
||||||
53
assets/css/demo.css
Normal file
53
assets/css/demo.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
56
assets/css/form.css
Normal file
56
assets/css/form.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
89
assets/js/formProcessor.js
Normal file
89
assets/js/formProcessor.js
Normal file
|
|
@ -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 = "<br><p><em>" + text + "</em></p>";
|
||||||
|
};
|
||||||
|
function responseAlert(text) {
|
||||||
|
document.getElementById("serverresponse").innerHTML = "<br><p><em>" + text + "</em></p>";
|
||||||
|
};
|
||||||
|
|
||||||
|
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]);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}());
|
||||||
35
demo.html
Normal file
35
demo.html
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
|
||||||
|
<link type="text/css" rel="stylesheet" href="/v1/assets/css/demo.css">
|
||||||
|
<link type="text/css" rel="stylesheet" href="/v1/assets/css/form.css">
|
||||||
|
<script src="/v1/assets/js/validate.min.js" type="text/javascript"></script>
|
||||||
|
<script src="/v1/assets/js/formProcessor.js" type="text/javascript"></script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container header">
|
||||||
|
<h1>ghost-contact-form <small>demo</small></h1>
|
||||||
|
|
||||||
|
<form id="contact-form" method="post" data-format="inline">
|
||||||
|
<input type="text" id="name" placeholder="Name" pattern=".{2,30}" required>
|
||||||
|
<input type="email" id="email" placeholder="Your email address" required>
|
||||||
|
<select id="subject" pattern=".{1,20}" required>
|
||||||
|
<option value="" disabled selected>Pick one subject...</option>
|
||||||
|
<option value="comment">I want to leave a comment</option>
|
||||||
|
<option value="contribution">I want to contribute</option>
|
||||||
|
</select>
|
||||||
|
<textarea rows="5" id="message" placeholder="Message" pattern=".{10,4000}" required></textarea>
|
||||||
|
<input type="text" name="_norobots" style="display:none !important;">
|
||||||
|
<button class="btn" type="submit" id="submit" value="Send" onclick="formProcessor.process('/v1/contact');return false;">Send</button>
|
||||||
|
<span id="responsemsg"></span>
|
||||||
|
<span id="serverresponse" style="color: red;"></span>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="container footer">Generated by <a href="https://github.com/styxlab/ghost-contact-form">styxlab/ghost-contact-form</a></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2
ghost-admin/code-injection-page-footer.html
Normal file
2
ghost-admin/code-injection-page-footer.html
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
<script src="https://api.your-blog.com/v1/assets/js/validate.min.js" type="text/javascript"></script>
|
||||||
|
<script src="https://api.your-blog.com/v1/assets/js/formProcessor.js" type="text/javascript"></script>
|
||||||
1
ghost-admin/code-injection-page-header.html
Normal file
1
ghost-admin/code-injection-page-header.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<link type="text/css" rel="stylesheet" href="https://api.your-blog.com/v1/assets/css/form.css">
|
||||||
13
ghost-admin/contact-form.html
Normal file
13
ghost-admin/contact-form.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<form id="contact-form" method="post" data-format="inline">
|
||||||
|
<input type="text" id="name" placeholder="Name" pattern=".{2,30}" required>
|
||||||
|
<input type="email" id="email" placeholder="Your email address" required>
|
||||||
|
<select id="subject" pattern=".{1,20}" required>
|
||||||
|
<option value="" disabled selected>Pick one subject...</option>
|
||||||
|
<option value="comment">I want to leave a comment</option>
|
||||||
|
<option value="contribution">I want to contribute</option>
|
||||||
|
</select>
|
||||||
|
<textarea rows="5" id="message" placeholder="Message" pattern=".{10,4000}" required></textarea>
|
||||||
|
<input type="text" name="_norobots" style="display:none !important;">
|
||||||
|
<button class="btn" type="submit" id="submit" value="Send" onclick="formProcessor.process();return false;">Send</button>
|
||||||
|
<span id="responsemsg"></span>
|
||||||
|
</form>
|
||||||
58
ghost-contact-svc.js
Normal file
58
ghost-contact-svc.js
Normal file
|
|
@ -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 = `
|
||||||
|
<p>Hello,<p>
|
||||||
|
<p>You got a new contact request.</p>
|
||||||
|
<h3>Contact Details</h3>
|
||||||
|
<ul><li>Name: ${data.name}</li><li>Email: ${data.email}</li></ul>
|
||||||
|
<h3>Message:</h3>
|
||||||
|
<p>${data.message}</p>
|
||||||
|
`
|
||||||
|
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"});
|
||||||
|
});
|
||||||
|
};
|
||||||
14
ghost-contact.service
Normal file
14
ghost-contact.service
Normal file
|
|
@ -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
|
||||||
13
install.sh
Executable file
13
install.sh
Executable file
|
|
@ -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
|
||||||
8
test.sh
Executable file
8
test.sh
Executable file
|
|
@ -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!"}'
|
||||||
|
|
||||||
Loading…
Reference in a new issue