Compare commits
2 commits
a2b1fc9bc8
...
0ab9521e7a
Author | SHA1 | Date | |
---|---|---|---|
0ab9521e7a | |||
21545dcd6f |
45 changed files with 611 additions and 389 deletions
|
@ -51,30 +51,16 @@ nconf.overrides({})
|
||||||
|
|
||||||
nconf.defaults({
|
nconf.defaults({
|
||||||
"NODE_ENV": "development",
|
"NODE_ENV": "development",
|
||||||
"knex": {
|
|
||||||
"client": "pg",
|
|
||||||
"connection": {
|
|
||||||
"host" : "127.0.0.1",
|
|
||||||
"user" : "postgres",
|
|
||||||
"password" : "postgres",
|
|
||||||
"database" : "nfpmoe"
|
|
||||||
},
|
|
||||||
"connectionslave": null,
|
|
||||||
"migrations": {
|
|
||||||
},
|
|
||||||
"acquireConnectionTimeout": 10000
|
|
||||||
},
|
|
||||||
"frontend": {
|
"frontend": {
|
||||||
"url": "http://beta01.nfp.moe"
|
"url": "http://beta01.nfp.moe"
|
||||||
},
|
},
|
||||||
"jwt": {
|
"mssql": {
|
||||||
"secret": "this-is-my-secret",
|
"floor": 1,
|
||||||
"options": {
|
"ceiling": 2,
|
||||||
"expiresIn": 604800
|
"heartbeatSecs": 20,
|
||||||
}
|
"inactivityTimeoutSecs": 60,
|
||||||
|
"connectionString": "Driver={ODBC Driver 17 for SQL Server}; Server=localhost;UID=dev; PWD=dev; Database=nfp_moe",
|
||||||
},
|
},
|
||||||
"sessionsecret": "this-is-session-secret-lol",
|
|
||||||
"bcrypt": 5,
|
|
||||||
"fileSize": 524288000,
|
"fileSize": 524288000,
|
||||||
"upload": {
|
"upload": {
|
||||||
"baseurl": "https://cdn.nfp.is",
|
"baseurl": "https://cdn.nfp.is",
|
||||||
|
|
|
@ -1,9 +1,3 @@
|
||||||
|
|
||||||
import { createPrototype, safeColumns } from '../knex.mjs'
|
|
||||||
import Media from '../media/model.mjs'
|
|
||||||
// import Staff from '../staff/model.mjs'
|
|
||||||
// import Article from '../article/model.mjs'
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
Page model:
|
Page model:
|
||||||
|
@ -25,101 +19,8 @@ Page model:
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function PageItem(data) {
|
export async function getTree(ctx) {
|
||||||
Object.assign(this, data)
|
let res = await ctx.db.promises.callProc('pages_gettree', [])
|
||||||
this.children = []
|
console.log(res)
|
||||||
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Page() {
|
|
||||||
this.tableName = 'pages'
|
|
||||||
this.Model = PageItem
|
|
||||||
this.includes = {
|
|
||||||
media: Media.includeHasOne('pages.media_id', 'id'),
|
|
||||||
banner: Media.includeHasOne('pages.banner_id', 'id'),
|
|
||||||
}
|
|
||||||
this.publicFields = this.privateFields = safeColumns([
|
|
||||||
'staff_id',
|
|
||||||
'parent_id',
|
|
||||||
'name',
|
|
||||||
'path',
|
|
||||||
'description',
|
|
||||||
'banner_id',
|
|
||||||
'media_id',
|
|
||||||
])
|
|
||||||
this.init()
|
|
||||||
}
|
|
||||||
|
|
||||||
Page.prototype = createPrototype({
|
|
||||||
/* includes: {
|
|
||||||
staff: Staff.includeHasOne('staff_id', 'id'),
|
|
||||||
}, */
|
|
||||||
|
|
||||||
/*banner() {
|
|
||||||
return this.belongsTo(Media, 'banner_id')
|
|
||||||
},
|
|
||||||
|
|
||||||
parent() {
|
|
||||||
return this.belongsTo(Page, 'parent_id')
|
|
||||||
},
|
|
||||||
|
|
||||||
children() {
|
|
||||||
return this.hasManyFiltered(Page, 'children', 'parent_id')
|
|
||||||
.query(qb => {
|
|
||||||
qb.orderBy('name', 'ASC')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
news() {
|
|
||||||
return this.hasManyFiltered(Article, 'news', 'parent_id')
|
|
||||||
.query(qb => {
|
|
||||||
qb.orderBy('id', 'desc')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
media() {
|
|
||||||
return this.belongsTo(Media, 'media_id')
|
|
||||||
},
|
|
||||||
|
|
||||||
staff() {
|
|
||||||
return this.belongsTo(Staff, 'staff_id')
|
|
||||||
},*/
|
|
||||||
|
|
||||||
getSingle(id, includes = [], require = true, ctx = null) {
|
|
||||||
return this._getSingle(qb => {
|
|
||||||
qb.where(subq => {
|
|
||||||
subq.where(this.tableName + '.id', '=', Number(id) || 0)
|
|
||||||
.orWhere(this.tableName + '.path', '=', id)
|
|
||||||
})
|
|
||||||
}, includes, require, ctx)
|
|
||||||
},
|
|
||||||
|
|
||||||
async getTree() {
|
|
||||||
let items = await this.getAllQuery(this.query(
|
|
||||||
qb => qb.orderBy('name', 'ASC'),
|
|
||||||
[],
|
|
||||||
['parent_id', 'id', 'name', 'path']
|
|
||||||
))
|
|
||||||
|
|
||||||
let out = []
|
|
||||||
let map = new Map()
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
if (!items[i].parent_id) {
|
|
||||||
out.push(items[i])
|
|
||||||
}
|
|
||||||
map.set(items[i].id, items[i])
|
|
||||||
}
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
if (items[i].parent_id && map.has(items[i].parent_id)) {
|
|
||||||
map.get(items[i].parent_id).children.push(items[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const pageInstance = new Page()
|
|
||||||
|
|
||||||
pageInstance.addInclude('children', pageInstance.includeHasMany('parent_id', 'pages.id'))
|
|
||||||
pageInstance.addInclude('parent', pageInstance.includeHasOne('pages.parent_id', 'id'))
|
|
||||||
|
|
||||||
export default pageInstance
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import Page from './model.mjs'
|
import * as Page from './model.mjs'
|
||||||
import * as security from './security.mjs'
|
import * as security from './security.mjs'
|
||||||
|
|
||||||
export default class PageRoutes {
|
export default class PageRoutes {
|
||||||
|
@ -11,43 +11,6 @@ export default class PageRoutes {
|
||||||
|
|
||||||
/** GET: /api/pagetree */
|
/** GET: /api/pagetree */
|
||||||
async getPageTree(ctx) {
|
async getPageTree(ctx) {
|
||||||
ctx.body = await this.Page.getTree()
|
ctx.body = await this.Page.getTree(ctx)
|
||||||
}
|
|
||||||
|
|
||||||
/** GET: /api/pages */
|
|
||||||
async getAllPages(ctx) {
|
|
||||||
await this.security.ensureIncludes(ctx)
|
|
||||||
|
|
||||||
ctx.body = await this.Page.getAll(ctx, null, ctx.state.filter.includes, 'name')
|
|
||||||
}
|
|
||||||
|
|
||||||
/** GET: /api/pages/:id */
|
|
||||||
async getSinglePage(ctx) {
|
|
||||||
await this.security.ensureIncludes(ctx)
|
|
||||||
|
|
||||||
ctx.body = await this.Page.getSingle(ctx.params.pageId, ctx.state.filter.includes, true, ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** POST: /api/pages */
|
|
||||||
async createPage(ctx) {
|
|
||||||
await this.security.validUpdate(ctx)
|
|
||||||
|
|
||||||
ctx.body = await this.Page.create(ctx.request.body)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** PUT: /api/pages/:id */
|
|
||||||
async updatePage(ctx) {
|
|
||||||
await this.security.validUpdate(ctx)
|
|
||||||
|
|
||||||
let page = await this.Page.updateSingle(ctx, ctx.params.id, ctx.request.body)
|
|
||||||
|
|
||||||
ctx.body = page
|
|
||||||
}
|
|
||||||
|
|
||||||
/** DELETE: /api/pages/:id */
|
|
||||||
async removePage(ctx) {
|
|
||||||
await this.Page.updateSingle(ctx, ctx.params.id, { is_deleted: true })
|
|
||||||
|
|
||||||
ctx.status = 204
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
import filter from '../filter.mjs'
|
|
||||||
|
|
||||||
const requiredFields = [
|
|
||||||
'name',
|
|
||||||
'path',
|
|
||||||
]
|
|
||||||
|
|
||||||
const validFields = [
|
|
||||||
'name',
|
|
||||||
'path',
|
|
||||||
'parent_id',
|
|
||||||
'description',
|
|
||||||
'media_id',
|
|
||||||
'banner_id',
|
|
||||||
]
|
|
||||||
|
|
||||||
export async function ensureIncludes(ctx) {
|
|
||||||
let out = filter(ctx.state.filter.includes, ['staff', 'media', 'banner', 'news', 'news.media', 'parent', 'children'])
|
|
||||||
|
|
||||||
if (out.length > 0) {
|
|
||||||
ctx.throw(422, `Includes had following invalid values: ${out.join(', ')}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function validUpdate(ctx) {
|
|
||||||
requiredFields.forEach(item => {
|
|
||||||
if (ctx.request.body[item] == null) {
|
|
||||||
ctx.throw(422, `Property was missing: ${item}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
let out = filter(Object.keys(ctx.request.body), validFields)
|
|
||||||
|
|
||||||
if (out.length > 0) {
|
|
||||||
ctx.throw(422, `Body had following invalid properties: ${out.join(', ')}`)
|
|
||||||
}
|
|
||||||
}
|
|
124
api/pagination/helpers.mjs
Normal file
124
api/pagination/helpers.mjs
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
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
|
||||||
|
}
|
33
api/pagination/parser.mjs
Normal file
33
api/pagination/parser.mjs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,61 +1,38 @@
|
||||||
import send from 'koa-send'
|
import path from 'path'
|
||||||
import defaults from './defaults.mjs'
|
import { FileResponse, HttpError } from 'flaska'
|
||||||
import access from './access/index.mjs'
|
import fs from 'fs/promises'
|
||||||
import { restrict } from './access/middleware.mjs'
|
|
||||||
import { serveIndex } from './serveindex.mjs'
|
|
||||||
import config from './config.mjs'
|
|
||||||
|
|
||||||
const restrictAdmin = restrict(access.Manager)
|
export default class ServeHandler {
|
||||||
|
constructor(opts = {}) {
|
||||||
export function serve(docRoot, pathname, options = {}) {
|
Object.assign(this, {
|
||||||
options.root = docRoot
|
fs: opts.fs || fs,
|
||||||
|
root: opts.root,
|
||||||
return async (ctx, next) => {
|
|
||||||
let opts = defaults({}, options)
|
|
||||||
if (ctx.request.method === 'OPTIONS') return
|
|
||||||
|
|
||||||
let filepath = ctx.path.replace(pathname, '')
|
|
||||||
|
|
||||||
if (filepath === '/') {
|
|
||||||
filepath = '/index.html'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filepath.endsWith('.jpg')
|
|
||||||
|| filepath.endsWith('.png')
|
|
||||||
|| filepath.endsWith('.js')
|
|
||||||
|| filepath.endsWith('.css')
|
|
||||||
|| filepath.endsWith('.avif')
|
|
||||||
|| filepath.endsWith('.svg')) {
|
|
||||||
if (filepath.indexOf('admin') === -1) {
|
|
||||||
opts = defaults({ maxage: 2592000 * 1000 }, opts)
|
|
||||||
}
|
|
||||||
if (filepath.endsWith('.avif')) {
|
|
||||||
ctx.type = 'image/avif'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filepath === '/index.html') {
|
|
||||||
return serveIndex(ctx, '/')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filepath.indexOf('admin') >= 0
|
|
||||||
&& (filepath.indexOf('js') >= 0
|
|
||||||
|| filepath.indexOf('css') >= 0)) {
|
|
||||||
if (filepath.indexOf('.map') === -1 && filepath.indexOf('.scss') === -1) {
|
|
||||||
await restrictAdmin(ctx)
|
|
||||||
ctx.set('Cache-Control', 'no-store, no-cache, must-revalidate')
|
|
||||||
} else if (config.get('NODE_ENV') !== 'development') {
|
|
||||||
ctx.status = 404
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return send(ctx, filepath, opts).catch((er) => {
|
|
||||||
if (er.code === 'ENOENT' && er.status === 404) {
|
|
||||||
ctx.type = null
|
|
||||||
return serveIndex(ctx, filepath)
|
|
||||||
// return send(ctx, '/index.html', options)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/** GET: /::file */
|
||||||
|
serve(ctx) {
|
||||||
|
if (ctx.params.file.startsWith('api/')) {
|
||||||
|
throw new HttpError(404, 'Not Found: ' + ctx.params.file, { status: 404, message: 'Not Found: ' + ctx.params.file })
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = path.resolve(path.join(this.root, ctx.params.file ? ctx.params.file : 'index.html'))
|
||||||
|
|
||||||
|
if (!file.startsWith(this.root)) {
|
||||||
|
ctx.status = 404
|
||||||
|
ctx.body = 'HTTP 404 Error'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.fs.stat(file).catch((err) => {
|
||||||
|
if (err.code === 'ENOENT') {
|
||||||
|
file = path.resolve(path.join(this.root, 'index.html'))
|
||||||
|
return this.fs.stat(file)
|
||||||
|
}
|
||||||
|
return Promise.reject(err)
|
||||||
|
})
|
||||||
|
.then(function(stat) {
|
||||||
|
ctx.body = new FileResponse(file, stat)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
123
api/server.mjs
123
api/server.mjs
|
@ -1,39 +1,96 @@
|
||||||
import Koa from 'koa-lite'
|
import { Flaska, QueryHandler } from 'flaska'
|
||||||
import bodyParser from 'koa-bodyparser'
|
import MSSQL from 'msnodesqlv8'
|
||||||
import cors from '@koa/cors'
|
|
||||||
|
|
||||||
import config from './config.mjs'
|
import config from './config.mjs'
|
||||||
import router from './router.mjs'
|
import PageRoutes from './page/routes.mjs'
|
||||||
import Jwt from './jwt.mjs'
|
import ServeHandler from './serve.mjs'
|
||||||
import log from './log.mjs'
|
// import ArticleRoutes from './article/routes.mjs'
|
||||||
import { serve } from './serve.mjs'
|
import ParserMiddleware from './pagination/parser.mjs'
|
||||||
import { mask } from './middlewares/mask.mjs'
|
|
||||||
import { errorHandler } from './error/middleware.mjs'
|
|
||||||
import { accessChecks } from './access/middleware.mjs'
|
|
||||||
import ParserMiddleware from './parser/middleware.mjs'
|
|
||||||
|
|
||||||
const app = new Koa()
|
export function run(http, port, core) {
|
||||||
const parser = new ParserMiddleware()
|
let localUtil = new core.sc.Util(import.meta.url)
|
||||||
|
|
||||||
app.use(log.logMiddleware())
|
// Create our server
|
||||||
app.use(errorHandler())
|
const flaska = new Flaska({
|
||||||
app.use(mask())
|
log: core.log,
|
||||||
app.use(bodyParser())
|
nonce: ['script-src'],
|
||||||
app.use(parser.contextParser())
|
nonceCacheLength: 50,
|
||||||
app.use(accessChecks())
|
defaultHeaders: {
|
||||||
app.use(parser.generateLinks())
|
'Server': 'Flaska',
|
||||||
app.use(Jwt.jwtMiddleware())
|
'X-Content-Type-Options': 'nosniff',
|
||||||
app.use(cors({
|
'Content-Security-Policy': `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; object-src 'none'; frame-ancestors 'none'; script-src 'self'`,
|
||||||
exposeHeaders: ['link', 'pagination_total'],
|
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||||
credentials: true,
|
'Cross-Origin-Resource-Policy': 'same-origin',
|
||||||
}))
|
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||||
app.use(router.routes())
|
},
|
||||||
app.use(router.allowedMethods())
|
}, http)
|
||||||
app.use(serve('./public', '/public'))
|
|
||||||
|
// Create our database pool
|
||||||
|
const pool = new MSSQL.Pool(config.get('mssql'))
|
||||||
|
|
||||||
const server = app.listen(
|
core.log.info(config.get('mssql'), 'MSSQL database setttings')
|
||||||
config.get('server:port'),
|
|
||||||
() => log.info(`Server running on port ${config.get('server:port')}`)
|
|
||||||
)
|
|
||||||
|
|
||||||
export default server
|
pool.on('open', function() {
|
||||||
|
core.log.info('MSSQL connection open')
|
||||||
|
})
|
||||||
|
|
||||||
|
pool.on('error', function(error) {
|
||||||
|
core.log.error(error, 'Error in MSSQL pool')
|
||||||
|
})
|
||||||
|
|
||||||
|
pool.open()
|
||||||
|
|
||||||
|
// configure our server
|
||||||
|
if (config.get('NODE_ENV') === 'development') {
|
||||||
|
flaska.devMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
const parser = new ParserMiddleware()
|
||||||
|
|
||||||
|
flaska.before(function(ctx) {
|
||||||
|
ctx.state.started = new Date().getTime()
|
||||||
|
ctx.db = pool
|
||||||
|
})
|
||||||
|
flaska.before(QueryHandler())
|
||||||
|
flaska.before(parser.contextParser())
|
||||||
|
|
||||||
|
//
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
if (ctx.status >= 500) {
|
||||||
|
level = 'error'
|
||||||
|
}
|
||||||
|
|
||||||
|
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/pages', page.getAllPages.bind(page))
|
||||||
|
// flaska.get('/api/pages/:pageId', page.getSinglePage.bind(page))
|
||||||
|
|
||||||
|
// const article = new ArticleRoutes()
|
||||||
|
// flaska.get('/api/articles/public', article.getPublicAllArticles.bind(article))
|
||||||
|
// flaska.get('/api/articles/public/:id', article.getPublicSingleArticle.bind(article))
|
||||||
|
// flaska.get('/api/pages/:pageId/articles/public', article.getPublicAllPageArticles.bind(article))
|
||||||
|
|
||||||
|
const serve = new ServeHandler({
|
||||||
|
root: localUtil.getPathFromRoot('../public'),
|
||||||
|
})
|
||||||
|
flaska.get('/::file', serve.serve.bind(serve))
|
||||||
|
|
||||||
|
return flaska.listenAsync(port).then(function() {
|
||||||
|
core.log.info('Server is listening on port ' + port)
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,98 +0,0 @@
|
||||||
import path from 'path'
|
|
||||||
import fs from 'fs/promises'
|
|
||||||
import { Flaska, FileResponse, HttpError, QueryHandler } from 'flaska'
|
|
||||||
|
|
||||||
import config from './config.mjs'
|
|
||||||
import PageRoutes from './page/routes.mjs'
|
|
||||||
import ArticleRoutes from './article/routes.mjs'
|
|
||||||
import ParserMiddleware from './parser/middleware.mjs'
|
|
||||||
|
|
||||||
export function run(http, port, core) {
|
|
||||||
let localUtil = new core.sc.Util(import.meta.url)
|
|
||||||
const staticRoot = localUtil.getPathFromRoot('../public')
|
|
||||||
|
|
||||||
const flaska = new Flaska({
|
|
||||||
log: core.log,
|
|
||||||
nonce: ['script-src'],
|
|
||||||
nonceCacheLength: 50,
|
|
||||||
defaultHeaders: {
|
|
||||||
'Server': 'Flaska',
|
|
||||||
'X-Content-Type-Options': 'nosniff',
|
|
||||||
'Content-Security-Policy': `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; object-src 'none'; frame-ancestors 'none'; script-src 'self'`,
|
|
||||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
|
||||||
'Cross-Origin-Resource-Policy': 'same-origin',
|
|
||||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
|
||||||
},
|
|
||||||
}, http)
|
|
||||||
|
|
||||||
if (config.get('NODE_ENV') === 'development') {
|
|
||||||
flaska.devMode()
|
|
||||||
}
|
|
||||||
|
|
||||||
const parser = new ParserMiddleware()
|
|
||||||
|
|
||||||
flaska.before(function(ctx) {
|
|
||||||
ctx.state.started = new Date().getTime()
|
|
||||||
})
|
|
||||||
flaska.before(QueryHandler())
|
|
||||||
flaska.before(parser.contextParser())
|
|
||||||
|
|
||||||
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'
|
|
||||||
}
|
|
||||||
if (ctx.status >= 500) {
|
|
||||||
level = 'error'
|
|
||||||
}
|
|
||||||
|
|
||||||
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/pages', page.getAllPages.bind(page))
|
|
||||||
flaska.get('/api/pages/:pageId', page.getSinglePage.bind(page))
|
|
||||||
|
|
||||||
const article = new ArticleRoutes()
|
|
||||||
flaska.get('/api/articles/public', article.getPublicAllArticles.bind(article))
|
|
||||||
flaska.get('/api/articles/public/:id', article.getPublicSingleArticle.bind(article))
|
|
||||||
flaska.get('/api/pages/:pageId/articles/public', article.getPublicAllPageArticles.bind(article))
|
|
||||||
|
|
||||||
flaska.get('/::file', function(ctx) {
|
|
||||||
if (ctx.params.file.startsWith('api/')) {
|
|
||||||
throw new HttpError(404, 'Not Found: ' + ctx.params.file, { status: 404, message: 'Not Found: ' + ctx.params.file })
|
|
||||||
}
|
|
||||||
|
|
||||||
let file = path.resolve(path.join(staticRoot, ctx.params.file ? ctx.params.file : 'index.html'))
|
|
||||||
|
|
||||||
if (!file.startsWith(staticRoot)) {
|
|
||||||
ctx.status = 404
|
|
||||||
ctx.body = 'HTTP 404 Error'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return fs.stat(file).catch(function(err) {
|
|
||||||
if (err.code === 'ENOENT') {
|
|
||||||
file = path.resolve(path.join(staticRoot, 'index.html'))
|
|
||||||
return fs.stat(file)
|
|
||||||
}
|
|
||||||
return Promise.reject(err)
|
|
||||||
})
|
|
||||||
.then(function(stat) {
|
|
||||||
ctx.body = new FileResponse(file, stat)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return flaska.listenAsync(port).then(function() {
|
|
||||||
core.log.info('Server is listening on port ' + port)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -3,7 +3,7 @@ import config from './api/config.mjs'
|
||||||
export function start(http, port, ctx) {
|
export function start(http, port, ctx) {
|
||||||
config.stores.overrides.store = ctx.config
|
config.stores.overrides.store = ctx.config
|
||||||
|
|
||||||
return import('./api/server_flaska.mjs')
|
return import('./api/server.mjs')
|
||||||
.then(function(server) {
|
.then(function(server) {
|
||||||
return server.run(http, port, ctx)
|
return server.run(http, port, ctx)
|
||||||
})
|
})
|
||||||
|
|
125
old/page/model.mjs
Normal file
125
old/page/model.mjs
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
|
||||||
|
import { createPrototype, safeColumns } from '../knex.mjs'
|
||||||
|
import Media from '../media/model.mjs'
|
||||||
|
// import Staff from '../staff/model.mjs'
|
||||||
|
// import Article from '../article/model.mjs'
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Page model:
|
||||||
|
{
|
||||||
|
filename,
|
||||||
|
filetype,
|
||||||
|
small_image,
|
||||||
|
medium_image,
|
||||||
|
large_image,
|
||||||
|
*small_url,
|
||||||
|
*medium_url,
|
||||||
|
*large_url,
|
||||||
|
size,
|
||||||
|
staff_id,
|
||||||
|
is_deleted,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
function PageItem(data) {
|
||||||
|
Object.assign(this, data)
|
||||||
|
this.children = []
|
||||||
|
}
|
||||||
|
|
||||||
|
function Page() {
|
||||||
|
this.tableName = 'pages'
|
||||||
|
this.Model = PageItem
|
||||||
|
this.includes = {
|
||||||
|
media: Media.includeHasOne('pages.media_id', 'id'),
|
||||||
|
banner: Media.includeHasOne('pages.banner_id', 'id'),
|
||||||
|
}
|
||||||
|
this.publicFields = this.privateFields = safeColumns([
|
||||||
|
'staff_id',
|
||||||
|
'parent_id',
|
||||||
|
'name',
|
||||||
|
'path',
|
||||||
|
'description',
|
||||||
|
'banner_id',
|
||||||
|
'media_id',
|
||||||
|
])
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
Page.prototype = createPrototype({
|
||||||
|
/* includes: {
|
||||||
|
staff: Staff.includeHasOne('staff_id', 'id'),
|
||||||
|
}, */
|
||||||
|
|
||||||
|
/*banner() {
|
||||||
|
return this.belongsTo(Media, 'banner_id')
|
||||||
|
},
|
||||||
|
|
||||||
|
parent() {
|
||||||
|
return this.belongsTo(Page, 'parent_id')
|
||||||
|
},
|
||||||
|
|
||||||
|
children() {
|
||||||
|
return this.hasManyFiltered(Page, 'children', 'parent_id')
|
||||||
|
.query(qb => {
|
||||||
|
qb.orderBy('name', 'ASC')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
news() {
|
||||||
|
return this.hasManyFiltered(Article, 'news', 'parent_id')
|
||||||
|
.query(qb => {
|
||||||
|
qb.orderBy('id', 'desc')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
media() {
|
||||||
|
return this.belongsTo(Media, 'media_id')
|
||||||
|
},
|
||||||
|
|
||||||
|
staff() {
|
||||||
|
return this.belongsTo(Staff, 'staff_id')
|
||||||
|
},*/
|
||||||
|
|
||||||
|
getSingle(id, includes = [], require = true, ctx = null) {
|
||||||
|
return this._getSingle(qb => {
|
||||||
|
qb.where(subq => {
|
||||||
|
subq.where(this.tableName + '.id', '=', Number(id) || 0)
|
||||||
|
.orWhere(this.tableName + '.path', '=', id)
|
||||||
|
})
|
||||||
|
}, includes, require, ctx)
|
||||||
|
},
|
||||||
|
|
||||||
|
async getTree() {
|
||||||
|
let items = await this.getAllQuery(this.query(
|
||||||
|
qb => qb.orderBy('name', 'ASC'),
|
||||||
|
[],
|
||||||
|
['parent_id', 'id', 'name', 'path']
|
||||||
|
))
|
||||||
|
|
||||||
|
let out = []
|
||||||
|
let map = new Map()
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
if (!items[i].parent_id) {
|
||||||
|
out.push(items[i])
|
||||||
|
}
|
||||||
|
map.set(items[i].id, items[i])
|
||||||
|
}
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
if (items[i].parent_id && map.has(items[i].parent_id)) {
|
||||||
|
map.get(items[i].parent_id).children.push(items[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const pageInstance = new Page()
|
||||||
|
|
||||||
|
pageInstance.addInclude('children', pageInstance.includeHasMany('parent_id', 'pages.id'))
|
||||||
|
pageInstance.addInclude('parent', pageInstance.includeHasOne('pages.parent_id', 'id'))
|
||||||
|
|
||||||
|
export default pageInstance
|
53
old/page/routes.mjs
Normal file
53
old/page/routes.mjs
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import Page from './model.mjs'
|
||||||
|
import * as security from './security.mjs'
|
||||||
|
|
||||||
|
export default class PageRoutes {
|
||||||
|
constructor(opts = {}) {
|
||||||
|
Object.assign(this, {
|
||||||
|
Page: opts.Page || Page,
|
||||||
|
security: opts.security || security,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET: /api/pagetree */
|
||||||
|
async getPageTree(ctx) {
|
||||||
|
ctx.body = await this.Page.getTree()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET: /api/pages */
|
||||||
|
async getAllPages(ctx) {
|
||||||
|
await this.security.ensureIncludes(ctx)
|
||||||
|
|
||||||
|
ctx.body = await this.Page.getAll(ctx, null, ctx.state.filter.includes, 'name')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET: /api/pages/:id */
|
||||||
|
async getSinglePage(ctx) {
|
||||||
|
await this.security.ensureIncludes(ctx)
|
||||||
|
|
||||||
|
ctx.body = await this.Page.getSingle(ctx.params.pageId, ctx.state.filter.includes, true, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST: /api/pages */
|
||||||
|
async createPage(ctx) {
|
||||||
|
await this.security.validUpdate(ctx)
|
||||||
|
|
||||||
|
ctx.body = await this.Page.create(ctx.request.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** PUT: /api/pages/:id */
|
||||||
|
async updatePage(ctx) {
|
||||||
|
await this.security.validUpdate(ctx)
|
||||||
|
|
||||||
|
let page = await this.Page.updateSingle(ctx, ctx.params.id, ctx.request.body)
|
||||||
|
|
||||||
|
ctx.body = page
|
||||||
|
}
|
||||||
|
|
||||||
|
/** DELETE: /api/pages/:id */
|
||||||
|
async removePage(ctx) {
|
||||||
|
await this.Page.updateSingle(ctx, ctx.params.id, { is_deleted: true })
|
||||||
|
|
||||||
|
ctx.status = 204
|
||||||
|
}
|
||||||
|
}
|
37
old/page/security.mjs
Normal file
37
old/page/security.mjs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import filter from '../filter.mjs'
|
||||||
|
|
||||||
|
const requiredFields = [
|
||||||
|
'name',
|
||||||
|
'path',
|
||||||
|
]
|
||||||
|
|
||||||
|
const validFields = [
|
||||||
|
'name',
|
||||||
|
'path',
|
||||||
|
'parent_id',
|
||||||
|
'description',
|
||||||
|
'media_id',
|
||||||
|
'banner_id',
|
||||||
|
]
|
||||||
|
|
||||||
|
export async function ensureIncludes(ctx) {
|
||||||
|
let out = filter(ctx.state.filter.includes, ['staff', 'media', 'banner', 'news', 'news.media', 'parent', 'children'])
|
||||||
|
|
||||||
|
if (out.length > 0) {
|
||||||
|
ctx.throw(422, `Includes had following invalid values: ${out.join(', ')}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function validUpdate(ctx) {
|
||||||
|
requiredFields.forEach(item => {
|
||||||
|
if (ctx.request.body[item] == null) {
|
||||||
|
ctx.throw(422, `Property was missing: ${item}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let out = filter(Object.keys(ctx.request.body), validFields)
|
||||||
|
|
||||||
|
if (out.length > 0) {
|
||||||
|
ctx.throw(422, `Body had following invalid properties: ${out.join(', ')}`)
|
||||||
|
}
|
||||||
|
}
|
61
old/serve.mjs
Normal file
61
old/serve.mjs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import send from 'koa-send'
|
||||||
|
import defaults from './defaults.mjs'
|
||||||
|
import access from './access/index.mjs'
|
||||||
|
import { restrict } from './access/middleware.mjs'
|
||||||
|
import { serveIndex } from './serveindex.mjs'
|
||||||
|
import config from './config.mjs'
|
||||||
|
|
||||||
|
const restrictAdmin = restrict(access.Manager)
|
||||||
|
|
||||||
|
export function serve(docRoot, pathname, options = {}) {
|
||||||
|
options.root = docRoot
|
||||||
|
|
||||||
|
return async (ctx, next) => {
|
||||||
|
let opts = defaults({}, options)
|
||||||
|
if (ctx.request.method === 'OPTIONS') return
|
||||||
|
|
||||||
|
let filepath = ctx.path.replace(pathname, '')
|
||||||
|
|
||||||
|
if (filepath === '/') {
|
||||||
|
filepath = '/index.html'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filepath.endsWith('.jpg')
|
||||||
|
|| filepath.endsWith('.png')
|
||||||
|
|| filepath.endsWith('.js')
|
||||||
|
|| filepath.endsWith('.css')
|
||||||
|
|| filepath.endsWith('.avif')
|
||||||
|
|| filepath.endsWith('.svg')) {
|
||||||
|
if (filepath.indexOf('admin') === -1) {
|
||||||
|
opts = defaults({ maxage: 2592000 * 1000 }, opts)
|
||||||
|
}
|
||||||
|
if (filepath.endsWith('.avif')) {
|
||||||
|
ctx.type = 'image/avif'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filepath === '/index.html') {
|
||||||
|
return serveIndex(ctx, '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filepath.indexOf('admin') >= 0
|
||||||
|
&& (filepath.indexOf('js') >= 0
|
||||||
|
|| filepath.indexOf('css') >= 0)) {
|
||||||
|
if (filepath.indexOf('.map') === -1 && filepath.indexOf('.scss') === -1) {
|
||||||
|
await restrictAdmin(ctx)
|
||||||
|
ctx.set('Cache-Control', 'no-store, no-cache, must-revalidate')
|
||||||
|
} else if (config.get('NODE_ENV') !== 'development') {
|
||||||
|
ctx.status = 404
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return send(ctx, filepath, opts).catch((er) => {
|
||||||
|
if (er.code === 'ENOENT' && er.status === 404) {
|
||||||
|
ctx.type = null
|
||||||
|
return serveIndex(ctx, filepath)
|
||||||
|
// return send(ctx, '/index.html', options)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
39
old/server.mjs
Normal file
39
old/server.mjs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import Koa from 'koa-lite'
|
||||||
|
import bodyParser from 'koa-bodyparser'
|
||||||
|
import cors from '@koa/cors'
|
||||||
|
|
||||||
|
import config from './config.mjs'
|
||||||
|
import router from './router.mjs'
|
||||||
|
import Jwt from './jwt.mjs'
|
||||||
|
import log from './log.mjs'
|
||||||
|
import { serve } from './serve.mjs'
|
||||||
|
import { mask } from './middlewares/mask.mjs'
|
||||||
|
import { errorHandler } from './error/middleware.mjs'
|
||||||
|
import { accessChecks } from './access/middleware.mjs'
|
||||||
|
import ParserMiddleware from './parser/middleware.mjs'
|
||||||
|
|
||||||
|
const app = new Koa()
|
||||||
|
const parser = new ParserMiddleware()
|
||||||
|
|
||||||
|
app.use(log.logMiddleware())
|
||||||
|
app.use(errorHandler())
|
||||||
|
app.use(mask())
|
||||||
|
app.use(bodyParser())
|
||||||
|
app.use(parser.contextParser())
|
||||||
|
app.use(accessChecks())
|
||||||
|
app.use(parser.generateLinks())
|
||||||
|
app.use(Jwt.jwtMiddleware())
|
||||||
|
app.use(cors({
|
||||||
|
exposeHeaders: ['link', 'pagination_total'],
|
||||||
|
credentials: true,
|
||||||
|
}))
|
||||||
|
app.use(router.routes())
|
||||||
|
app.use(router.allowedMethods())
|
||||||
|
app.use(serve('./public', '/public'))
|
||||||
|
|
||||||
|
const server = app.listen(
|
||||||
|
config.get('server:port'),
|
||||||
|
() => log.info(`Server running on port ${config.get('server:port')}`)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default server
|
|
@ -55,6 +55,7 @@
|
||||||
"http-errors": "^1.7.2",
|
"http-errors": "^1.7.2",
|
||||||
"json-mask": "^0.3.8",
|
"json-mask": "^0.3.8",
|
||||||
"knex-core": "^0.19.5",
|
"knex-core": "^0.19.5",
|
||||||
|
"msnodesqlv8": "^2.4.7",
|
||||||
"nconf-lite": "^1.0.1",
|
"nconf-lite": "^1.0.1",
|
||||||
"parse-torrent": "^7.0.1",
|
"parse-torrent": "^7.0.1",
|
||||||
"pg": "^8.7.3",
|
"pg": "^8.7.3",
|
||||||
|
|
Loading…
Reference in a new issue