diff --git a/initialisers/http.js b/initialisers/http.js index d377929..d26d731 100644 --- a/initialisers/http.js +++ b/initialisers/http.js @@ -2,26 +2,35 @@ if (!process.env.PORT) require('dotenv').config() const { createServer } = require('http') -const serveStaticFile = require('../lib/responder') +const { serveStaticFile, serveRoute } = require('../lib/responder') const errors = require('../config/errors') const { PORT, APP_NAME } = process.env module.exports = () => { - const server = createServer(async ({ url }, response) => { - const urlTokens = url.split('.') - const extension = urlTokens.length > 1 ? `${urlTokens[urlTokens.length - 1].toLowerCase().trim()}` : false - const isRoot = [ '', '/' ].indexOf(url) > -1 - const path = isRoot ? '/index.html' : url + const server = createServer(async (request, response) => { + const urlTokens = request.url.split('.') + const extension = urlTokens.length > 1 ? urlTokens[urlTokens.length - 1].toLowerCase().trim() : false + const serveResponse = extension ? serveStaticFile : serveRoute + const responseParams = { path: request.url } + + if (extension) { + responseParams.extension = extension + } else { + responseParams.request = request + responseParams.context = { + app_name: APP_NAME + } + } try { - return await serveStaticFile({ file: path, extension: isRoot ? 'html' : extension }, response) + return await serveResponse(responseParams, response) } catch (error) { console.error(error) const errorData = errors(error) return await serveStaticFile({ - file: '/error.html', + path: '/error.html', extension: 'html', statusCode: errorData.code }, response) diff --git a/lib/formdata.js b/lib/formdata.js new file mode 100644 index 0000000..3875af8 --- /dev/null +++ b/lib/formdata.js @@ -0,0 +1,15 @@ +const { parse: parseFormadata } = require('querystring') + +const getRequestBody = request => new Promise((resolve, reject) => { + let formData = '' + + request.on('data', buffer => formData += buffer.toString()) + request.on('error', reject) + + request.on('end', () => { + const parsedData = parseFormadata(formData) + return resolve(parsedData) + }) +}) + +module.exports = getRequestBody diff --git a/lib/responder.js b/lib/responder.js index 9736b43..381938f 100644 --- a/lib/responder.js +++ b/lib/responder.js @@ -1,15 +1,19 @@ -const { open } = require('fs').promises +const { readFileSync, promises: { open } } = require('fs') const { lookup } = require('mime-types') +const Handlebars = require('handlebars') const { STATIC_EXTENSIONS } = require('../config/constants') -const serveStaticFile = async ({ file, extension, statusCode }, response) => { +const routes = require('../routes') +const basePage = readFileSync(`./templates/template.hbs`, { encoding: 'utf8' }) + +const serveStaticFile = async ({ path, extension, statusCode }, response) => { if (STATIC_EXTENSIONS.indexOf(extension) === -1) throw new Error('not_found') let fileHandle try { - fileHandle = await open(`./public/${file}`, 'r') + fileHandle = await open(`./public/${path}`, 'r') const staticFile = await fileHandle.readFile() const mime = lookup(extension) @@ -28,4 +32,19 @@ const serveStaticFile = async ({ file, extension, statusCode }, response) => { } } -module.exports = serveStaticFile +const serveRoute = async ({ request, context }, response) => { + const key = `${request.method}:${request.url}` + const route = routes[key] + + if (!route) throw new Error('not_found') + + Handlebars.registerPartial('content', route.body) + const hbs = Handlebars.compile(basePage) + + let routeContext = {} + if (route.data) routeContext = await route.data(request) + + return response.end(hbs({ ...context, ...routeContext })) +} + +module.exports = { serveStaticFile, serveRoute } diff --git a/package-lock.json b/package-lock.json index 0dd75f7..980c6f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -100,8 +100,7 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base": { "version": "0.11.2", @@ -183,7 +182,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -338,6 +336,12 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "optional": true + }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -347,8 +351,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "configstore": { "version": "3.1.2", @@ -682,6 +685,11 @@ "map-cache": "^0.2.2" } }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, "fsevents": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.8.tgz", @@ -1242,6 +1250,19 @@ "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", "dev": true }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, "glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", @@ -1297,6 +1318,24 @@ "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", "dev": true }, + "handlebars": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", + "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", + "requires": { + "neo-async": "^2.6.0", + "optimist": "^0.6.1", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -1353,11 +1392,19 @@ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { "version": "1.3.5", @@ -1670,7 +1717,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -1734,6 +1780,11 @@ "to-regex": "^3.0.1" } }, + "neo-async": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.0.tgz", + "integrity": "sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==" + }, "nodemon": { "version": "1.18.11", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.18.11.tgz", @@ -1825,6 +1876,30 @@ "isobject": "^3.0.1" } }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" + } + } + }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -1858,8 +1933,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-is-inside": { "version": "1.0.2", @@ -2366,6 +2440,24 @@ "nopt": "~1.0.10" } }, + "uglify-js": { + "version": "3.5.9", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.9.tgz", + "integrity": "sha512-WpT0RqsDtAWPNJK955DEnb6xjymR8Fn0OlK4TT4pS0ASYsVPqr5ELhgwOwLCP5J5vHeJ4xmMmz3DEgdqC10JeQ==", + "optional": true, + "requires": { + "commander": "~2.20.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + } + } + }, "undefsafe": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.2.tgz", @@ -2545,6 +2637,16 @@ "string-width": "^2.1.1" } }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, "write-file-atomic": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.2.tgz", diff --git a/package.json b/package.json index 4ee6923..206015b 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,8 @@ "description": "Schedule a reminder for yourself, by email", "main": "app.js", "dependencies": { + "glob": "^7.1.3", + "handlebars": "^4.1.2", "mime-types": "^2.1.24" }, "devDependencies": { @@ -11,7 +13,7 @@ "nodemon": "^1.18.11" }, "scripts": { - "start": "nodemon --inspect -e js,html" + "start": "nodemon --inspect -e js,html,hbs" }, "repository": { "type": "git", @@ -22,5 +24,8 @@ "bugs": { "url": "https://github.com/mtimofiiv/remind.ist/issues" }, - "homepage": "https://github.com/mtimofiiv/remind.ist#readme" + "homepage": "https://github.com/mtimofiiv/remind.ist#readme", + "engines": { + "node": ">=12" + } } diff --git a/routes/index.js b/routes/index.js new file mode 100644 index 0000000..946438e --- /dev/null +++ b/routes/index.js @@ -0,0 +1,17 @@ +const { readFileSync } = require('fs') +const glob = require('glob') + +module.exports = glob.sync( + './routes/**/*.js', + { ignore: [ './routes/index.js' ] } +).reduce((routeMap, filename) => { + const route = require(`.${filename}`) + + if (route.template) { + route.body = readFileSync(`./templates/${route.template}.hbs`, { encoding: 'utf8' }) + } + + routeMap[`${route.method || 'GET'}:${route.uri}`] = route + + return routeMap +}, {}) diff --git a/routes/new-reminder.js b/routes/new-reminder.js new file mode 100644 index 0000000..c9e8f92 --- /dev/null +++ b/routes/new-reminder.js @@ -0,0 +1,10 @@ +const getRequestBody = require('../lib/formdata') + +exports.uri = '/new' +exports.template = 'new_reminder' +exports.method = 'POST' + +exports.data = async request => { + const formData = await getRequestBody(request) + return formData +} diff --git a/routes/root.js b/routes/root.js new file mode 100644 index 0000000..cd9812d --- /dev/null +++ b/routes/root.js @@ -0,0 +1,2 @@ +exports.uri = '/' +exports.template = 'root' diff --git a/templates/new_reminder.hbs b/templates/new_reminder.hbs new file mode 100644 index 0000000..b604228 --- /dev/null +++ b/templates/new_reminder.hbs @@ -0,0 +1,7 @@ +

Data received!

+ +

Email: {{email}}

+ +

Message:

+ +

{{message}}

diff --git a/templates/root.hbs b/templates/root.hbs new file mode 100644 index 0000000..d4162d3 --- /dev/null +++ b/templates/root.hbs @@ -0,0 +1,20 @@ +
+ +

+ {{app_name}} is here +

+ +
+
+
+ +
+ +
+ +
+ +
+ +
diff --git a/public/index.html b/templates/template.hbs similarity index 81% rename from public/index.html rename to templates/template.hbs index 5da35e7..03126d3 100644 --- a/public/index.html +++ b/templates/template.hbs @@ -3,7 +3,7 @@ - remind.ist + {{app_name}} @@ -32,10 +32,9 @@ -
+ {{>content}} -

- remind.ist is here -

+ +