diff --git a/base/.npmrc b/base/.npmrc new file mode 100644 index 0000000..9cf9495 --- /dev/null +++ b/base/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/base/article/routes.mjs b/base/article/routes.mjs index 8ff966f..491d570 100644 --- a/base/article/routes.mjs +++ b/base/article/routes.mjs @@ -1,3 +1,4 @@ +import { FormidableHandler } from 'flaska' import { parseFiles } from '../file/util.mjs' import { parseArticles, parseArticle } from './util.mjs' import { upload } from '../media/upload.mjs' @@ -10,6 +11,17 @@ export default class ArticleRoutes { }) } + register(server) { + server.flaska.get('/api/articles/:path', this.getArticle.bind(this)) + server.flaska.get('/api/auth/articles', server.authenticate(), this.auth_getAllArticles.bind(this)) + server.flaska.get('/api/auth/articles/:id', server.authenticate(), this.auth_getSingleArticle.bind(this)) + server.flaska.put('/api/auth/articles/:id', [ + server.authenticate(), + server.formidable({ maxFileSize: 20 * 1024 * 1024, }), + ], this.auth_updateCreateSingleArticle.bind(this)) + server.flaska.delete('/api/auth/articles/:id', server.authenticate(), this.auth_removeSingleArticle.bind(this)) + } + /** GET: /api/articles/[path] */ async getArticle(ctx) { let res = await ctx.db.safeCallProc('article_get_single', [ctx.params.path]) diff --git a/base/authentication/routes.mjs b/base/authentication/routes.mjs index 839bf33..9176811 100644 --- a/base/authentication/routes.mjs +++ b/base/authentication/routes.mjs @@ -11,6 +11,10 @@ export default class AuthenticationRoutes { }) } + register(server) { + server.flaska.post('/api/authentication/login', server.jsonHandler(), this.login.bind(this)) + } + /** GET: /api/authentication/login */ async login(ctx) { let res = await ctx.db.safeCallProc('auth_login', [ diff --git a/base/config.mjs b/base/config.mjs index c726c08..589d4ea 100644 --- a/base/config.mjs +++ b/base/config.mjs @@ -1,6 +1,5 @@ import _ from 'lodash' import nconf from 'nconf-lite' -import { readFileSync } from 'fs' // Helper method for global usage. nconf.inTest = () => nconf.get('NODE_ENV') === 'test' diff --git a/base/defaults.mjs b/base/defaults.mjs deleted file mode 100644 index a2ef666..0000000 --- a/base/defaults.mjs +++ /dev/null @@ -1,34 +0,0 @@ - -// taken from isobject npm library -function isObject(val) { - return val != null && typeof val === 'object' && Array.isArray(val) === false -} - -export default function defaults(options, def) { - let out = { } - - if (options) { - Object.keys(options || {}).forEach(key => { - out[key] = options[key] - - if (Array.isArray(out[key])) { - out[key] = out[key].map(item => { - if (isObject(item)) return defaults(item) - return item - }) - } else if (out[key] && typeof out[key] === 'object') { - out[key] = defaults(options[key], def && def[key]) - } - }) - } - - if (def) { - Object.keys(def).forEach(function(key) { - if (typeof out[key] === 'undefined') { - out[key] = def[key] - } - }) - } - - return out -} diff --git a/base/package.json b/base/package.json new file mode 100644 index 0000000..b4675a4 --- /dev/null +++ b/base/package.json @@ -0,0 +1,9 @@ +{ + "dependencies": { + "dot": "^2.0.0-beta.1", + "flaska": "^1.3.0", + "formidable": "^1.2.6", + "msnodesqlv8": "^2.4.7", + "nconf-lite": "^1.0.1" + } +} diff --git a/base/page/routes.mjs b/base/page/routes.mjs index f39f2ed..40c4ca3 100644 --- a/base/page/routes.mjs +++ b/base/page/routes.mjs @@ -11,6 +11,19 @@ export default class PageRoutes { }) } + register(server) { + server.flaska.get('/api/pagetree', this.getPageTree.bind(this)) + server.flaska.get('/api/frontpage', this.getPage.bind(this)) + server.flaska.get('/api/pages/:path', this.getPage.bind(this)) + server.flaska.get('/api/auth/pages', server.authenticate(), this.auth_getAllPages.bind(this)) + server.flaska.get('/api/auth/pages/:id', server.authenticate(), this.auth_getSinglePage.bind(this)) + server.flaska.put('/api/auth/pages/:id', [ + server.authenticate(), + server.formidable({ maxFileSize: 20 * 1024 * 1024, }), + ], this.auth_updateCreateSinglePage.bind(this)) + server.flaska.delete('/api/auth/pages/:id', server.authenticate(), this.auth_removeSinglePage.bind(this)) + } + /** GET: /api/pagetree */ async getPageTree(ctx, onlyReturn = false) { let res = await ctx.db.safeCallProc('pages_get_tree', []) diff --git a/base/pagination/helpers.mjs b/base/pagination/helpers.mjs deleted file mode 100644 index 0764b83..0000000 --- a/base/pagination/helpers.mjs +++ /dev/null @@ -1,124 +0,0 @@ -import _ from 'lodash' -import config from '../config.mjs' - -function limit(value, min, max, fallback) { - let out = parseInt(value, 10) - - if (!out) { - out = fallback - } - - if (out < min) { - out = min - } - - if (out > max) { - out = max - } - - return out -} - -export function parsePagination(ctx) { - let out = { - perPage: limit(ctx.query.perPage, 1, 1500, 1250), - page: limit(ctx.query.page, 1, Number.MAX_SAFE_INTEGER, 1), - } - - Object.keys(ctx.query).forEach(item => { - if (item.startsWith('perPage.')) { - let name = item.substring(8) - out[name] = { - perPage: limit(ctx.query[`perPage.${name}`], 1, 1500, 1250), - page: limit(ctx.query[`page.${name}`], 1, Number.MAX_SAFE_INTEGER, 1), - } - } - }) - - return out -} - -export function parseFilter(ctx) { - let where - let whereNot - - where = _.omitBy(ctx.query, test => test[0] === '!') - - whereNot = _.pickBy(ctx.query, test => test[0] === '!') - whereNot = _.transform( - whereNot, - (result, value, key) => (result[key] = value.slice(1)) - ) - - return { - where: pick => _.pick(where, pick), - whereNot: pick => _.pick(whereNot, pick), - includes: (ctx.query.includes && ctx.query.includes.split(',')) || [], - } -} - -export function generateLinks(ctx, total) { - let out = [] - - let base = _(ctx.query) - .omit(['page']) - .transform((res, val, key) => res.push(`${key}=${val}`), []) - .value() - - if (!ctx.query.perPage) { - base.push(`perPage=${ctx.state.pagination.perPage}`) - } - - // let protocol = ctx.protocol - - // if (config.get('frontend:url').startsWith('https')) { - // protocol = 'https' - // } - - let proto = 'http' - - if (config.get('frontend:url').startsWith('https')) { - proto = 'https' - } - - let first = new URL(ctx.path, proto + '://' + ctx.host).toString() - - first += `?${base.join('&')}` - - // Add the current page first - out.push({ - rel: 'current', - title: `Page ${ctx.query.page || 1}`, - url: `${first}`, - }) - - // Then add any previous pages if we can - if (ctx.state.pagination.page > 1) { - out.push({ - rel: 'previous', - title: 'Previous', - url: `${first}&page=${ctx.state.pagination.page - 1}`, - }) - out.push({ - rel: 'first', - title: 'First', - url: `${first}&page=1`, - }) - } - - // Then add any next pages if we can - if ((ctx.state.pagination.perPage * (ctx.state.pagination.page - 1)) + ctx.state.pagination.perPage < total) { - out.push({ - rel: 'next', - title: 'Next', - url: `${first}&page=${ctx.state.pagination.page + 1}`, - }) - out.push({ - rel: 'last', - title: 'Last', - url: `${first}&page=${Math.ceil(total / ctx.state.pagination.perPage)}`, - }) - } - - return out -} diff --git a/base/pagination/parser.mjs b/base/pagination/parser.mjs deleted file mode 100644 index cdb6603..0000000 --- a/base/pagination/parser.mjs +++ /dev/null @@ -1,33 +0,0 @@ -import format from 'format-link-header' - -import * as pagination from './helpers.mjs' - -export default class ParserMiddleware { - constructor(opts = {}) { - Object.assign(this, { - pagination: opts.pagination || pagination, - format: opts.format || format, - }) - } - - contextParser() { - return (ctx) => { - ctx.state.pagination = this.pagination.parsePagination(ctx) - ctx.state.filter = this.pagination.parseFilter(ctx) - } - } - - generateLinks() { - return async (ctx, next) => { - await next() - - if (ctx.state.pagination.total > 0) { - ctx.set('Link', this.format(this.pagination.generateLinks(ctx, ctx.state.pagination.total))) - } - - if (ctx.state.pagination.total != null) { - ctx.set('pagination_total', ctx.state.pagination.total) - } - } - } -} diff --git a/base/serve.mjs b/base/serve.mjs index 906eada..e6afec0 100644 --- a/base/serve.mjs +++ b/base/serve.mjs @@ -21,6 +21,10 @@ export default class ServeHandler { // console.log(indexFile.toString()) } + register(server) { + server.flaska.get('/::file', this.serve.bind(this)) + } + /** GET: /::file */ serve(ctx) { if (ctx.params.file.startsWith('api/')) { diff --git a/base/server.mjs b/base/server.mjs index 1be6fe2..35d842f 100644 --- a/base/server.mjs +++ b/base/server.mjs @@ -4,93 +4,106 @@ import formidable from 'formidable' import { initPool } from './db.mjs' import config from './config.mjs' import PageRoutes from './page/routes.mjs' -import ServeHandler from './serve.mjs' import ArticleRoutes from './article/routes.mjs' import AuthenticationRoutes from './authentication/routes.mjs' import { authenticate } from './authentication/security.mjs' -export function run(http, port, core) { - let localUtil = new core.sc.Util(import.meta.url) +export default class Server { + constructor(http, port, core, opts = {}) { + Object.assign(this, opts) + this.http = http + this.port = port + this.core = core - // Create our server - const flaska = new Flaska({ - appendHeaders: { - 'Content-Security-Policy': `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; font-src 'self' data:; object-src 'none'; frame-ancestors 'none'`, - }, - log: core.log, - nonce: ['script-src'], - nonceCacheLength: 50, - }, http) - - // Create our database pool - let pool = initPool(core, config.get('mssql')) - - // configure our server - if (config.get('NODE_ENV') === 'development') { - flaska.devMode() + this.authenticate = authenticate + this.formidable = FormidableHandler.bind(this, formidable) + this.jsonHandler = JsonHandler + this.routes = [ + new PageRoutes(), + new ArticleRoutes(), + new AuthenticationRoutes(), + ] } - flaska.before(function(ctx) { - ctx.state.started = new Date().getTime() - ctx.db = pool - }) - flaska.before(QueryHandler()) - - flaska.after(function(ctx) { - let ended = new Date().getTime() - var requestTime = ended - ctx.state.started - - let status = '' - let level = 'info' - if (ctx.status >= 400) { - status = ctx.status + ' ' - level = 'warn' + getRouteInstance(type) { + for (let route of this.routes) { + if (route instanceof type) { + return route + } } - if (ctx.status >= 500) { - level = 'error' + } + + addCustomRoutes() { + + } + + runCreateServer() { + // Create our server + this.flaska = new Flaska({ + appendHeaders: { + 'Content-Security-Policy': `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; font-src 'self' data:; object-src 'none'; frame-ancestors 'none'`, + }, + log: this.core.log, + nonce: ['script-src'], + nonceCacheLength: 50, + }, this.http) + + // Create our database pool + let pool = this.runCreateDatabase() + + // configure our server + if (config.get('NODE_ENV') === 'development') { + this.flaska.devMode() } - ctx.log[level]({ - duration: requestTime, - status: ctx.status, - }, `<-- ${status}${ctx.method} ${ctx.url}`) - }) - - const page = new PageRoutes() - flaska.get('/api/pagetree', page.getPageTree.bind(page)) - flaska.get('/api/frontpage', page.getPage.bind(page)) - flaska.get('/api/pages/:path', page.getPage.bind(page)) - flaska.get('/api/auth/pages', authenticate(), page.auth_getAllPages.bind(page)) - flaska.get('/api/auth/pages/:id', authenticate(), page.auth_getSinglePage.bind(page)) - flaska.put('/api/auth/pages/:id', [ - authenticate(), - FormidableHandler(formidable, { maxFileSize: 20 * 1024 * 1024, }), - ], page.auth_updateCreateSinglePage.bind(page)) - flaska.delete('/api/auth/pages/:id', authenticate(), page.auth_removeSinglePage.bind(page)) + this.flaska.before(function(ctx) { + ctx.state.started = new Date().getTime() + ctx.db = pool + }) + this.flaska.before(QueryHandler()) + this.flaska.after(function(ctx) { + let ended = new Date().getTime() + var requestTime = ended - ctx.state.started - const article = new ArticleRoutes() - flaska.get('/api/articles/:path', article.getArticle.bind(article)) - flaska.get('/api/auth/articles', authenticate(), article.auth_getAllArticles.bind(article)) - flaska.get('/api/auth/articles/:id', authenticate(), article.auth_getSingleArticle.bind(article)) - flaska.put('/api/auth/articles/:id', [ - authenticate(), - FormidableHandler(formidable, { maxFileSize: 20 * 1024 * 1024, }), - ], article.auth_updateCreateSingleArticle.bind(article)) - flaska.delete('/api/auth/articles/:id', authenticate(), article.auth_removeSingleArticle.bind(article)) + let status = '' + let level = 'info' + if (ctx.status >= 400) { + status = ctx.status + ' ' + level = 'warn' + } + if (ctx.status >= 500) { + level = 'error' + } - const authentication = new AuthenticationRoutes() - flaska.post('/api/authentication/login', JsonHandler(), authentication.login.bind(authentication)) + ctx.log[level]({ + duration: requestTime, + status: ctx.status, + }, `<-- ${status}${ctx.method} ${ctx.url}`) + }) + } - const serve = new ServeHandler({ - pageRoutes: page, - root: localUtil.getPathFromRoot('../public'), - version: core.app.running, - frontend: config.get('frontend:url'), - }) - flaska.get('/::file', serve.serve.bind(serve)) + runRegisterRoutes() { + for (let route of this.routes) { + route.register(this) + } + } - return flaska.listenAsync(port).then(function() { - core.log.info('Server is listening on port ' + port) - }) -} \ No newline at end of file + runCreateDatabase() { + return initPool(this.core, config.get('mssql')) + } + + runStartListen() { + return this.flaska.listenAsync(this.port).then(() => { + this.core.log.info('Server is listening on port ' + this.port) + }) + } + + run() { + this.addCustomRoutes() + this.runCreateServer() + this.runRegisterRoutes() + + return this.runStartListen() + } +} diff --git a/nfp_moe/.npmrc b/nfp_moe/.npmrc new file mode 100644 index 0000000..9cf9495 --- /dev/null +++ b/nfp_moe/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/nfp_moe/api/server.mjs b/nfp_moe/api/server.mjs new file mode 100644 index 0000000..c8df354 --- /dev/null +++ b/nfp_moe/api/server.mjs @@ -0,0 +1,18 @@ +import config from '../base/config.mjs' +import Parent from '../base/server.mjs' +import ServeHandler from '../base/serve.mjs' +import PageRoutes from '../base/page/routes.mjs' + +export default class Server extends Parent { + addCustomRoutes() { + let page = this.getRouteInstance(PageRoutes) + + let localUtil = new this.core.sc.Util(import.meta.url) + this.routes.push(new ServeHandler({ + pageRoutes: page, + root: localUtil.getPathFromRoot('../public'), + version: this.core.app.running, + frontend: config.get('frontend:url'), + })) + } +} diff --git a/nfp_moe/base b/nfp_moe/base new file mode 120000 index 0000000..24312d1 --- /dev/null +++ b/nfp_moe/base @@ -0,0 +1 @@ +../base \ No newline at end of file diff --git a/nfp_moe/index.mjs b/nfp_moe/index.mjs index 1383212..d23ba90 100644 --- a/nfp_moe/index.mjs +++ b/nfp_moe/index.mjs @@ -1,32 +1,11 @@ -import config from './api/config.mjs' +import config from '../base/config.mjs' export function start(http, port, ctx) { config.stores.overrides.store = ctx.config return import('./api/server.mjs') - .then(function(server) { - return server.run(http, port, ctx) + .then(function(module) { + let server = new module.default(http, port, ctx) + return server.run() }) } - -/* -import log from './api/log.mjs' - -// Run the database script automatically. -import setup from './api/setup.mjs' - -setup().catch(async (error) => { - log.error({ code: error.code, message: error.message }, 'Error while preparing database') - log.error('Unable to verify database integrity.') - log.warn('Continuing anyways') - // import('./api/config').then(module => { - // log.error(error, 'Error while preparing database') - // log.error({ config: module.default.get() }, 'config used') - // process.exit(1) - // }) -}).then(() => - import('./api/server.mjs') -).catch(error => { - log.error(error, 'Unknown error starting server') -}) -*/ \ No newline at end of file diff --git a/nfp_moe/package.json b/nfp_moe/package.json index 295b50c..88176d6 100644 --- a/nfp_moe/package.json +++ b/nfp_moe/package.json @@ -22,7 +22,8 @@ "watch": { "dev:server": { "patterns": [ - "api/*" + "api/*", + "base/*" ], "extensions": "js,mjs", "quiet": true,