diff --git a/lib/config.mjs b/lib/config.mjs index 9bcf84a..230c9e9 100644 --- a/lib/config.mjs +++ b/lib/config.mjs @@ -4,7 +4,15 @@ import fs from 'fs' const ROOT = process.cwd() const configPath = path.join(ROOT, 'turbo-serv.json') -const configDefault = {static: {dir: 'public'}} +const configDefault = { static: { dir: 'public' } } +const sessionDefault = { + store: 'memory', + options: { + host: '0.0.0.0', + port: 0, + expire: 120 + } +} let config try { @@ -13,6 +21,7 @@ try { } catch (e) { console.log('Parser Error in config file') config = configDefault + config.session = sessionDefault } config.ROOT = ROOT diff --git a/lib/handlers/session.mjs b/lib/handlers/session.mjs index 6aa1674..5f638b5 100644 --- a/lib/handlers/session.mjs +++ b/lib/handlers/session.mjs @@ -1,22 +1,12 @@ import uid from 'uid-safe' import signature from 'cookie-signature' -import MemoryStore from '../stores/memory' -// import RedisStore from '../stores/redis' -// import MongoStore from '../stores/mongo' +import { DataBase, dbOptions } from './../../lib/stores/store' -let options = { - // host: '127.0.0.2', - // port: 5432, - // expire: 120, // 2 min - // database: RedisStore - // database: MongoStore -} -let DB = options.database || MemoryStore -let sessionStoreDB = new DB() -let sessionStore = sessionStoreDB.init(options) +let sessionStoreDB = new DataBase() +let sessionStore = sessionStoreDB.init(dbOptions) class Session { - async init (req, res) { + async init(req, res) { let data const SECRET = 'session' if (req.cookies && req.cookies.sess_id) { @@ -34,33 +24,33 @@ class Session { this.sess_id = data.id this.data = data res.setCookie('sess_id', signature.sign(this.sess_id, SECRET), { - maxAge: options.expire || 60 * 60 * 24 * 7 // 1 week + maxAge: dbOptions.expire }) } - async set (key, value) { + async set(key, value) { let data = sessionStore.get(this.sess_id) if (data instanceof Promise) { data = await data } if (!data) { - data = {sess_id: this.sess_id} + data = { sess_id: this.sess_id } } data[key] = value await sessionStore.set(this.sess_id, data) } - get (key) { + get(key) { let result = sessionStore.get(this.sess_id) return result } - delete () { + delete() { sessionStore.delete(this.sess_id) } } -export default async function () { +export default async function() { if (sessionStore instanceof Promise) { sessionStore = await sessionStore } diff --git a/lib/router.mjs b/lib/router.mjs index a8eafd7..98d4481 100644 --- a/lib/router.mjs +++ b/lib/router.mjs @@ -1,20 +1,52 @@ - export default class Router { - constructor (path = '/') { - this.routes = {GET: {}, POST: {}} + constructor(path = '/') { + this.routes = { GET: {}, POST: {} } this.path = path this.isRouter = true + this.routers = [] } - get (path, fn) { + get(path, fn) { this.routes.GET[path] = fn } - post (path, fn) { + post(path, fn) { this.routes.POST[path] = fn } - getHandler (method, path) { - return this.routes[method][path] + addRouter(router) { + this.routers.push(router) + } + + getHandler(method, path) { + const handler = this.routes[method][path] + return typeof handler == 'function' + ? handler + : this.findRouter(method, path, this.routers) + } + + findRouter(method, path, routers) { + if (!Array.isArray(routers)) routers = [routers] + for (const router of routers) { + if (path.startsWith(router.path)) { + path = path.slice(router.path.length) + const deepRouter = this.hasDeepRouter( + router, + path + .slice(1) + .split('/') + .map(token => `/${token}`) + .shift() + ) + return deepRouter + ? this.findRouter(method, path, deepRouter) + : router.routes[method][path] + } + } + } + + hasDeepRouter(router, pathToken) { + const route = router.routers.filter(route => route.path === pathToken) + return route ? route[0] : null } } diff --git a/lib/server.mjs b/lib/server.mjs index 04d7672..5b9406c 100644 --- a/lib/server.mjs +++ b/lib/server.mjs @@ -8,55 +8,50 @@ import finalHandler from './handlers/final' import Router from './router' import sessionHandler from './handlers/session' -const HANDLERS = [ - staticHandler, - cookieParser, - sessionHandler, - bodyParser -] +const HANDLERS = [staticHandler, cookieParser, sessionHandler, bodyParser] export default class App { - constructor () { - this.server = turbo.createServer(function (req, res) { + constructor() { + this.server = turbo.createServer(function(req, res) { Object.assign(req, Request) Object.assign(res, Response) req.res = res req._handlers = HANDLERS res.req = req }) - this.addRouter(new Router()) + this.addRoute(new Router()) } - - addRouter (router) { + addRoute(router) { if (!this.router) { this.router = router - HANDLERS.push(function () { + HANDLERS.push(function() { const handler = router.getHandler(this.method, this.url) if (handler) { return this.callHandlers(handler) } return null - }, - finalHandler) + }, finalHandler) } } - getRouter () { + getRouter() { return this.router } - listen (port) { + listen(port) { this.PORT = process.env.PORT || port || 5000 this.server.listen(this.PORT) console.log('Server running on PORT ' + this.PORT) } - close () { + close() { this.server.close(_ => { console.log('Shutting down App!') process.exit() }) } - static get Router () { return Router } + static get Router() { + return Router + } } diff --git a/lib/stores/memory.mjs b/lib/stores/memory.mjs index f02d57f..58b9856 100644 --- a/lib/stores/memory.mjs +++ b/lib/stores/memory.mjs @@ -1,15 +1,20 @@ export default class MemoryStore { - init () { + init(options) { this.store = {} + this.expiryTime = options.expire return this } - set (key, value) { + set(key, value) { this.store[key] = value + + setTimeout(() => { + this.delete(key) + }, this.expiryTime * 1000) } - get (key) { + get(key) { return this.store[key] } - delete (key) { + delete(key) { delete this.store[key] } } diff --git a/lib/stores/mongo.mjs b/lib/stores/mongo.mjs index 38cb07f..7f949fe 100644 --- a/lib/stores/mongo.mjs +++ b/lib/stores/mongo.mjs @@ -1,30 +1,39 @@ import mongo from 'mongodb' export default class mongoStore { - async init (options) { - let url = options.url || 'mongodb://localhost:27017/' + async init(options) { + let url = `mongodb://${options.host}:${options.port}/` this.client = mongo.MongoClient let db = await this.client.connect(url) - console.log('mongo db connected...') + console.log('Connected to mongo...') this.store = db.db('session-data') + this.store + .collection('sessions') + .createIndex({ createdAt: -1 }, { expireAfterSeconds: options.expire }) return this } - set (hash, value) { + set(hash, value) { let data = {} data['sess_id'] = hash data['data'] = value let obj = {} obj['sess_id'] = hash - this.store.collection('sessions').replaceOne(obj, { $set: data }, { upsert: true }, (err, res) => { - if (err) { - console.log(err) - } - }) + obj['createdAt'] = new Date() + this.store + .collection('sessions') + .replaceOne(obj, { $set: data }, { upsert: true }, (err, res) => { + if (err) { + console.log(err) + } + }) } - async get (key) { - let result = await this.store.collection('sessions').find({ sess_id: key }).toArray() + async get(key) { + let result = await this.store + .collection('sessions') + .find({ sess_id: key }) + .toArray() if (result.length >= 1) { result = result[0] if (result) { @@ -34,7 +43,7 @@ export default class mongoStore { return null } - delete (key) { + delete(key) { this.store.collection('sessions').remove({ sess_id: key }) } } diff --git a/lib/stores/store.mjs b/lib/stores/store.mjs new file mode 100644 index 0000000..7dfe02d --- /dev/null +++ b/lib/stores/store.mjs @@ -0,0 +1,19 @@ +import MemoryStore from './memory' +import RedisStore from './redis' +import MongoStore from './mongo' + +import config from './../config' + +// Available storage types: In-Memory, Redis, Mongo + +let storeConfig = { + memory: MemoryStore, + redis: RedisStore, + mongo: MongoStore +} +// Set prefered DB by setting dbConfig['mongo' | 'redis' | memory] + +const DataBase = storeConfig[config.session.store] +const dbOptions = config.session.options + +export { DataBase, dbOptions } diff --git a/package.json b/package.json index 495ce87..64e5df0 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,10 @@ "cookie-signature": "^1.1.0", "formidable": "^1.2.1", "mime": "^2.2.0", + "mongodb": "^3.0.6", "node-fetch": "^2.0.0", "querystring": "^0.2.0", + "redis": "^2.8.0", "turbo-http": "^0.3.0", "uid-safe": "^2.1.5" }, diff --git a/tests/test.mjs b/tests/test.mjs index ebdf980..b109db5 100644 --- a/tests/test.mjs +++ b/tests/test.mjs @@ -3,32 +3,77 @@ import test from 'tape' import App from '../lib/server' import FormData from 'form-data' import signature from 'cookie-signature' - // Start App Server const app = new App() const router = app.getRouter() -router.post('/', function () { +const settingsRouter = new App.Router('/settings') +const adminRouter = new App.Router('/admin') +const layoutRouter = new App.Router('/layout') +const sideBarRouter = new App.Router('/sidebar') + +router.addRouter(settingsRouter) +router.addRouter(adminRouter) +adminRouter.addRouter(settingsRouter) +settingsRouter.addRouter(layoutRouter) +layoutRouter.addRouter(sideBarRouter) + +router.post('/', function() { this.res.send(this.body) }) -router.get('/session', function () { - this.res.send({sess_id: this.session.sess_id}) +router.get('/session', function() { + this.res.send({ sess_id: this.session.sess_id }) +}) + +settingsRouter.get('/', function() { + this.res.send(`From settings router root path`) +}) + +settingsRouter.get('/create', function() { + this.res.send('settings/create route') +}) + +settingsRouter.post('/config', function() { + this.res.send('settings/config route') +}) + +adminRouter.post('/createUser', function() { + this.res.send('admin/createUser route') +}) + +layoutRouter.post('/modify', function() { + this.res.send('settings/layout/modify route') +}) + +layoutRouter.get('/currentStyle', function() { + this.res.send('settings/layout/currentStyle route') +}) +sideBarRouter.get('/status', function() { + this.res.send('settings/layout/sidebar/status route') +}) + +sideBarRouter.post('/toggle', function() { + this.res.send('settings/layout/sidebar/toggle route') +}) + +router.get('/session', function() { + this.res.send({ sess_id: this.session.sess_id }) }) -router.get('/redirect', function () { +router.get('/redirect', function() { this.res.redirect('/wonderland') }) -router.post('/urlencoded', function () { +router.post('/urlencoded', function() { this.res.send(this.body) }) -router.post('/multipartform', function () { +router.post('/multipartform', function() { this.res.send(this.body) }) -router.get('/download', function () { +router.get('/download', function() { const file = './public/index.html' const filename = 'app.html' this.res.download(file, filename) @@ -36,7 +81,7 @@ router.get('/download', function () { app.listen() // process.env.PORT || 5000 -test('responds to requests', async (t) => { +test('responds to requests', async t => { t.plan(24) let res, data, cookie, error @@ -67,8 +112,8 @@ test('responds to requests', async (t) => { try { res = await fetch('http://127.0.0.1:5000', { method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({hello: 'world'}) + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ hello: 'world' }) }) data = await res.json() } catch (e) { @@ -76,7 +121,7 @@ test('responds to requests', async (t) => { } t.false(error) t.equal(res.status, 200) - t.deepEqual(data, {hello: 'world'}) + t.deepEqual(data, { hello: 'world' }) // Test Session @@ -86,7 +131,7 @@ test('responds to requests', async (t) => { cookie = res.headers.get('set-cookie') const [name, value] = cookie.split(';')[0].split('=') const val = signature.unsign(decodeURIComponent(value), 'session') - cookie = {[name]: val} + cookie = { [name]: val } } catch (e) { error = e } @@ -114,7 +159,7 @@ test('responds to requests', async (t) => { try { res = await fetch('http://127.0.0.1:5000/urlencoded', { method: 'POST', - headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'input1=hello&input2=world&input3=are+you%3F' }) data = await res.json() @@ -123,7 +168,7 @@ test('responds to requests', async (t) => { } t.false(error) t.equal(res.status, 200) - t.deepEqual(data, {'input1': 'hello', 'input2': 'world', 'input3': 'are you?'}) + t.deepEqual(data, { input1: 'hello', input2: 'world', input3: 'are you?' }) // Test multipart form-data @@ -142,7 +187,11 @@ test('responds to requests', async (t) => { } t.false(error) t.equal(res.status, 200) - t.deepEqual(data.fields, {'input1': 'hello', 'input2': 'world', 'input3': 'are you?'}) + t.deepEqual(data.fields, { + input1: 'hello', + input2: 'world', + input3: 'are you?' + }) // Test res.download