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
|
||||
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