libre
Version:
World's lightest CMS
174 lines (142 loc) • 5.54 kB
JavaScript
import React from 'react';
import express from 'express';
import compression from 'compression';
import bodyParser from 'body-parser';
import path from 'path';
import HBS from 'express-handlebars';
import proxy from 'proxy-middleware';
import url from 'url';
import {renderToStaticMarkup, renderToString} from 'react-dom/server';
import Sheet from './Sheet';
import Libre from '../Libre';
import SEO from './SEO';
export default class Server {
constructor(app, routes, typeMap, plugins, libreConfig, server) {
this.hbs = 'index';
this.router = server || express();
this.routes = routes || {};
this.libreConfig = libreConfig;
this.app = app; //seo-friendly version of app component
Libre.setup({typeMap, plugins});
}
serve() {
const router = this.router;
//1. setup app
router.use(bodyParser.json());
router.use(bodyParser.urlencoded({ extended: true }));
var port = process.env.PORT || 8081;
//redirect http to https on production
router.get('*', (req,res,next) => {
if (req.headers['x-forwarded-proto'] != 'https' && process.env.NODE_ENV === 'production') {
res.redirect('https://'+req.hostname+req.url);
} else {
next() /* Continue to other routes if we're not redirecting */
}
});
//use gzip compression
router.use(compression());
//config handlebars
router.engine('hbs', HBS({
extname: '.hbs',
defaultLayout: 'index',
layoutsDir: './'
}));
router.set('views', path.resolve('./'));
router.set('view engine', 'hbs');
//handle static assets at /assets
//TODO: make these configurable!
router.use('/assets', express.static(path.resolve('./assets')));
router.use('/client.js', express.static(path.resolve('./client.js')));
router.use('/style.css', express.static(path.resolve('./style.css')));
//listen for all other routes to power app
router.get('*', (req, res) => {
const context = {};
let args = {};
//this enables individual app pages to emit their own meta tags
//if implemented, the call to renderToStaticMarkup() will cause this to populate args
//so it's aptly named, cause this is pretty fucking og.
const og = (meta) => args = Object.assign({}, args, meta);
const app = <SEO location={req.url} routes={this.routes} app={this.app} libre={Libre} og={og} />
const html = renderToStaticMarkup(app);
const urlBase = `${req.protocol}://${req.hostname}`;
args.app = html;
args.url = `${urlBase}${req.url}`;
//special case for images passed through og() with server-relative paths)
//we need to tack on the url base here to make sure it's exposed to OG as full url
if (args.image && !/https?\:\/\//.test(args.image)) {
if (args.image.startsWith('/')) args.image = `${urlBase}${args.image}`;
else args.image = `${urlBase}${req.url}/${args.image}`
}
res.render(this.hbs, args);
});
//listen and we're done!
router.listen(port, () => {
console.log('[LIBRE] Server listening on port', port);
});
}
//access the singleton instance of Libre from externally to make sure it's the same instance
get libre() {
return Libre;
}
load(creds, key, tabs, store, success) {
var loaded = 0;
const sheet = new Sheet(creds);
tabs.map(tab => {
sheet.loadSheet(key, tab, (content) => {
store[tab] = content;
loaded += 1;
if (loaded == tabs.length && typeof(success) == 'function') success(store);
});
});
}
run(existingServer) {
const store = {};
const {tabs, sheetKey, sheetCreds, writeKey, writeTab} = this.libreConfig;
const routes = this.routes;
const router = this.router || existingServer;
//TODO: figure out where a setup() function should go
//these need to be here in order to enable post handling to work properly
router.use(bodyParser.json());
router.use(bodyParser.urlencoded({ extended: true }));
//load all content in according to config
this.load(sheetCreds, sheetKey, tabs, store, (store) => {
//once content is loaded from spreadsheet, also load it into memory
//this is used for SEO rendering, so that all content is available at request
Libre.init(store);
//only launch server if existing server was not passed in
if (!existingServer) this.serve();
});
//listen for content requests from client
router.get('/libre/:path?', (req, res) => {
const {path} = req.params;
//if path is specified send just that object (one tab)
//(this may be undefined --> send null)
if (path) res.send(store[path] || null);
//otherwise send the whole store
else res.send(store);
});
//enable client to invoke content refresh on server
//TODO: figure out how to secure this
router.post('/libre/refresh', (req, res) => {
this.load(sheetCreds, sheetKey, tabs, store, (store) => {
Libre.init(store);
res.send(store);
});
});
//if writeKey and writeTab are enabled, also activate listener for writes
if (writeKey && writeTab) {
router.post('/libre/push', (req, res) => {
const {tab, row} = req.body;
if (tab && row) {
const sheet = new Sheet(sheetCreds);
sheet.addRow(writeKey, tab, row, () => {
res.send({success: true});
});
}
else {
res.send({success: false});
}
});
}
}
}