mirror of https://github.com/requarks/wiki
parent
fc820eb1b6
commit
e9e93eff42
@ -1,6 +1,4 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
arabic: '\u0600-\u06ff\u0750-\u077f\ufb50-\ufc3f\ufe70-\ufefc',
|
||||
cjk: '\u4E00-\u9FBF\u3040-\u309F\u30A0-\u30FFㄱ-ㅎ가-힣ㅏ-ㅣ',
|
||||
youtube: /(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|&v(?:i)?=))([^#&?]*).*/,
|
@ -1,158 +0,0 @@
|
||||
/* global WIKI */
|
||||
|
||||
const express = require('express')
|
||||
const ExpressBrute = require('express-brute')
|
||||
const BruteKnex = require('../helpers/brute-knex')
|
||||
const router = express.Router()
|
||||
const moment = require('moment')
|
||||
const _ = require('lodash')
|
||||
const path = require('path')
|
||||
|
||||
const bruteforce = new ExpressBrute(new BruteKnex({
|
||||
createTable: true,
|
||||
knex: WIKI.db.knex
|
||||
}), {
|
||||
freeRetries: 5,
|
||||
minWait: 5 * 60 * 1000, // 5 minutes
|
||||
maxWait: 60 * 60 * 1000, // 1 hour
|
||||
failCallback: (req, res, next) => {
|
||||
res.status(401).send('Too many failed attempts. Try again later.')
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Login form
|
||||
*/
|
||||
router.get('/login', async (req, res, next) => {
|
||||
// -> Bypass Login
|
||||
if (WIKI.config.auth.autoLogin && !req.query.all) {
|
||||
const stg = await WIKI.db.authentication.query().orderBy('order').first()
|
||||
const stgInfo = _.find(WIKI.data.authentication, ['key', stg.strategyKey])
|
||||
if (!stgInfo.useForm) {
|
||||
return res.redirect(`/login/${stg.key}`)
|
||||
}
|
||||
}
|
||||
// -> Show Login
|
||||
res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
|
||||
})
|
||||
|
||||
/**
|
||||
* Social Strategies Login
|
||||
*/
|
||||
router.get('/login/:strategy', async (req, res, next) => {
|
||||
try {
|
||||
await WIKI.db.users.login({
|
||||
strategy: req.params.strategy
|
||||
}, { req, res })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Social Strategies Callback
|
||||
*/
|
||||
router.all('/login/:strategy/callback', async (req, res, next) => {
|
||||
if (req.method !== 'GET' && req.method !== 'POST') { return next() }
|
||||
|
||||
try {
|
||||
const authResult = await WIKI.db.users.login({
|
||||
strategy: req.params.strategy
|
||||
}, { req, res })
|
||||
res.cookie('jwt', authResult.jwt, { expires: moment().add(1, 'y').toDate() })
|
||||
|
||||
const loginRedirect = req.cookies['loginRedirect']
|
||||
if (loginRedirect === '/' && authResult.redirect) {
|
||||
res.clearCookie('loginRedirect')
|
||||
res.redirect(authResult.redirect)
|
||||
} else if (loginRedirect) {
|
||||
res.clearCookie('loginRedirect')
|
||||
res.redirect(loginRedirect)
|
||||
} else if (authResult.redirect) {
|
||||
res.redirect(authResult.redirect)
|
||||
} else {
|
||||
res.redirect('/')
|
||||
}
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Logout
|
||||
*/
|
||||
router.get('/logout', async (req, res, next) => {
|
||||
const redirURL = await WIKI.db.users.logout({ req, res })
|
||||
req.logout((err) => {
|
||||
if (err) { return next(err) }
|
||||
res.clearCookie('jwt')
|
||||
res.redirect(redirURL)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Register form
|
||||
*/
|
||||
router.get('/register', async (req, res, next) => {
|
||||
_.set(res.locals, 'pageMeta.title', 'Register')
|
||||
const localStrg = await WIKI.db.authentication.getStrategy('local')
|
||||
if (localStrg.selfRegistration) {
|
||||
res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
|
||||
} else {
|
||||
next(new WIKI.Error.AuthRegistrationDisabled())
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Verify
|
||||
*/
|
||||
router.get('/verify/:token', bruteforce.prevent, async (req, res, next) => {
|
||||
try {
|
||||
const usr = await WIKI.db.userKeys.validateToken({ kind: 'verify', token: req.params.token })
|
||||
await WIKI.db.users.query().patch({ isVerified: true }).where('id', usr.id)
|
||||
req.brute.reset()
|
||||
if (WIKI.config.auth.enforce2FA) {
|
||||
res.redirect('/login')
|
||||
} else {
|
||||
const result = await WIKI.db.users.refreshToken(usr)
|
||||
res.cookie('jwt', result.token, { expires: moment().add(1, 'years').toDate() })
|
||||
res.redirect('/')
|
||||
}
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Reset Password
|
||||
*/
|
||||
router.get('/login-reset/:token', bruteforce.prevent, async (req, res, next) => {
|
||||
try {
|
||||
const usr = await WIKI.db.userKeys.validateToken({ kind: 'resetPwd', token: req.params.token })
|
||||
if (!usr) {
|
||||
throw new Error('Invalid Token')
|
||||
}
|
||||
req.brute.reset()
|
||||
|
||||
const changePwdContinuationToken = await WIKI.db.userKeys.generateToken({
|
||||
userId: usr.id,
|
||||
kind: 'changePwd'
|
||||
})
|
||||
const bgUrl = !_.isEmpty(WIKI.config.auth.loginBgUrl) ? WIKI.config.auth.loginBgUrl : '/_assets/img/splash/1.jpg'
|
||||
res.render('login', { bgUrl, hideLocal: WIKI.config.auth.hideLocal, changePwdContinuationToken })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* JWT Public Endpoints
|
||||
*/
|
||||
router.get('/.well-known/jwk.json', function (req, res, next) {
|
||||
res.json(WIKI.config.certs.jwk)
|
||||
})
|
||||
router.get('/.well-known/jwk.pem', function (req, res, next) {
|
||||
res.send(WIKI.config.certs.public)
|
||||
})
|
||||
|
||||
module.exports = router
|
@ -0,0 +1,161 @@
|
||||
/* global WIKI */
|
||||
|
||||
import express from 'express'
|
||||
import ExpressBrute from 'express-brute'
|
||||
import BruteKnex from '../helpers/brute-knex.mjs'
|
||||
import { find, isEmpty, set } from 'lodash-es'
|
||||
import path from 'node:path'
|
||||
import { DateTime } from 'luxon'
|
||||
|
||||
export default function () {
|
||||
const router = express.Router()
|
||||
|
||||
const bruteforce = new ExpressBrute(new BruteKnex({
|
||||
createTable: true,
|
||||
knex: WIKI.db.knex
|
||||
}), {
|
||||
freeRetries: 5,
|
||||
minWait: 5 * 60 * 1000, // 5 minutes
|
||||
maxWait: 60 * 60 * 1000, // 1 hour
|
||||
failCallback: (req, res, next) => {
|
||||
res.status(401).send('Too many failed attempts. Try again later.')
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Login form
|
||||
*/
|
||||
router.get('/login', async (req, res, next) => {
|
||||
// -> Bypass Login
|
||||
if (WIKI.config.auth.autoLogin && !req.query.all) {
|
||||
const stg = await WIKI.db.authentication.query().orderBy('order').first()
|
||||
const stgInfo = find(WIKI.data.authentication, ['key', stg.strategyKey])
|
||||
if (!stgInfo.useForm) {
|
||||
return res.redirect(`/login/${stg.key}`)
|
||||
}
|
||||
}
|
||||
// -> Show Login
|
||||
res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
|
||||
})
|
||||
|
||||
/**
|
||||
* Social Strategies Login
|
||||
*/
|
||||
router.get('/login/:strategy', async (req, res, next) => {
|
||||
try {
|
||||
await WIKI.db.users.login({
|
||||
strategy: req.params.strategy
|
||||
}, { req, res })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Social Strategies Callback
|
||||
*/
|
||||
router.all('/login/:strategy/callback', async (req, res, next) => {
|
||||
if (req.method !== 'GET' && req.method !== 'POST') { return next() }
|
||||
|
||||
try {
|
||||
const authResult = await WIKI.db.users.login({
|
||||
strategy: req.params.strategy
|
||||
}, { req, res })
|
||||
res.cookie('jwt', authResult.jwt, { expires: DateTime.now().plus({ years: 1 }).toJSDate() })
|
||||
|
||||
const loginRedirect = req.cookies['loginRedirect']
|
||||
if (loginRedirect === '/' && authResult.redirect) {
|
||||
res.clearCookie('loginRedirect')
|
||||
res.redirect(authResult.redirect)
|
||||
} else if (loginRedirect) {
|
||||
res.clearCookie('loginRedirect')
|
||||
res.redirect(loginRedirect)
|
||||
} else if (authResult.redirect) {
|
||||
res.redirect(authResult.redirect)
|
||||
} else {
|
||||
res.redirect('/')
|
||||
}
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Logout
|
||||
*/
|
||||
router.get('/logout', async (req, res, next) => {
|
||||
const redirURL = await WIKI.db.users.logout({ req, res })
|
||||
req.logout((err) => {
|
||||
if (err) { return next(err) }
|
||||
res.clearCookie('jwt')
|
||||
res.redirect(redirURL)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Register form
|
||||
*/
|
||||
router.get('/register', async (req, res, next) => {
|
||||
set(res.locals, 'pageMeta.title', 'Register')
|
||||
const localStrg = await WIKI.db.authentication.getStrategy('local')
|
||||
if (localStrg.selfRegistration) {
|
||||
res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
|
||||
} else {
|
||||
next(new WIKI.Error.AuthRegistrationDisabled())
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Verify
|
||||
*/
|
||||
router.get('/verify/:token', bruteforce.prevent, async (req, res, next) => {
|
||||
try {
|
||||
const usr = await WIKI.db.userKeys.validateToken({ kind: 'verify', token: req.params.token })
|
||||
await WIKI.db.users.query().patch({ isVerified: true }).where('id', usr.id)
|
||||
req.brute.reset()
|
||||
if (WIKI.config.auth.enforce2FA) {
|
||||
res.redirect('/login')
|
||||
} else {
|
||||
const result = await WIKI.db.users.refreshToken(usr)
|
||||
res.cookie('jwt', result.token, { expires: DateTime.now().plus({ years: 1 }).toJSDate() })
|
||||
res.redirect('/')
|
||||
}
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Reset Password
|
||||
*/
|
||||
router.get('/login-reset/:token', bruteforce.prevent, async (req, res, next) => {
|
||||
try {
|
||||
const usr = await WIKI.db.userKeys.validateToken({ kind: 'resetPwd', token: req.params.token })
|
||||
if (!usr) {
|
||||
throw new Error('Invalid Token')
|
||||
}
|
||||
req.brute.reset()
|
||||
|
||||
const changePwdContinuationToken = await WIKI.db.userKeys.generateToken({
|
||||
userId: usr.id,
|
||||
kind: 'changePwd'
|
||||
})
|
||||
const bgUrl = !isEmpty(WIKI.config.auth.loginBgUrl) ? WIKI.config.auth.loginBgUrl : '/_assets/img/splash/1.jpg'
|
||||
res.render('login', { bgUrl, hideLocal: WIKI.config.auth.hideLocal, changePwdContinuationToken })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* JWT Public Endpoints
|
||||
*/
|
||||
router.get('/.well-known/jwk.json', function (req, res, next) {
|
||||
res.json(WIKI.config.certs.jwk)
|
||||
})
|
||||
router.get('/.well-known/jwk.pem', function (req, res, next) {
|
||||
res.send(WIKI.config.certs.public)
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
@ -1,533 +0,0 @@
|
||||
const express = require('express')
|
||||
const router = express.Router()
|
||||
const pageHelper = require('../helpers/page')
|
||||
const _ = require('lodash')
|
||||
const CleanCSS = require('clean-css')
|
||||
const moment = require('moment')
|
||||
const path = require('path')
|
||||
const siteAssetsPath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'assets')
|
||||
|
||||
/**
|
||||
* Robots.txt
|
||||
*/
|
||||
router.get('/robots.txt', (req, res, next) => {
|
||||
res.type('text/plain')
|
||||
if (_.includes(WIKI.config.seo.robots, 'noindex')) {
|
||||
res.send('User-agent: *\nDisallow: /')
|
||||
} else {
|
||||
res.status(200).end()
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Health Endpoint
|
||||
*/
|
||||
router.get('/healthz', (req, res, next) => {
|
||||
if (WIKI.db.knex.client.pool.numFree() < 1 && WIKI.db.knex.client.pool.numUsed() < 1) {
|
||||
res.status(503).json({ ok: false }).end()
|
||||
} else {
|
||||
res.status(200).json({ ok: true }).end()
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Site Asset
|
||||
*/
|
||||
router.get('/_site/:siteId?/:resource', async (req, res, next) => {
|
||||
const site = req.params.siteId ? WIKI.sites[req.params.siteId] : await WIKI.db.sites.getSiteByHostname({ hostname: req.hostname })
|
||||
if (!site) {
|
||||
return res.status(404).send('Site Not Found')
|
||||
}
|
||||
switch (req.params.resource) {
|
||||
case 'logo': {
|
||||
if (site.config.assets.logo) {
|
||||
// TODO: Fetch from db if not in disk cache
|
||||
res.sendFile(path.join(siteAssetsPath, `logo-${site.id}.${site.config.assets.logoExt}`))
|
||||
} else {
|
||||
res.sendFile(path.join(WIKI.ROOTPATH, 'assets/_assets/logo-wikijs.svg'))
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'favicon': {
|
||||
if (site.config.assets.favicon) {
|
||||
// TODO: Fetch from db if not in disk cache
|
||||
res.sendFile(path.join(siteAssetsPath, `favicon-${site.id}.${site.config.assets.faviconExt}`))
|
||||
} else {
|
||||
res.sendFile(path.join(WIKI.ROOTPATH, 'assets/_assets/logo-wikijs.svg'))
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'loginbg': {
|
||||
if (site.config.assets.loginBg) {
|
||||
// TODO: Fetch from db if not in disk cache
|
||||
res.sendFile(path.join(siteAssetsPath, `loginbg-${site.id}.jpg`))
|
||||
} else {
|
||||
res.sendFile(path.join(WIKI.ROOTPATH, 'assets/_assets/bg/login.jpg'))
|
||||
}
|
||||
break
|
||||
}
|
||||
default: {
|
||||
return res.status(404).send('Invalid Site Resource')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Asset Thumbnails / Download
|
||||
*/
|
||||
router.get('/_thumb/:id.webp', async (req, res, next) => {
|
||||
const thumb = await WIKI.db.assets.getThumbnail({
|
||||
id: req.params.id
|
||||
})
|
||||
|
||||
if (thumb) {
|
||||
// TODO: Check permissions
|
||||
|
||||
switch (thumb.previewState) {
|
||||
case 'pending': {
|
||||
res.redirect('/_assets/illustrations/fileman-pending.svg')
|
||||
break
|
||||
}
|
||||
case 'ready': {
|
||||
res.set('Content-Type', 'image/webp')
|
||||
res.send(thumb.preview)
|
||||
break
|
||||
}
|
||||
case 'failed': {
|
||||
res.redirect('/_assets/illustrations/fileman-failed.svg')
|
||||
break
|
||||
}
|
||||
default: {
|
||||
return res.status(500).send('Invalid Thumbnail Preview State')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
})
|
||||
|
||||
// router.get(['/_admin', '/_admin/*'], (req, res, next) => {
|
||||
// if (!WIKI.auth.checkAccess(req.user, [
|
||||
// 'manage:system',
|
||||
// 'write:users',
|
||||
// 'manage:users',
|
||||
// 'write:groups',
|
||||
// 'manage:groups',
|
||||
// 'manage:navigation',
|
||||
// 'manage:theme',
|
||||
// 'manage:api'
|
||||
// ])) {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
|
||||
// return res.status(403).render('unauthorized', { action: 'view' })
|
||||
// }
|
||||
|
||||
// _.set(res.locals, 'pageMeta.title', 'Admin')
|
||||
// res.render('admin')
|
||||
|
||||
// })
|
||||
|
||||
// /**
|
||||
// * Download Page / Version
|
||||
// */
|
||||
// router.get(['/d', '/d/*'], async (req, res, next) => {
|
||||
// const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
|
||||
|
||||
// const versionId = (req.query.v) ? _.toSafeInteger(req.query.v) : 0
|
||||
|
||||
// const page = await WIKI.db.pages.getPageFromDb({
|
||||
// path: pageArgs.path,
|
||||
// locale: pageArgs.locale,
|
||||
// userId: req.user.id,
|
||||
// isPrivate: false
|
||||
// })
|
||||
|
||||
// pageArgs.tags = _.get(page, 'tags', [])
|
||||
|
||||
// if (versionId > 0) {
|
||||
// if (!WIKI.auth.checkAccess(req.user, ['read:history'], pageArgs)) {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
|
||||
// return res.render('unauthorized', { action: 'downloadVersion' })
|
||||
// }
|
||||
// } else {
|
||||
// if (!WIKI.auth.checkAccess(req.user, ['read:source'], pageArgs)) {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
|
||||
// return res.render('unauthorized', { action: 'download' })
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (page) {
|
||||
// const fileName = _.last(page.path.split('/')) + '.' + pageHelper.getFileExtension(page.contentType)
|
||||
// res.attachment(fileName)
|
||||
// if (versionId > 0) {
|
||||
// const pageVersion = await WIKI.db.pageHistory.getVersion({ pageId: page.id, versionId })
|
||||
// res.send(pageHelper.injectPageMetadata(pageVersion))
|
||||
// } else {
|
||||
// res.send(pageHelper.injectPageMetadata(page))
|
||||
// }
|
||||
// } else {
|
||||
// res.status(404).end()
|
||||
// }
|
||||
// })
|
||||
|
||||
// /**
|
||||
// * Create/Edit document
|
||||
// */
|
||||
// router.get(['/_edit', '/_edit/*'], async (req, res, next) => {
|
||||
// const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
|
||||
// const site = await WIKI.db.sites.getSiteByHostname({ hostname: req.hostname })
|
||||
|
||||
// if (!site) {
|
||||
// throw new Error('INVALID_SITE')
|
||||
// }
|
||||
|
||||
// if (pageArgs.path === '') {
|
||||
// return res.redirect(`/_edit/home`)
|
||||
// }
|
||||
|
||||
// // if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
|
||||
// // return res.redirect(`/_edit/${pageArgs.locale}/${pageArgs.path}`)
|
||||
// // }
|
||||
|
||||
// // req.i18n.changeLanguage(pageArgs.locale)
|
||||
|
||||
// // -> Set Editor Lang
|
||||
// _.set(res, 'locals.siteConfig.lang', pageArgs.locale)
|
||||
// // _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')
|
||||
|
||||
// // -> Check for reserved path
|
||||
// if (pageHelper.isReservedPath(pageArgs.path)) {
|
||||
// return next(new Error('Cannot create this page because it starts with a system reserved path.'))
|
||||
// }
|
||||
|
||||
// // -> Get page data from DB
|
||||
// let page = await WIKI.db.pages.getPageFromDb({
|
||||
// siteId: site.id,
|
||||
// path: pageArgs.path,
|
||||
// locale: pageArgs.locale,
|
||||
// userId: req.user.id
|
||||
// })
|
||||
|
||||
// pageArgs.tags = _.get(page, 'tags', [])
|
||||
|
||||
// // -> Effective Permissions
|
||||
// const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)
|
||||
|
||||
// const injectCode = {
|
||||
// css: '', // WIKI.config.theming.injectCSS,
|
||||
// head: '', // WIKI.config.theming.injectHead,
|
||||
// body: '' // WIKI.config.theming.injectBody
|
||||
// }
|
||||
|
||||
// if (page) {
|
||||
// // -> EDIT MODE
|
||||
// if (!(effectivePermissions.pages.write || effectivePermissions.pages.manage)) {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
|
||||
// return res.render('unauthorized', { action: 'edit' })
|
||||
// }
|
||||
|
||||
// // -> Get page tags
|
||||
// await page.$relatedQuery('tags')
|
||||
// page.tags = _.map(page.tags, 'tag')
|
||||
|
||||
// // Handle missing extra field
|
||||
// page.extra = page.extra || { css: '', js: '' }
|
||||
|
||||
// // -> Beautify Script CSS
|
||||
// if (!_.isEmpty(page.extra.css)) {
|
||||
// page.extra.css = new CleanCSS({ format: 'beautify' }).minify(page.extra.css).styles
|
||||
// }
|
||||
|
||||
// _.set(res.locals, 'pageMeta.title', `Edit ${page.title}`)
|
||||
// _.set(res.locals, 'pageMeta.description', page.description)
|
||||
// page.mode = 'update'
|
||||
// page.isPublished = (page.isPublished === true || page.isPublished === 1) ? 'true' : 'false'
|
||||
// page.content = Buffer.from(page.content).toString('base64')
|
||||
// } else {
|
||||
// // -> CREATE MODE
|
||||
// if (!effectivePermissions.pages.write) {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
|
||||
// return res.render('unauthorized', { action: 'create' })
|
||||
// }
|
||||
|
||||
// _.set(res.locals, 'pageMeta.title', `New Page`)
|
||||
// page = {
|
||||
// path: pageArgs.path,
|
||||
// localeCode: pageArgs.locale,
|
||||
// editorKey: null,
|
||||
// mode: 'create',
|
||||
// content: null,
|
||||
// title: null,
|
||||
// description: null,
|
||||
// updatedAt: new Date().toISOString(),
|
||||
// extra: {
|
||||
// css: '',
|
||||
// js: ''
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// res.render('editor', { page, injectCode, effectivePermissions })
|
||||
// })
|
||||
|
||||
// /**
|
||||
// * History
|
||||
// */
|
||||
// router.get(['/h', '/h/*'], async (req, res, next) => {
|
||||
// const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
|
||||
|
||||
// if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
|
||||
// return res.redirect(`/h/${pageArgs.locale}/${pageArgs.path}`)
|
||||
// }
|
||||
|
||||
// req.i18n.changeLanguage(pageArgs.locale)
|
||||
|
||||
// _.set(res, 'locals.siteConfig.lang', pageArgs.locale)
|
||||
// _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')
|
||||
|
||||
// const page = await WIKI.db.pages.getPageFromDb({
|
||||
// path: pageArgs.path,
|
||||
// locale: pageArgs.locale,
|
||||
// userId: req.user.id,
|
||||
// isPrivate: false
|
||||
// })
|
||||
|
||||
// if (!page) {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Page Not Found')
|
||||
// return res.status(404).render('notfound', { action: 'history' })
|
||||
// }
|
||||
|
||||
// pageArgs.tags = _.get(page, 'tags', [])
|
||||
|
||||
// const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)
|
||||
|
||||
// if (!effectivePermissions.history.read) {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
|
||||
// return res.render('unauthorized', { action: 'history' })
|
||||
// }
|
||||
|
||||
// if (page) {
|
||||
// _.set(res.locals, 'pageMeta.title', page.title)
|
||||
// _.set(res.locals, 'pageMeta.description', page.description)
|
||||
|
||||
// res.render('history', { page, effectivePermissions })
|
||||
// } else {
|
||||
// res.redirect(`/${pageArgs.path}`)
|
||||
// }
|
||||
// })
|
||||
|
||||
// /**
|
||||
// * Page ID redirection
|
||||
// */
|
||||
// router.get(['/i', '/i/:id'], async (req, res, next) => {
|
||||
// const pageId = _.toSafeInteger(req.params.id)
|
||||
// if (pageId <= 0) {
|
||||
// return res.redirect('/')
|
||||
// }
|
||||
|
||||
// const page = await WIKI.db.pages.query().column(['path', 'localeCode', 'isPrivate', 'privateNS']).findById(pageId)
|
||||
// if (!page) {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Page Not Found')
|
||||
// return res.status(404).render('notfound', { action: 'view' })
|
||||
// }
|
||||
|
||||
// if (!WIKI.auth.checkAccess(req.user, ['read:pages'], {
|
||||
// locale: page.localeCode,
|
||||
// path: page.path,
|
||||
// private: page.isPrivate,
|
||||
// privateNS: page.privateNS,
|
||||
// explicitLocale: false,
|
||||
// tags: page.tags
|
||||
// })) {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
|
||||
// return res.render('unauthorized', { action: 'view' })
|
||||
// }
|
||||
|
||||
// if (WIKI.config.lang.namespacing) {
|
||||
// return res.redirect(`/${page.localeCode}/${page.path}`)
|
||||
// } else {
|
||||
// return res.redirect(`/${page.path}`)
|
||||
// }
|
||||
// })
|
||||
|
||||
// /**
|
||||
// * Source
|
||||
// */
|
||||
// router.get(['/s', '/s/*'], async (req, res, next) => {
|
||||
// const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
|
||||
// const versionId = (req.query.v) ? _.toSafeInteger(req.query.v) : 0
|
||||
|
||||
// const page = await WIKI.db.pages.getPageFromDb({
|
||||
// path: pageArgs.path,
|
||||
// locale: pageArgs.locale,
|
||||
// userId: req.user.id,
|
||||
// isPrivate: false
|
||||
// })
|
||||
|
||||
// pageArgs.tags = _.get(page, 'tags', [])
|
||||
|
||||
// if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
|
||||
// return res.redirect(`/s/${pageArgs.locale}/${pageArgs.path}`)
|
||||
// }
|
||||
|
||||
// // -> Effective Permissions
|
||||
// const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)
|
||||
|
||||
// _.set(res, 'locals.siteConfig.lang', pageArgs.locale)
|
||||
// _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')
|
||||
|
||||
// if (versionId > 0) {
|
||||
// if (!effectivePermissions.history.read) {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
|
||||
// return res.render('unauthorized', { action: 'sourceVersion' })
|
||||
// }
|
||||
// } else {
|
||||
// if (!effectivePermissions.source.read) {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
|
||||
// return res.render('unauthorized', { action: 'source' })
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (page) {
|
||||
// if (versionId > 0) {
|
||||
// const pageVersion = await WIKI.db.pageHistory.getVersion({ pageId: page.id, versionId })
|
||||
// _.set(res.locals, 'pageMeta.title', pageVersion.title)
|
||||
// _.set(res.locals, 'pageMeta.description', pageVersion.description)
|
||||
// res.render('source', {
|
||||
// page: {
|
||||
// ...page,
|
||||
// ...pageVersion
|
||||
// },
|
||||
// effectivePermissions
|
||||
// })
|
||||
// } else {
|
||||
// _.set(res.locals, 'pageMeta.title', page.title)
|
||||
// _.set(res.locals, 'pageMeta.description', page.description)
|
||||
|
||||
// res.render('source', { page, effectivePermissions })
|
||||
// }
|
||||
// } else {
|
||||
// res.redirect(`/${pageArgs.path}`)
|
||||
// }
|
||||
// })
|
||||
|
||||
// /**
|
||||
// * Tags
|
||||
// */
|
||||
// router.get(['/t', '/t/*'], (req, res, next) => {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Tags')
|
||||
// res.render('tags')
|
||||
// })
|
||||
|
||||
/**
|
||||
* User Avatar
|
||||
*/
|
||||
router.get('/_user/:uid/avatar', async (req, res, next) => {
|
||||
if (!WIKI.auth.checkAccess(req.user, ['read:pages'])) {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
const av = await WIKI.db.users.getUserAvatarData(req.params.uid)
|
||||
if (av) {
|
||||
res.set('Content-Type', 'image/jpeg')
|
||||
return res.send(av)
|
||||
}
|
||||
|
||||
return res.sendStatus(404)
|
||||
})
|
||||
|
||||
// /**
|
||||
// * View document / asset
|
||||
// */
|
||||
// router.get('/*', async (req, res, next) => {
|
||||
// const stripExt = _.some(WIKI.data.pageExtensions, ext => _.endsWith(req.path, `.${ext}`))
|
||||
// const pageArgs = pageHelper.parsePath(req.path, { stripExt })
|
||||
// const isPage = (stripExt || pageArgs.path.indexOf('.') === -1)
|
||||
// const site = await WIKI.db.sites.getSiteByHostname({ hostname: req.hostname })
|
||||
|
||||
// if (!site) {
|
||||
// throw new Error('INVALID_SITE')
|
||||
// }
|
||||
|
||||
// if (isPage) {
|
||||
// // if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
|
||||
// // return res.redirect(`/${pageArgs.locale}/${pageArgs.path}`)
|
||||
// // }
|
||||
|
||||
// // req.i18n.changeLanguage(pageArgs.locale)
|
||||
|
||||
// try {
|
||||
// // -> Get Page from cache
|
||||
// const page = await WIKI.db.pages.getPage({
|
||||
// siteId: site.id,
|
||||
// path: pageArgs.path,
|
||||
// locale: pageArgs.locale,
|
||||
// userId: req.user.id
|
||||
// })
|
||||
// pageArgs.tags = _.get(page, 'tags', [])
|
||||
|
||||
// // -> Effective Permissions
|
||||
// const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)
|
||||
|
||||
// // -> Check User Access
|
||||
// if (!effectivePermissions.pages.read) {
|
||||
// if (req.user.id === WIKI.auth.guest.id) {
|
||||
// res.cookie('loginRedirect', req.path, {
|
||||
// maxAge: 15 * 60 * 1000
|
||||
// })
|
||||
// }
|
||||
// if (pageArgs.path === 'home' && req.user.id === WIKI.auth.guest.id) {
|
||||
// return res.redirect('/login')
|
||||
// }
|
||||
// return res.redirect(`/_error/unauthorized?from=${req.path}`)
|
||||
// }
|
||||
|
||||
// _.set(res, 'locals.siteConfig.lang', pageArgs.locale)
|
||||
// // _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')
|
||||
|
||||
// if (page) {
|
||||
// _.set(res.locals, 'pageMeta.title', page.title)
|
||||
// _.set(res.locals, 'pageMeta.description', page.description)
|
||||
|
||||
// // -> Check Publishing State
|
||||
// let pageIsPublished = page.isPublished
|
||||
// if (pageIsPublished && !_.isEmpty(page.publishStartDate)) {
|
||||
// pageIsPublished = moment(page.publishStartDate).isSameOrBefore()
|
||||
// }
|
||||
// if (pageIsPublished && !_.isEmpty(page.publishEndDate)) {
|
||||
// pageIsPublished = moment(page.publishEndDate).isSameOrAfter()
|
||||
// }
|
||||
// if (!pageIsPublished && !effectivePermissions.pages.write) {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
|
||||
// return res.status(403).render('unauthorized', {
|
||||
// action: 'view'
|
||||
// })
|
||||
// }
|
||||
|
||||
// // -> Render view
|
||||
// res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
|
||||
// } else if (pageArgs.path === 'home') {
|
||||
// res.redirect('/_welcome')
|
||||
// } else {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Page Not Found')
|
||||
// if (effectivePermissions.pages.write) {
|
||||
// res.status(404).render('new', { path: pageArgs.path, locale: pageArgs.locale })
|
||||
// } else {
|
||||
// res.status(404).render('notfound', { action: 'view' })
|
||||
// }
|
||||
// }
|
||||
// } catch (err) {
|
||||
// next(err)
|
||||
// }
|
||||
// } else {
|
||||
// if (!WIKI.auth.checkAccess(req.user, ['read:assets'], pageArgs)) {
|
||||
// return res.sendStatus(403)
|
||||
// }
|
||||
|
||||
// await WIKI.db.assets.getAsset(pageArgs.path, res)
|
||||
// }
|
||||
// })
|
||||
|
||||
router.get('/*', (req, res, next) => {
|
||||
res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
|
||||
})
|
||||
|
||||
module.exports = router
|
@ -0,0 +1,534 @@
|
||||
import express from 'express'
|
||||
// import pageHelper from '../helpers/page.mjs'
|
||||
// import CleanCSS from 'clean-css'
|
||||
import path from 'node:path'
|
||||
|
||||
export default function () {
|
||||
const router = express.Router()
|
||||
const siteAssetsPath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'assets')
|
||||
|
||||
/**
|
||||
* Robots.txt
|
||||
*/
|
||||
router.get('/robots.txt', (req, res, next) => {
|
||||
res.type('text/plain')
|
||||
if (WIKI.config.seo.robots.includes('noindex')) {
|
||||
res.send('User-agent: *\nDisallow: /')
|
||||
} else {
|
||||
res.status(200).end()
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Health Endpoint
|
||||
*/
|
||||
router.get('/healthz', (req, res, next) => {
|
||||
if (WIKI.db.knex.client.pool.numFree() < 1 && WIKI.db.knex.client.pool.numUsed() < 1) {
|
||||
res.status(503).json({ ok: false }).end()
|
||||
} else {
|
||||
res.status(200).json({ ok: true }).end()
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Site Asset
|
||||
*/
|
||||
router.get('/_site/:siteId?/:resource', async (req, res, next) => {
|
||||
const site = req.params.siteId ? WIKI.sites[req.params.siteId] : await WIKI.db.sites.getSiteByHostname({ hostname: req.hostname })
|
||||
if (!site) {
|
||||
return res.status(404).send('Site Not Found')
|
||||
}
|
||||
switch (req.params.resource) {
|
||||
case 'logo': {
|
||||
if (site.config.assets.logo) {
|
||||
// TODO: Fetch from db if not in disk cache
|
||||
res.sendFile(path.join(siteAssetsPath, `logo-${site.id}.${site.config.assets.logoExt}`))
|
||||
} else {
|
||||
res.sendFile(path.join(WIKI.ROOTPATH, 'assets/_assets/logo-wikijs.svg'))
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'favicon': {
|
||||
if (site.config.assets.favicon) {
|
||||
// TODO: Fetch from db if not in disk cache
|
||||
res.sendFile(path.join(siteAssetsPath, `favicon-${site.id}.${site.config.assets.faviconExt}`))
|
||||
} else {
|
||||
res.sendFile(path.join(WIKI.ROOTPATH, 'assets/_assets/logo-wikijs.svg'))
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'loginbg': {
|
||||
if (site.config.assets.loginBg) {
|
||||
// TODO: Fetch from db if not in disk cache
|
||||
res.sendFile(path.join(siteAssetsPath, `loginbg-${site.id}.jpg`))
|
||||
} else {
|
||||
res.sendFile(path.join(WIKI.ROOTPATH, 'assets/_assets/bg/login.jpg'))
|
||||
}
|
||||
break
|
||||
}
|
||||
default: {
|
||||
return res.status(404).send('Invalid Site Resource')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Asset Thumbnails / Download
|
||||
*/
|
||||
router.get('/_thumb/:id.webp', async (req, res, next) => {
|
||||
const thumb = await WIKI.db.assets.getThumbnail({
|
||||
id: req.params.id
|
||||
})
|
||||
|
||||
if (thumb) {
|
||||
// TODO: Check permissions
|
||||
|
||||
switch (thumb.previewState) {
|
||||
case 'pending': {
|
||||
res.redirect('/_assets/illustrations/fileman-pending.svg')
|
||||
break
|
||||
}
|
||||
case 'ready': {
|
||||
res.set('Content-Type', 'image/webp')
|
||||
res.send(thumb.preview)
|
||||
break
|
||||
}
|
||||
case 'failed': {
|
||||
res.redirect('/_assets/illustrations/fileman-failed.svg')
|
||||
break
|
||||
}
|
||||
default: {
|
||||
return res.status(500).send('Invalid Thumbnail Preview State')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
})
|
||||
|
||||
// router.get(['/_admin', '/_admin/*'], (req, res, next) => {
|
||||
// if (!WIKI.auth.checkAccess(req.user, [
|
||||
// 'manage:system',
|
||||
// 'write:users',
|
||||
// 'manage:users',
|
||||
// 'write:groups',
|
||||
// 'manage:groups',
|
||||
// 'manage:navigation',
|
||||
// 'manage:theme',
|
||||
// 'manage:api'
|
||||
// ])) {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
|
||||
// return res.status(403).render('unauthorized', { action: 'view' })
|
||||
// }
|
||||
|
||||
// _.set(res.locals, 'pageMeta.title', 'Admin')
|
||||
// res.render('admin')
|
||||
|
||||
// })
|
||||
|
||||
// /**
|
||||
// * Download Page / Version
|
||||
// */
|
||||
// router.get(['/d', '/d/*'], async (req, res, next) => {
|
||||
// const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
|
||||
|
||||
// const versionId = (req.query.v) ? _.toSafeInteger(req.query.v) : 0
|
||||
|
||||
// const page = await WIKI.db.pages.getPageFromDb({
|
||||
// path: pageArgs.path,
|
||||
// locale: pageArgs.locale,
|
||||
// userId: req.user.id,
|
||||
// isPrivate: false
|
||||
// })
|
||||
|
||||
// pageArgs.tags = _.get(page, 'tags', [])
|
||||
|
||||
// if (versionId > 0) {
|
||||
// if (!WIKI.auth.checkAccess(req.user, ['read:history'], pageArgs)) {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
|
||||
// return res.render('unauthorized', { action: 'downloadVersion' })
|
||||
// }
|
||||
// } else {
|
||||
// if (!WIKI.auth.checkAccess(req.user, ['read:source'], pageArgs)) {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
|
||||
// return res.render('unauthorized', { action: 'download' })
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (page) {
|
||||
// const fileName = _.last(page.path.split('/')) + '.' + pageHelper.getFileExtension(page.contentType)
|
||||
// res.attachment(fileName)
|
||||
// if (versionId > 0) {
|
||||
// const pageVersion = await WIKI.db.pageHistory.getVersion({ pageId: page.id, versionId })
|
||||
// res.send(pageHelper.injectPageMetadata(pageVersion))
|
||||
// } else {
|
||||
// res.send(pageHelper.injectPageMetadata(page))
|
||||
// }
|
||||
// } else {
|
||||
// res.status(404).end()
|
||||
// }
|
||||
// })
|
||||
|
||||
// /**
|
||||
// * Create/Edit document
|
||||
// */
|
||||
// router.get(['/_edit', '/_edit/*'], async (req, res, next) => {
|
||||
// const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
|
||||
// const site = await WIKI.db.sites.getSiteByHostname({ hostname: req.hostname })
|
||||
|
||||
// if (!site) {
|
||||
// throw new Error('INVALID_SITE')
|
||||
// }
|
||||
|
||||
// if (pageArgs.path === '') {
|
||||
// return res.redirect(`/_edit/home`)
|
||||
// }
|
||||
|
||||
// // if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
|
||||
// // return res.redirect(`/_edit/${pageArgs.locale}/${pageArgs.path}`)
|
||||
// // }
|
||||
|
||||
// // req.i18n.changeLanguage(pageArgs.locale)
|
||||
|
||||
// // -> Set Editor Lang
|
||||
// _.set(res, 'locals.siteConfig.lang', pageArgs.locale)
|
||||
// // _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')
|
||||
|
||||
// // -> Check for reserved path
|
||||
// if (pageHelper.isReservedPath(pageArgs.path)) {
|
||||
// return next(new Error('Cannot create this page because it starts with a system reserved path.'))
|
||||
// }
|
||||
|
||||
// // -> Get page data from DB
|
||||
// let page = await WIKI.db.pages.getPageFromDb({
|
||||
// siteId: site.id,
|
||||
// path: pageArgs.path,
|
||||
// locale: pageArgs.locale,
|
||||
// userId: req.user.id
|
||||
// })
|
||||
|
||||
// pageArgs.tags = _.get(page, 'tags', [])
|
||||
|
||||
// // -> Effective Permissions
|
||||
// const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)
|
||||
|
||||
// const injectCode = {
|
||||
// css: '', // WIKI.config.theming.injectCSS,
|
||||
// head: '', // WIKI.config.theming.injectHead,
|
||||
// body: '' // WIKI.config.theming.injectBody
|
||||
// }
|
||||
|
||||
// if (page) {
|
||||
// // -> EDIT MODE
|
||||
// if (!(effectivePermissions.pages.write || effectivePermissions.pages.manage)) {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
|
||||
// return res.render('unauthorized', { action: 'edit' })
|
||||
// }
|
||||
|
||||
// // -> Get page tags
|
||||
// await page.$relatedQuery('tags')
|
||||
// page.tags = _.map(page.tags, 'tag')
|
||||
|
||||
// // Handle missing extra field
|
||||
// page.extra = page.extra || { css: '', js: '' }
|
||||
|
||||
// // -> Beautify Script CSS
|
||||
// if (!_.isEmpty(page.extra.css)) {
|
||||
// page.extra.css = new CleanCSS({ format: 'beautify' }).minify(page.extra.css).styles
|
||||
// }
|
||||
|
||||
// _.set(res.locals, 'pageMeta.title', `Edit ${page.title}`)
|
||||
// _.set(res.locals, 'pageMeta.description', page.description)
|
||||
// page.mode = 'update'
|
||||
// page.isPublished = (page.isPublished === true || page.isPublished === 1) ? 'true' : 'false'
|
||||
// page.content = Buffer.from(page.content).toString('base64')
|
||||
// } else {
|
||||
// // -> CREATE MODE
|
||||
// if (!effectivePermissions.pages.write) {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
|
||||
// return res.render('unauthorized', { action: 'create' })
|
||||
// }
|
||||
|
||||
// _.set(res.locals, 'pageMeta.title', `New Page`)
|
||||
// page = {
|
||||
// path: pageArgs.path,
|
||||
// localeCode: pageArgs.locale,
|
||||
// editorKey: null,
|
||||
// mode: 'create',
|
||||
// content: null,
|
||||
// title: null,
|
||||
// description: null,
|
||||
// updatedAt: new Date().toISOString(),
|
||||
// extra: {
|
||||
// css: '',
|
||||
// js: ''
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// res.render('editor', { page, injectCode, effectivePermissions })
|
||||
// })
|
||||
|
||||
// /**
|
||||
// * History
|
||||
// */
|
||||
// router.get(['/h', '/h/*'], async (req, res, next) => {
|
||||
// const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
|
||||
|
||||
// if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
|
||||
// return res.redirect(`/h/${pageArgs.locale}/${pageArgs.path}`)
|
||||
// }
|
||||
|
||||
// req.i18n.changeLanguage(pageArgs.locale)
|
||||
|
||||
// _.set(res, 'locals.siteConfig.lang', pageArgs.locale)
|
||||
// _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')
|
||||
|
||||
// const page = await WIKI.db.pages.getPageFromDb({
|
||||
// path: pageArgs.path,
|
||||
// locale: pageArgs.locale,
|
||||
// userId: req.user.id,
|
||||
// isPrivate: false
|
||||
// })
|
||||
|
||||
// if (!page) {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Page Not Found')
|
||||
// return res.status(404).render('notfound', { action: 'history' })
|
||||
// }
|
||||
|
||||
// pageArgs.tags = _.get(page, 'tags', [])
|
||||
|
||||
// const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)
|
||||
|
||||
// if (!effectivePermissions.history.read) {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
|
||||
// return res.render('unauthorized', { action: 'history' })
|
||||
// }
|
||||
|
||||
// if (page) {
|
||||
// _.set(res.locals, 'pageMeta.title', page.title)
|
||||
// _.set(res.locals, 'pageMeta.description', page.description)
|
||||
|
||||
// res.render('history', { page, effectivePermissions })
|
||||
// } else {
|
||||
// res.redirect(`/${pageArgs.path}`)
|
||||
// }
|
||||
// })
|
||||
|
||||
// /**
|
||||
// * Page ID redirection
|
||||
// */
|
||||
// router.get(['/i', '/i/:id'], async (req, res, next) => {
|
||||
// const pageId = _.toSafeInteger(req.params.id)
|
||||
// if (pageId <= 0) {
|
||||
// return res.redirect('/')
|
||||
// }
|
||||
|
||||
// const page = await WIKI.db.pages.query().column(['path', 'localeCode', 'isPrivate', 'privateNS']).findById(pageId)
|
||||
// if (!page) {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Page Not Found')
|
||||
// return res.status(404).render('notfound', { action: 'view' })
|
||||
// }
|
||||
|
||||
// if (!WIKI.auth.checkAccess(req.user, ['read:pages'], {
|
||||
// locale: page.localeCode,
|
||||
// path: page.path,
|
||||
// private: page.isPrivate,
|
||||
// privateNS: page.privateNS,
|
||||
// explicitLocale: false,
|
||||
// tags: page.tags
|
||||
// })) {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
|
||||
// return res.render('unauthorized', { action: 'view' })
|
||||
// }
|
||||
|
||||
// if (WIKI.config.lang.namespacing) {
|
||||
// return res.redirect(`/${page.localeCode}/${page.path}`)
|
||||
// } else {
|
||||
// return res.redirect(`/${page.path}`)
|
||||
// }
|
||||
// })
|
||||
|
||||
// /**
|
||||
// * Source
|
||||
// */
|
||||
// router.get(['/s', '/s/*'], async (req, res, next) => {
|
||||
// const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
|
||||
// const versionId = (req.query.v) ? _.toSafeInteger(req.query.v) : 0
|
||||
|
||||
// const page = await WIKI.db.pages.getPageFromDb({
|
||||
// path: pageArgs.path,
|
||||
// locale: pageArgs.locale,
|
||||
// userId: req.user.id,
|
||||
// isPrivate: false
|
||||
// })
|
||||
|
||||
// pageArgs.tags = _.get(page, 'tags', [])
|
||||
|
||||
// if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
|
||||
// return res.redirect(`/s/${pageArgs.locale}/${pageArgs.path}`)
|
||||
// }
|
||||
|
||||
// // -> Effective Permissions
|
||||
// const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)
|
||||
|
||||
// _.set(res, 'locals.siteConfig.lang', pageArgs.locale)
|
||||
// _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')
|
||||
|
||||
// if (versionId > 0) {
|
||||
// if (!effectivePermissions.history.read) {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
|
||||
// return res.render('unauthorized', { action: 'sourceVersion' })
|
||||
// }
|
||||
// } else {
|
||||
// if (!effectivePermissions.source.read) {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
|
||||
// return res.render('unauthorized', { action: 'source' })
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (page) {
|
||||
// if (versionId > 0) {
|
||||
// const pageVersion = await WIKI.db.pageHistory.getVersion({ pageId: page.id, versionId })
|
||||
// _.set(res.locals, 'pageMeta.title', pageVersion.title)
|
||||
// _.set(res.locals, 'pageMeta.description', pageVersion.description)
|
||||
// res.render('source', {
|
||||
// page: {
|
||||
// ...page,
|
||||
// ...pageVersion
|
||||
// },
|
||||
// effectivePermissions
|
||||
// })
|
||||
// } else {
|
||||
// _.set(res.locals, 'pageMeta.title', page.title)
|
||||
// _.set(res.locals, 'pageMeta.description', page.description)
|
||||
|
||||
// res.render('source', { page, effectivePermissions })
|
||||
// }
|
||||
// } else {
|
||||
// res.redirect(`/${pageArgs.path}`)
|
||||
// }
|
||||
// })
|
||||
|
||||
// /**
|
||||
// * Tags
|
||||
// */
|
||||
// router.get(['/t', '/t/*'], (req, res, next) => {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Tags')
|
||||
// res.render('tags')
|
||||
// })
|
||||
|
||||
/**
|
||||
* User Avatar
|
||||
*/
|
||||
router.get('/_user/:uid/avatar', async (req, res, next) => {
|
||||
if (!WIKI.auth.checkAccess(req.user, ['read:pages'])) {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
const av = await WIKI.db.users.getUserAvatarData(req.params.uid)
|
||||
if (av) {
|
||||
res.set('Content-Type', 'image/jpeg')
|
||||
return res.send(av)
|
||||
}
|
||||
|
||||
return res.sendStatus(404)
|
||||
})
|
||||
|
||||
// /**
|
||||
// * View document / asset
|
||||
// */
|
||||
// router.get('/*', async (req, res, next) => {
|
||||
// const stripExt = _.some(WIKI.data.pageExtensions, ext => _.endsWith(req.path, `.${ext}`))
|
||||
// const pageArgs = pageHelper.parsePath(req.path, { stripExt })
|
||||
// const isPage = (stripExt || pageArgs.path.indexOf('.') === -1)
|
||||
// const site = await WIKI.db.sites.getSiteByHostname({ hostname: req.hostname })
|
||||
|
||||
// if (!site) {
|
||||
// throw new Error('INVALID_SITE')
|
||||
// }
|
||||
|
||||
// if (isPage) {
|
||||
// // if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
|
||||
// // return res.redirect(`/${pageArgs.locale}/${pageArgs.path}`)
|
||||
// // }
|
||||
|
||||
// // req.i18n.changeLanguage(pageArgs.locale)
|
||||
|
||||
// try {
|
||||
// // -> Get Page from cache
|
||||
// const page = await WIKI.db.pages.getPage({
|
||||
// siteId: site.id,
|
||||
// path: pageArgs.path,
|
||||
// locale: pageArgs.locale,
|
||||
// userId: req.user.id
|
||||
// })
|
||||
// pageArgs.tags = _.get(page, 'tags', [])
|
||||
|
||||
// // -> Effective Permissions
|
||||
// const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)
|
||||
|
||||
// // -> Check User Access
|
||||
// if (!effectivePermissions.pages.read) {
|
||||
// if (req.user.id === WIKI.auth.guest.id) {
|
||||
// res.cookie('loginRedirect', req.path, {
|
||||
// maxAge: 15 * 60 * 1000
|
||||
// })
|
||||
// }
|
||||
// if (pageArgs.path === 'home' && req.user.id === WIKI.auth.guest.id) {
|
||||
// return res.redirect('/login')
|
||||
// }
|
||||
// return res.redirect(`/_error/unauthorized?from=${req.path}`)
|
||||
// }
|
||||
|
||||
// _.set(res, 'locals.siteConfig.lang', pageArgs.locale)
|
||||
// // _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')
|
||||
|
||||
// if (page) {
|
||||
// _.set(res.locals, 'pageMeta.title', page.title)
|
||||
// _.set(res.locals, 'pageMeta.description', page.description)
|
||||
|
||||
// // -> Check Publishing State
|
||||
// let pageIsPublished = page.isPublished
|
||||
// if (pageIsPublished && !_.isEmpty(page.publishStartDate)) {
|
||||
// pageIsPublished = moment(page.publishStartDate).isSameOrBefore()
|
||||
// }
|
||||
// if (pageIsPublished && !_.isEmpty(page.publishEndDate)) {
|
||||
// pageIsPublished = moment(page.publishEndDate).isSameOrAfter()
|
||||
// }
|
||||
// if (!pageIsPublished && !effectivePermissions.pages.write) {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Unauthorized')
|
||||
// return res.status(403).render('unauthorized', {
|
||||
// action: 'view'
|
||||
// })
|
||||
// }
|
||||
|
||||
// // -> Render view
|
||||
// res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
|
||||
// } else if (pageArgs.path === 'home') {
|
||||
// res.redirect('/_welcome')
|
||||
// } else {
|
||||
// _.set(res.locals, 'pageMeta.title', 'Page Not Found')
|
||||
// if (effectivePermissions.pages.write) {
|
||||
// res.status(404).render('new', { path: pageArgs.path, locale: pageArgs.locale })
|
||||
// } else {
|
||||
// res.status(404).render('notfound', { action: 'view' })
|
||||
// }
|
||||
// }
|
||||
// } catch (err) {
|
||||
// next(err)
|
||||
// }
|
||||
// } else {
|
||||
// if (!WIKI.auth.checkAccess(req.user, ['read:assets'], pageArgs)) {
|
||||
// return res.sendStatus(403)
|
||||
// }
|
||||
|
||||
// await WIKI.db.assets.getAsset(pageArgs.path, res)
|
||||
// }
|
||||
// })
|
||||
|
||||
router.get('/*', (req, res, next) => {
|
||||
res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
const express = require('express')
|
||||
const router = express.Router()
|
||||
const _ = require('lodash')
|
||||
const qs = require('querystring')
|
||||
|
||||
/**
|
||||
* Let's Encrypt Challenge
|
||||
*/
|
||||
router.get('/.well-known/acme-challenge/:token', (req, res, next) => {
|
||||
res.type('text/plain')
|
||||
if (_.get(WIKI.config, 'letsencrypt.challenge', false)) {
|
||||
if (WIKI.config.letsencrypt.challenge.token === req.params.token) {
|
||||
res.send(WIKI.config.letsencrypt.challenge.keyAuthorization)
|
||||
WIKI.logger.info(`(LETSENCRYPT) Received valid challenge request. [ ACCEPTED ]`)
|
||||
} else {
|
||||
res.status(406).send('Invalid Challenge Token!')
|
||||
WIKI.logger.warn(`(LETSENCRYPT) Received invalid challenge request. [ REJECTED ]`)
|
||||
}
|
||||
} else {
|
||||
res.status(418).end()
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Redirect to HTTPS if HTTP Redirection is enabled
|
||||
*/
|
||||
// router.all('/*', (req, res, next) => {
|
||||
// if (WIKI.config.server.sslRedir && !req.secure && WIKI.servers.servers.https) {
|
||||
// let query = (!_.isEmpty(req.query)) ? `?${qs.stringify(req.query)}` : ``
|
||||
// return res.redirect(`https://${req.hostname}${req.originalUrl}${query}`)
|
||||
// } else {
|
||||
// next()
|
||||
// }
|
||||
// })
|
||||
|
||||
module.exports = router
|
@ -0,0 +1,39 @@
|
||||
import express from 'express'
|
||||
import { get } from 'lodash-es'
|
||||
import qs from 'querystring'
|
||||
|
||||
export default function () {
|
||||
const router = express.Router()
|
||||
|
||||
/**
|
||||
* Let's Encrypt Challenge
|
||||
*/
|
||||
router.get('/.well-known/acme-challenge/:token', (req, res, next) => {
|
||||
res.type('text/plain')
|
||||
if (get(WIKI.config, 'letsencrypt.challenge', false)) {
|
||||
if (WIKI.config.letsencrypt.challenge.token === req.params.token) {
|
||||
res.send(WIKI.config.letsencrypt.challenge.keyAuthorization)
|
||||
WIKI.logger.info(`(LETSENCRYPT) Received valid challenge request. [ ACCEPTED ]`)
|
||||
} else {
|
||||
res.status(406).send('Invalid Challenge Token!')
|
||||
WIKI.logger.warn(`(LETSENCRYPT) Received invalid challenge request. [ REJECTED ]`)
|
||||
}
|
||||
} else {
|
||||
res.status(418).end()
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Redirect to HTTPS if HTTP Redirection is enabled
|
||||
*/
|
||||
// router.all('/*', (req, res, next) => {
|
||||
// if (WIKI.config.server.sslRedir && !req.secure && WIKI.servers.servers.https) {
|
||||
// let query = (!_.isEmpty(req.query)) ? `?${qs.stringify(req.query)}` : ``
|
||||
// return res.redirect(`https://${req.hostname}${req.originalUrl}${query}`)
|
||||
// } else {
|
||||
// next()
|
||||
// }
|
||||
// })
|
||||
|
||||
return router
|
||||
}
|
@ -1,105 +0,0 @@
|
||||
const express = require('express')
|
||||
const router = express.Router()
|
||||
const _ = require('lodash')
|
||||
const multer = require('multer')
|
||||
const path = require('path')
|
||||
const sanitize = require('sanitize-filename')
|
||||
|
||||
/**
|
||||
* Upload files
|
||||
*/
|
||||
router.post('/u', (req, res, next) => {
|
||||
multer({
|
||||
dest: path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'uploads'),
|
||||
limits: {
|
||||
fileSize: WIKI.config.uploads.maxFileSize,
|
||||
files: WIKI.config.uploads.maxFiles
|
||||
}
|
||||
}).array('mediaUpload')(req, res, next)
|
||||
}, async (req, res, next) => {
|
||||
if (!_.some(req.user.permissions, pm => _.includes(['write:assets', 'manage:system'], pm))) {
|
||||
return res.status(403).json({
|
||||
succeeded: false,
|
||||
message: 'You are not authorized to upload files.'
|
||||
})
|
||||
} else if (req.files.length < 1) {
|
||||
return res.status(400).json({
|
||||
succeeded: false,
|
||||
message: 'Missing upload payload.'
|
||||
})
|
||||
} else if (req.files.length > 1) {
|
||||
return res.status(400).json({
|
||||
succeeded: false,
|
||||
message: 'You cannot upload multiple files within the same request.'
|
||||
})
|
||||
}
|
||||
const fileMeta = _.get(req, 'files[0]', false)
|
||||
if (!fileMeta) {
|
||||
return res.status(500).json({
|
||||
succeeded: false,
|
||||
message: 'Missing upload file metadata.'
|
||||
})
|
||||
}
|
||||
|
||||
// Get folder Id
|
||||
let folderId = null
|
||||
try {
|
||||
const folderRaw = _.get(req, 'body.mediaUpload', false)
|
||||
if (folderRaw) {
|
||||
folderId = _.get(JSON.parse(folderRaw), 'folderId', null)
|
||||
if (folderId === 0) {
|
||||
folderId = null
|
||||
}
|
||||
} else {
|
||||
throw new Error('Missing File Metadata')
|
||||
}
|
||||
} catch (err) {
|
||||
return res.status(400).json({
|
||||
succeeded: false,
|
||||
message: 'Missing upload folder metadata.'
|
||||
})
|
||||
}
|
||||
|
||||
// Build folder hierarchy
|
||||
let hierarchy = []
|
||||
if (folderId) {
|
||||
try {
|
||||
hierarchy = await WIKI.db.assetFolders.getHierarchy(folderId)
|
||||
} catch (err) {
|
||||
return res.status(400).json({
|
||||
succeeded: false,
|
||||
message: 'Failed to fetch folder hierarchy.'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize filename
|
||||
fileMeta.originalname = sanitize(fileMeta.originalname.toLowerCase().replace(/[\s,;#]+/g, '_'))
|
||||
|
||||
// Check if user can upload at path
|
||||
const assetPath = (folderId) ? hierarchy.map(h => h.slug).join('/') + `/${fileMeta.originalname}` : fileMeta.originalname
|
||||
if (!WIKI.auth.checkAccess(req.user, ['write:assets'], { path: assetPath })) {
|
||||
return res.status(403).json({
|
||||
succeeded: false,
|
||||
message: 'You are not authorized to upload files to this folder.'
|
||||
})
|
||||
}
|
||||
|
||||
// Process upload file
|
||||
await WIKI.db.assets.upload({
|
||||
...fileMeta,
|
||||
mode: 'upload',
|
||||
folderId: folderId,
|
||||
assetPath,
|
||||
user: req.user
|
||||
})
|
||||
res.send('ok')
|
||||
})
|
||||
|
||||
router.get('/u', async (req, res, next) => {
|
||||
res.json({
|
||||
ok: true
|
||||
})
|
||||
})
|
||||
|
||||
module.exports = router
|
@ -1,16 +1,16 @@
|
||||
const chalk = require('chalk')
|
||||
const os = require('node:os')
|
||||
import chalk from 'chalk'
|
||||
import os from 'node:os'
|
||||
|
||||
module.exports = () => {
|
||||
export default function () {
|
||||
WIKI.servers.ws.on('connection', (socket) => {
|
||||
// TODO: Validate token + permissions
|
||||
const token = socket.handshake.auth.token
|
||||
console.info(token)
|
||||
// console.info(token)
|
||||
|
||||
const listeners = {}
|
||||
|
||||
socket.on('server:logs', () => {
|
||||
socket.emit('server:log', chalk`{greenBright Streaming logs from {bold Wiki.js} instance {yellowBright.bold ${WIKI.INSTANCE_ID}} on host {yellowBright.bold ${os.hostname()}}...}`)
|
||||
socket.emit('server:log', chalk.greenBright(`Streaming logs from ${chalk.bold('Wiki.js')} instance ${chalk.yellowBright.bold(WIKI.INSTANCE_ID)} on host ${chalk.yellowBright.bold(os.hostname())}...`))
|
||||
listeners.serverLogs = (msg) => {
|
||||
socket.emit('server:log', msg)
|
||||
}
|
@ -1,20 +1,19 @@
|
||||
const pickle = require('chromium-pickle-js')
|
||||
const path = require('path')
|
||||
const UINT64 = require('cuint').UINT64
|
||||
const fs = require('fs')
|
||||
import pickle from 'chromium-pickle-js'
|
||||
import path from 'node:path'
|
||||
import { UINT64 } from 'cuint'
|
||||
import fs from 'node:fs'
|
||||
|
||||
/**
|
||||
* Based of express-serve-asar (https://github.com/toyobayashi/express-serve-asar)
|
||||
* by Fenglin Li (https://github.com/toyobayashi)
|
||||
*/
|
||||
|
||||
const packages = {
|
||||
'twemoji': path.join(WIKI.ROOTPATH, `assets-legacy/svg/twemoji.asar`)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
fdCache: {},
|
||||
async serve (pkgName, req, res, next) {
|
||||
const packages = {
|
||||
'twemoji': path.join(WIKI.ROOTPATH, `assets/svg/twemoji.asar`)
|
||||
}
|
||||
const file = this.readFilesystemSync(packages[pkgName])
|
||||
const { filesystem, fd } = file
|
||||
const info = filesystem.getFile(req.path.substring(1))
|
@ -1,7 +0,0 @@
|
||||
const NodeCache = require('node-cache')
|
||||
|
||||
module.exports = {
|
||||
init() {
|
||||
return new NodeCache()
|
||||
}
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
ext: {},
|
||||
async init () {
|
||||
const extDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/extensions'))
|
||||
WIKI.logger.info(`Checking for installed optional extensions...`)
|
||||
for (let dir of extDirs) {
|
||||
WIKI.extensions.ext[dir] = require(path.join(WIKI.SERVERPATH, 'modules/extensions', dir, 'ext.js'))
|
||||
for (const dir of extDirs) {
|
||||
WIKI.extensions.ext[dir] = (await import(path.join(WIKI.SERVERPATH, 'modules/extensions', dir, 'ext.mjs'))).default
|
||||
const isInstalled = await WIKI.extensions.ext[dir].check()
|
||||
if (isInstalled) {
|
||||
WIKI.logger.info(`Optional extension ${dir} is installed. [ OK ]`)
|
@ -1,60 +0,0 @@
|
||||
const chalk = require('chalk')
|
||||
const EventEmitter = require('events')
|
||||
|
||||
const LEVELS = ['error', 'warn', 'info', 'debug']
|
||||
const LEVELSIGNORED = ['verbose', 'silly']
|
||||
const LEVELCOLORS = {
|
||||
error: 'red',
|
||||
warn: 'yellow',
|
||||
info: 'green',
|
||||
debug: 'cyan'
|
||||
}
|
||||
|
||||
class Logger extends EventEmitter {}
|
||||
const primaryLogger = new Logger()
|
||||
|
||||
let ignoreNextLevels = false
|
||||
|
||||
primaryLogger.ws = new EventEmitter()
|
||||
|
||||
LEVELS.forEach(lvl => {
|
||||
primaryLogger[lvl] = (...args) => {
|
||||
primaryLogger.emit(lvl, ...args)
|
||||
}
|
||||
|
||||
if (!ignoreNextLevels) {
|
||||
primaryLogger.on(lvl, (msg) => {
|
||||
let formatted = ''
|
||||
if (WIKI.config.logFormat === 'json') {
|
||||
formatted = JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
instance: WIKI.INSTANCE_ID,
|
||||
level: lvl,
|
||||
message: msg
|
||||
})
|
||||
} else {
|
||||
if (msg instanceof Error) {
|
||||
msg = msg.stack
|
||||
}
|
||||
formatted = chalk`${new Date().toISOString()} {dim [${WIKI.INSTANCE_ID}]} {${LEVELCOLORS[lvl]}.bold ${lvl}}: ${msg}`
|
||||
}
|
||||
|
||||
console.log(formatted)
|
||||
primaryLogger.ws.emit('log', formatted)
|
||||
})
|
||||
}
|
||||
if (lvl === WIKI.config.logLevel) {
|
||||
ignoreNextLevels = true
|
||||
}
|
||||
})
|
||||
|
||||
LEVELSIGNORED.forEach(lvl => {
|
||||
primaryLogger[lvl] = () => {}
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
loggers: {},
|
||||
init () {
|
||||
return primaryLogger
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
import chalk from 'chalk'
|
||||
import EventEmitter from 'node:events'
|
||||
|
||||
const LEVELS = ['error', 'warn', 'info', 'debug']
|
||||
const LEVELSIGNORED = ['verbose', 'silly']
|
||||
const LEVELCOLORS = {
|
||||
error: 'red',
|
||||
warn: 'yellow',
|
||||
info: 'green',
|
||||
debug: 'cyan'
|
||||
}
|
||||
|
||||
class Logger extends EventEmitter {}
|
||||
|
||||
export default {
|
||||
loggers: {},
|
||||
init () {
|
||||
const primaryLogger = new Logger()
|
||||
|
||||
let ignoreNextLevels = false
|
||||
|
||||
primaryLogger.ws = new EventEmitter()
|
||||
|
||||
LEVELS.forEach(lvl => {
|
||||
primaryLogger[lvl] = (...args) => {
|
||||
primaryLogger.emit(lvl, ...args)
|
||||
}
|
||||
|
||||
if (!ignoreNextLevels) {
|
||||
primaryLogger.on(lvl, (msg) => {
|
||||
let formatted = ''
|
||||
if (WIKI.config.logFormat === 'json') {
|
||||
formatted = JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
instance: WIKI.INSTANCE_ID,
|
||||
level: lvl,
|
||||
message: msg
|
||||
})
|
||||
} else {
|
||||
if (msg instanceof Error) {
|
||||
msg = msg.stack
|
||||
}
|
||||
formatted = `${new Date().toISOString()} ${chalk.dim('[' + WIKI.INSTANCE_ID + ']')} ${chalk[LEVELCOLORS[lvl]].bold(lvl)}: ${msg}`
|
||||
}
|
||||
|
||||
console.log(formatted)
|
||||
primaryLogger.ws.emit('log', formatted)
|
||||
})
|
||||
}
|
||||
if (lvl === WIKI.config.logLevel) {
|
||||
ignoreNextLevels = true
|
||||
}
|
||||
})
|
||||
|
||||
LEVELSIGNORED.forEach(lvl => {
|
||||
primaryLogger[lvl] = () => {}
|
||||
})
|
||||
|
||||
return primaryLogger
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
|
||||
module.exports = {
|
||||
updates: {
|
||||
channel: 'BETA',
|
||||
version: WIKI.version,
|
||||
releaseDate: WIKI.releaseDate,
|
||||
minimumVersionRequired: '3.0.0-beta.0',
|
||||
minimumNodeRequired: '18.0.0'
|
||||
},
|
||||
init () {
|
||||
fs.ensureDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'assets'))
|
||||
fs.ensureDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'uploads'))
|
||||
|
||||
// Clear content cache
|
||||
fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'cache'))
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import fse from 'fs-extra'
|
||||
import path from 'node:path'
|
||||
|
||||
export default {
|
||||
updates: {
|
||||
channel: 'BETA',
|
||||
version: WIKI.version,
|
||||
releaseDate: WIKI.releaseDate,
|
||||
minimumVersionRequired: '3.0.0-beta.0',
|
||||
minimumNodeRequired: '18.0.0'
|
||||
},
|
||||
init () {
|
||||
fse.ensureDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'assets'))
|
||||
fse.ensureDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'uploads'))
|
||||
|
||||
// Clear content cache
|
||||
fse.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'cache'))
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
const path = require('path')
|
||||
const fs = require('fs-extra')
|
||||
const semver = require('semver')
|
||||
|
||||
const baseMigrationPath = path.join(WIKI.SERVERPATH, 'db/migrations')
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Gets the migration names
|
||||
* @returns Promise<string[]>
|
||||
*/
|
||||
async getMigrations() {
|
||||
const migrationFiles = await fs.readdir(baseMigrationPath)
|
||||
return migrationFiles.map(m => m.replace('.js', '')).sort(semver.compare).map(m => ({
|
||||
file: m,
|
||||
directory: baseMigrationPath
|
||||
}))
|
||||
},
|
||||
|
||||
getMigrationName(migration) {
|
||||
return migration.file.indexOf('.js') >= 0 ? migration.file : `${migration.file}.js`
|
||||
},
|
||||
|
||||
getMigration(migration) {
|
||||
return require(path.join(baseMigrationPath, migration.file))
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import path from 'node:path'
|
||||
import fse from 'fs-extra'
|
||||
import semver from 'semver'
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Gets the migration names
|
||||
* @returns Promise<string[]>
|
||||
*/
|
||||
async getMigrations() {
|
||||
const baseMigrationPath = path.join(WIKI.SERVERPATH, 'db/migrations')
|
||||
const migrationFiles = await fse.readdir(baseMigrationPath)
|
||||
return migrationFiles.map(m => m.replace('.mjs', '')).sort(semver.compare).map(m => ({
|
||||
file: m,
|
||||
directory: baseMigrationPath
|
||||
}))
|
||||
},
|
||||
|
||||
getMigrationName(migration) {
|
||||
return migration.file.indexOf('.mjs') >= 0 ? migration.file : `${migration.file}.mjs`
|
||||
},
|
||||
|
||||
async getMigration(migration) {
|
||||
return import(path.join(WIKI.SERVERPATH, 'db/migrations', `${migration.file}.mjs`))
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
const _ = require('lodash')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const autoload = require('auto-load')
|
||||
const { makeExecutableSchema } = require('@graphql-tools/schema')
|
||||
const { defaultKeyGenerator, rateLimitDirective } = require('graphql-rate-limit-directive')
|
||||
const { GraphQLUpload } = require('graphql-upload')
|
||||
|
||||
// Rate Limiter
|
||||
|
||||
const { rateLimitDirectiveTypeDefs, rateLimitDirectiveTransformer } = rateLimitDirective({
|
||||
keyGenerator: (directiveArgs, source, args, context, info) => `${context.req.ip}:${defaultKeyGenerator(directiveArgs, source, args, context, info)}`
|
||||
})
|
||||
|
||||
// Schemas
|
||||
|
||||
WIKI.logger.info(`Loading GraphQL Schema...`)
|
||||
const typeDefs = [
|
||||
rateLimitDirectiveTypeDefs
|
||||
]
|
||||
const schemas = fs.readdirSync(path.join(WIKI.SERVERPATH, 'graph/schemas'))
|
||||
schemas.forEach(schema => {
|
||||
typeDefs.push(fs.readFileSync(path.join(WIKI.SERVERPATH, `graph/schemas/${schema}`), 'utf8'))
|
||||
})
|
||||
|
||||
// Resolvers
|
||||
|
||||
WIKI.logger.info(`Loading GraphQL Resolvers...`)
|
||||
let resolvers = {
|
||||
Date: require('./scalars/date'),
|
||||
JSON: require('./scalars/json'),
|
||||
UUID: require('./scalars/uuid'),
|
||||
Upload: GraphQLUpload
|
||||
}
|
||||
const resolversObj = _.values(autoload(path.join(WIKI.SERVERPATH, 'graph/resolvers')))
|
||||
resolversObj.forEach(resolver => {
|
||||
_.merge(resolvers, resolver)
|
||||
})
|
||||
|
||||
// Make executable schema
|
||||
|
||||
WIKI.logger.info(`Compiling GraphQL Schema...`)
|
||||
let schema = makeExecutableSchema({
|
||||
typeDefs,
|
||||
resolvers
|
||||
})
|
||||
|
||||
// Apply schema transforms
|
||||
|
||||
schema = rateLimitDirectiveTransformer(schema)
|
||||
|
||||
WIKI.logger.info(`GraphQL Schema: [ OK ]`)
|
||||
|
||||
module.exports = schema
|
@ -0,0 +1,61 @@
|
||||
import { merge } from 'lodash-es'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { makeExecutableSchema } from '@graphql-tools/schema'
|
||||
import { defaultKeyGenerator, rateLimitDirective } from 'graphql-rate-limit-directive'
|
||||
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs'
|
||||
|
||||
import DateScalar from './scalars/date.mjs'
|
||||
import JSONScalar from './scalars/json.mjs'
|
||||
import UUIDScalar from './scalars/uuid.mjs'
|
||||
|
||||
export async function initSchema () {
|
||||
// Rate Limiter
|
||||
|
||||
const { rateLimitDirectiveTypeDefs, rateLimitDirectiveTransformer } = rateLimitDirective({
|
||||
keyGenerator: (directiveArgs, source, args, context, info) => `${context.req.ip}:${defaultKeyGenerator(directiveArgs, source, args, context, info)}`
|
||||
})
|
||||
|
||||
// Schemas
|
||||
|
||||
WIKI.logger.info(`Loading GraphQL Schema...`)
|
||||
const typeDefs = [
|
||||
rateLimitDirectiveTypeDefs
|
||||
]
|
||||
const schemaList = await fs.readdir(path.join(WIKI.SERVERPATH, 'graph/schemas'))
|
||||
for (const schemaFile of schemaList) {
|
||||
typeDefs.push(await fs.readFile(path.join(WIKI.SERVERPATH, `graph/schemas/${schemaFile}`), 'utf8'))
|
||||
}
|
||||
|
||||
// Resolvers
|
||||
|
||||
WIKI.logger.info(`Loading GraphQL Resolvers...`)
|
||||
let resolvers = {
|
||||
Date: DateScalar,
|
||||
JSON: JSONScalar,
|
||||
UUID: UUIDScalar,
|
||||
Upload: GraphQLUpload
|
||||
}
|
||||
|
||||
const resolverList = await fs.readdir(path.join(WIKI.SERVERPATH, 'graph/resolvers'))
|
||||
for (const resolverFile of resolverList) {
|
||||
const resolver = (await import(path.join(WIKI.SERVERPATH, 'graph/resolvers', resolverFile))).default
|
||||
merge(resolvers, resolver)
|
||||
}
|
||||
|
||||
// Make executable schema
|
||||
|
||||
WIKI.logger.info(`Compiling GraphQL Schema...`)
|
||||
let schema = makeExecutableSchema({
|
||||
typeDefs,
|
||||
resolvers
|
||||
})
|
||||
|
||||
// Apply schema transforms
|
||||
|
||||
schema = rateLimitDirectiveTransformer(schema)
|
||||
|
||||
WIKI.logger.info(`GraphQL Schema: [ OK ]`)
|
||||
|
||||
return schema
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
const graphHelper = require('../../helpers/graph')
|
||||
|
||||
module.exports = {
|
||||
Mutation: {
|
||||
async rebuildSearchIndex (obj, args, context) {
|
||||
try {
|
||||
await WIKI.data.searchEngine.rebuild()
|
||||
return {
|
||||
responseResult: graphHelper.generateSuccess('Index rebuilt successfully')
|
||||
}
|
||||
} catch (err) {
|
||||
return graphHelper.generateError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import { generateError, generateSuccess } from '../../helpers/graph.mjs'
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
async rebuildSearchIndex (obj, args, context) {
|
||||
try {
|
||||
await WIKI.data.searchEngine.rebuild()
|
||||
return {
|
||||
responseResult: generateSuccess('Index rebuilt successfully')
|
||||
}
|
||||
} catch (err) {
|
||||
return generateError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
const gql = require('graphql')
|
||||
const { DateTime } = require('luxon')
|
||||
import gql from 'graphql'
|
||||
import { DateTime } from 'luxon'
|
||||
|
||||
function parseDateTime (value) {
|
||||
const nDate = DateTime.fromISO(value)
|
||||
return nDate.isValid ? nDate : null
|
||||
}
|
||||
|
||||
module.exports = new gql.GraphQLScalarType({
|
||||
export default new gql.GraphQLScalarType({
|
||||
name: 'Date',
|
||||
description: 'ISO date-time string at UTC',
|
||||
parseValue(value) {
|
@ -1,99 +0,0 @@
|
||||
const _ = require('lodash')
|
||||
const crypto = require('node:crypto')
|
||||
|
||||
module.exports = {
|
||||
/* eslint-disable promise/param-names */
|
||||
createDeferred () {
|
||||
let result, resolve, reject
|
||||
return {
|
||||
resolve: function (value) {
|
||||
if (resolve) {
|
||||
resolve(value)
|
||||
} else {
|
||||
result = result || new Promise(function (r) { r(value) })
|
||||
}
|
||||
},
|
||||
reject: function (reason) {
|
||||
if (reject) {
|
||||
reject(reason)
|
||||
} else {
|
||||
result = result || new Promise(function (x, j) { j(reason) })
|
||||
}
|
||||
},
|
||||
promise: new Promise(function (r, j) {
|
||||
if (result) {
|
||||
r(result)
|
||||
} else {
|
||||
resolve = r
|
||||
reject = j
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Decode a tree path
|
||||
*
|
||||
* @param {string} str String to decode
|
||||
* @returns Decoded tree path
|
||||
*/
|
||||
decodeTreePath (str) {
|
||||
return str.replaceAll('_', '-').replaceAll('.', '/')
|
||||
},
|
||||
/**
|
||||
* Encode a tree path
|
||||
*
|
||||
* @param {string} str String to encode
|
||||
* @returns Encoded tree path
|
||||
*/
|
||||
encodeTreePath (str) {
|
||||
return str?.toLowerCase()?.replaceAll('-', '_')?.replaceAll('/', '.') || ''
|
||||
},
|
||||
/**
|
||||
* Generate SHA-1 Hash of a string
|
||||
*
|
||||
* @param {string} str String to hash
|
||||
* @returns Hashed string
|
||||
*/
|
||||
generateHash (str) {
|
||||
return crypto.createHash('sha1').update(str).digest('hex')
|
||||
},
|
||||
/**
|
||||
* Get default value of type
|
||||
*
|
||||
* @param {any} type primitive type name
|
||||
* @returns Default value
|
||||
*/
|
||||
getTypeDefaultValue (type) {
|
||||
switch (type.toLowerCase()) {
|
||||
case 'string':
|
||||
return ''
|
||||
case 'number':
|
||||
return 0
|
||||
case 'boolean':
|
||||
return false
|
||||
}
|
||||
},
|
||||
parseModuleProps (props) {
|
||||
return _.transform(props, (result, value, key) => {
|
||||
let defaultValue = ''
|
||||
if (_.isPlainObject(value)) {
|
||||
defaultValue = !_.isNil(value.default) ? value.default : this.getTypeDefaultValue(value.type)
|
||||
} else {
|
||||
defaultValue = this.getTypeDefaultValue(value)
|
||||
}
|
||||
_.set(result, key, {
|
||||
default: defaultValue,
|
||||
type: (value.type || value).toLowerCase(),
|
||||
title: value.title || _.startCase(key),
|
||||
hint: value.hint || '',
|
||||
enum: value.enum || false,
|
||||
enumDisplay: value.enumDisplay || 'select',
|
||||
multiline: value.multiline || false,
|
||||
sensitive: value.sensitive || false,
|
||||
icon: value.icon || 'rename',
|
||||
order: value.order || 100
|
||||
})
|
||||
return result
|
||||
}, {})
|
||||
}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
import { isNil, isPlainObject, set, startCase, transform } from 'lodash-es'
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
/* eslint-disable promise/param-names */
|
||||
export function createDeferred () {
|
||||
let result, resolve, reject
|
||||
return {
|
||||
resolve: function (value) {
|
||||
if (resolve) {
|
||||
resolve(value)
|
||||
} else {
|
||||
result = result || new Promise(function (r) { r(value) })
|
||||
}
|
||||
},
|
||||
reject: function (reason) {
|
||||
if (reject) {
|
||||
reject(reason)
|
||||
} else {
|
||||
result = result || new Promise(function (x, j) { j(reason) })
|
||||
}
|
||||
},
|
||||
promise: new Promise(function (r, j) {
|
||||
if (result) {
|
||||
r(result)
|
||||
} else {
|
||||
resolve = r
|
||||
reject = j
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a tree path
|
||||
*
|
||||
* @param {string} str String to decode
|
||||
* @returns Decoded tree path
|
||||
*/
|
||||
export function decodeTreePath (str) {
|
||||
return str.replaceAll('_', '-').replaceAll('.', '/')
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a tree path
|
||||
*
|
||||
* @param {string} str String to encode
|
||||
* @returns Encoded tree path
|
||||
*/
|
||||
export function encodeTreePath (str) {
|
||||
return str?.toLowerCase()?.replaceAll('-', '_')?.replaceAll('/', '.') || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SHA-1 Hash of a string
|
||||
*
|
||||
* @param {string} str String to hash
|
||||
* @returns Hashed string
|
||||
*/
|
||||
export function generateHash (str) {
|
||||
return crypto.createHash('sha1').update(str).digest('hex')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default value of type
|
||||
*
|
||||
* @param {any} type primitive type name
|
||||
* @returns Default value
|
||||
*/
|
||||
export function getTypeDefaultValue (type) {
|
||||
switch (type.toLowerCase()) {
|
||||
case 'string':
|
||||
return ''
|
||||
case 'number':
|
||||
return 0
|
||||
case 'boolean':
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function parseModuleProps (props) {
|
||||
return transform(props, (result, value, key) => {
|
||||
let defaultValue = ''
|
||||
if (isPlainObject(value)) {
|
||||
defaultValue = !isNil(value.default) ? value.default : this.getTypeDefaultValue(value.type)
|
||||
} else {
|
||||
defaultValue = this.getTypeDefaultValue(value)
|
||||
}
|
||||
set(result, key, {
|
||||
default: defaultValue,
|
||||
type: (value.type || value).toLowerCase(),
|
||||
title: value.title || startCase(key),
|
||||
hint: value.hint || '',
|
||||
enum: value.enum || false,
|
||||
enumDisplay: value.enumDisplay || 'select',
|
||||
multiline: value.multiline || false,
|
||||
sensitive: value.sensitive || false,
|
||||
icon: value.icon || 'rename',
|
||||
order: value.order || 100
|
||||
})
|
||||
return result
|
||||
}, {})
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
const CustomError = require('custom-error-instance')
|
||||
import CustomError from 'custom-error-instance'
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
Custom (slug, message) {
|
||||
return CustomError(slug, { message })
|
||||
},
|
@ -1,21 +0,0 @@
|
||||
const _ = require('lodash')
|
||||
|
||||
module.exports = {
|
||||
generateSuccess (msg) {
|
||||
return {
|
||||
succeeded: true,
|
||||
errorCode: 0,
|
||||
slug: 'ok',
|
||||
message: _.defaultTo(msg, 'Operation succeeded.')
|
||||
}
|
||||
},
|
||||
generateError (err, complete = true) {
|
||||
const error = {
|
||||
succeeded: false,
|
||||
errorCode: _.isFinite(err.code) ? err.code : 1,
|
||||
slug: err.name,
|
||||
message: err.message || 'An unexpected error occured.'
|
||||
}
|
||||
return (complete) ? { operation: error } : error
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
import { defaultTo, isFinite } from 'lodash-es'
|
||||
|
||||
export function generateSuccess (msg) {
|
||||
return {
|
||||
succeeded: true,
|
||||
errorCode: 0,
|
||||
slug: 'ok',
|
||||
message: defaultTo(msg, 'Operation succeeded.')
|
||||
}
|
||||
}
|
||||
|
||||
export function generateError (err, complete = true) {
|
||||
const error = {
|
||||
succeeded: false,
|
||||
errorCode: isFinite(err.code) ? err.code : 1,
|
||||
slug: err.name,
|
||||
message: err.message || 'An unexpected error occured.'
|
||||
}
|
||||
return (complete) ? { operation: error } : error
|
||||
}
|
@ -1,151 +0,0 @@
|
||||
const qs = require('querystring')
|
||||
const _ = require('lodash')
|
||||
const crypto = require('crypto')
|
||||
const path = require('path')
|
||||
|
||||
const localeSegmentRegex = /^[A-Z]{2}(-[A-Z]{2})?$/i
|
||||
const localeFolderRegex = /^([a-z]{2}(?:-[a-z]{2})?\/)?(.*)/i
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const unsafeCharsRegex = /[\x00-\x1f\x80-\x9f\\"|<>:*?]/
|
||||
|
||||
const contentToExt = {
|
||||
markdown: 'md',
|
||||
html: 'html'
|
||||
}
|
||||
const extToContent = _.invert(contentToExt)
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Parse raw url path and make it safe
|
||||
*/
|
||||
parsePath (rawPath, opts = {}) {
|
||||
let pathObj = {
|
||||
// TODO: use site base lang
|
||||
locale: 'en', // WIKI.config.lang.code,
|
||||
path: 'home',
|
||||
private: false,
|
||||
privateNS: '',
|
||||
explicitLocale: false
|
||||
}
|
||||
|
||||
// Clean Path
|
||||
rawPath = _.trim(qs.unescape(rawPath))
|
||||
if (_.startsWith(rawPath, '/')) { rawPath = rawPath.substring(1) }
|
||||
rawPath = rawPath.replace(unsafeCharsRegex, '')
|
||||
if (rawPath === '') { rawPath = 'home' }
|
||||
|
||||
rawPath = rawPath.replace(/\\/g, '').replace(/\/\//g, '').replace(/\.\.+/ig, '')
|
||||
|
||||
// Extract Info
|
||||
let pathParts = _.filter(_.split(rawPath, '/'), p => {
|
||||
p = _.trim(p)
|
||||
return !_.isEmpty(p) && p !== '..' && p !== '.'
|
||||
})
|
||||
if (pathParts[0].startsWith('_')) {
|
||||
pathParts.shift()
|
||||
}
|
||||
if (localeSegmentRegex.test(pathParts[0])) {
|
||||
pathObj.locale = pathParts[0]
|
||||
pathObj.explicitLocale = true
|
||||
pathParts.shift()
|
||||
}
|
||||
|
||||
// Strip extension
|
||||
if (opts.stripExt && pathParts.length > 0) {
|
||||
const lastPart = _.last(pathParts)
|
||||
if (lastPart.indexOf('.') > 0) {
|
||||
pathParts.pop()
|
||||
const lastPartMeta = path.parse(lastPart)
|
||||
pathParts.push(lastPartMeta.name)
|
||||
}
|
||||
}
|
||||
|
||||
pathObj.path = _.join(pathParts, '/')
|
||||
return pathObj
|
||||
},
|
||||
/**
|
||||
* Generate unique hash from page
|
||||
*/
|
||||
generateHash(opts) {
|
||||
return crypto.createHash('sha1').update(`${opts.locale}|${opts.path}|${opts.privateNS}`).digest('hex')
|
||||
},
|
||||
/**
|
||||
* Inject Page Metadata
|
||||
*/
|
||||
injectPageMetadata(page) {
|
||||
let meta = [
|
||||
['title', page.title],
|
||||
['description', page.description],
|
||||
['published', page.isPublished.toString()],
|
||||
['date', page.updatedAt],
|
||||
['tags', page.tags ? page.tags.map(t => t.tag).join(', ') : ''],
|
||||
['editor', page.editorKey],
|
||||
['dateCreated', page.createdAt]
|
||||
]
|
||||
switch (page.contentType) {
|
||||
case 'markdown':
|
||||
return '---\n' + meta.map(mt => `${mt[0]}: ${mt[1]}`).join('\n') + '\n---\n\n' + page.content
|
||||
case 'html':
|
||||
return '<!--\n' + meta.map(mt => `${mt[0]}: ${mt[1]}`).join('\n') + '\n-->\n\n' + page.content
|
||||
case 'json':
|
||||
return {
|
||||
...page.content,
|
||||
_meta: _.fromPairs(meta)
|
||||
}
|
||||
default:
|
||||
return page.content
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Check if path is a reserved path
|
||||
*/
|
||||
isReservedPath(rawPath) {
|
||||
const firstSection = _.head(rawPath.split('/'))
|
||||
if (firstSection.length < 1) {
|
||||
return true
|
||||
} else if (localeSegmentRegex.test(firstSection)) {
|
||||
return true
|
||||
} else if (
|
||||
_.some(WIKI.data.reservedPaths, p => {
|
||||
return p === firstSection
|
||||
})) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Get file extension from content type
|
||||
*/
|
||||
getFileExtension(contentType) {
|
||||
return _.get(contentToExt, contentType, 'txt')
|
||||
},
|
||||
/**
|
||||
* Get content type from file extension
|
||||
*/
|
||||
getContentType (filePath) {
|
||||
const ext = _.last(filePath.split('.'))
|
||||
return _.get(extToContent, ext, false)
|
||||
},
|
||||
/**
|
||||
* Get Page Meta object from disk path
|
||||
*/
|
||||
getPagePath (filePath) {
|
||||
let fpath = filePath
|
||||
if (process.platform === 'win32') {
|
||||
fpath = filePath.replace(/\\/g, '/')
|
||||
}
|
||||
let meta = {
|
||||
locale: WIKI.config.lang.code,
|
||||
path: _.initial(fpath.split('.')).join('')
|
||||
}
|
||||
const result = localeFolderRegex.exec(meta.path)
|
||||
if (result[1]) {
|
||||
meta = {
|
||||
locale: result[1].replace('/', ''),
|
||||
path: result[2]
|
||||
}
|
||||
}
|
||||
return meta
|
||||
}
|
||||
}
|
@ -0,0 +1,155 @@
|
||||
import qs from 'querystring'
|
||||
import { fromPairs, get, initial, invert, isEmpty, last } from 'lodash-es'
|
||||
import crypto from 'node:crypto'
|
||||
import path from 'node:path'
|
||||
|
||||
const localeSegmentRegex = /^[A-Z]{2}(-[A-Z]{2})?$/i
|
||||
const localeFolderRegex = /^([a-z]{2}(?:-[a-z]{2})?\/)?(.*)/i
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const unsafeCharsRegex = /[\x00-\x1f\x80-\x9f\\"|<>:*?]/
|
||||
|
||||
const contentToExt = {
|
||||
markdown: 'md',
|
||||
html: 'html'
|
||||
}
|
||||
const extToContent = invert(contentToExt)
|
||||
|
||||
/**
|
||||
* Parse raw url path and make it safe
|
||||
*/
|
||||
export function parsePath (rawPath, opts = {}) {
|
||||
const pathObj = {
|
||||
// TODO: use site base lang
|
||||
locale: 'en', // WIKI.config.lang.code,
|
||||
path: 'home',
|
||||
private: false,
|
||||
privateNS: '',
|
||||
explicitLocale: false
|
||||
}
|
||||
|
||||
// Clean Path
|
||||
rawPath = qs.unescape(rawPath).trim()
|
||||
if (rawPath.startsWith('/')) { rawPath = rawPath.substring(1) }
|
||||
rawPath = rawPath.replace(unsafeCharsRegex, '')
|
||||
if (rawPath === '') { rawPath = 'home' }
|
||||
|
||||
rawPath = rawPath.replace(/\\/g, '').replace(/\/\//g, '').replace(/\.\.+/ig, '')
|
||||
|
||||
// Extract Info
|
||||
let pathParts = rawPath.split('/').filter(p => {
|
||||
p = p.trim()
|
||||
return !isEmpty(p) && p !== '..' && p !== '.'
|
||||
})
|
||||
if (pathParts[0].startsWith('_')) {
|
||||
pathParts.shift()
|
||||
}
|
||||
if (localeSegmentRegex.test(pathParts[0])) {
|
||||
pathObj.locale = pathParts[0]
|
||||
pathObj.explicitLocale = true
|
||||
pathParts.shift()
|
||||
}
|
||||
|
||||
// Strip extension
|
||||
if (opts.stripExt && pathParts.length > 0) {
|
||||
const lastPart = last(pathParts)
|
||||
if (lastPart.indexOf('.') > 0) {
|
||||
pathParts.pop()
|
||||
const lastPartMeta = path.parse(lastPart)
|
||||
pathParts.push(lastPartMeta.name)
|
||||
}
|
||||
}
|
||||
|
||||
pathObj.path = _.join(pathParts, '/')
|
||||
return pathObj
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique hash from page
|
||||
*/
|
||||
export function generateHash(opts) {
|
||||
return crypto.createHash('sha1').update(`${opts.locale}|${opts.path}|${opts.privateNS}`).digest('hex')
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject Page Metadata
|
||||
*/
|
||||
export function injectPageMetadata(page) {
|
||||
const meta = [
|
||||
['title', page.title],
|
||||
['description', page.description],
|
||||
['published', page.isPublished.toString()],
|
||||
['date', page.updatedAt],
|
||||
['tags', page.tags ? page.tags.map(t => t.tag).join(', ') : ''],
|
||||
['editor', page.editorKey],
|
||||
['dateCreated', page.createdAt]
|
||||
]
|
||||
switch (page.contentType) {
|
||||
case 'markdown':
|
||||
return '---\n' + meta.map(mt => `${mt[0]}: ${mt[1]}`).join('\n') + '\n---\n\n' + page.content
|
||||
case 'html':
|
||||
return '<!--\n' + meta.map(mt => `${mt[0]}: ${mt[1]}`).join('\n') + '\n-->\n\n' + page.content
|
||||
case 'json':
|
||||
return {
|
||||
...page.content,
|
||||
_meta: fromPairs(meta)
|
||||
}
|
||||
default:
|
||||
return page.content
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if path is a reserved path
|
||||
*/
|
||||
export function isReservedPath(rawPath) {
|
||||
const firstSection = _.head(rawPath.split('/'))
|
||||
if (firstSection.length < 1) {
|
||||
return true
|
||||
} else if (localeSegmentRegex.test(firstSection)) {
|
||||
return true
|
||||
} else if (
|
||||
WIKI.data.reservedPaths.some(p => {
|
||||
return p === firstSection
|
||||
})) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file extension from content type
|
||||
*/
|
||||
export function getFileExtension(contentType) {
|
||||
return get(contentToExt, contentType, 'txt')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content type from file extension
|
||||
*/
|
||||
export function getContentType (filePath) {
|
||||
const ext = last(filePath.split('.'))
|
||||
return get(extToContent, ext, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Page Meta object from disk path
|
||||
*/
|
||||
export function getPagePath (filePath) {
|
||||
let fpath = filePath
|
||||
if (process.platform === 'win32') {
|
||||
fpath = filePath.replace(/\\/g, '/')
|
||||
}
|
||||
let meta = {
|
||||
locale: WIKI.config.lang.code,
|
||||
path: initial(fpath.split('.')).join('')
|
||||
}
|
||||
const result = localeFolderRegex.exec(meta.path)
|
||||
if (result[1]) {
|
||||
meta = {
|
||||
locale: result[1].replace('/', ''),
|
||||
path: result[2]
|
||||
}
|
||||
}
|
||||
return meta
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
const util = require('node:util')
|
||||
const crypto = require('node:crypto')
|
||||
const randomBytes = util.promisify(crypto.randomBytes)
|
||||
const passportJWT = require('passport-jwt')
|
||||
|
||||
module.exports = {
|
||||
sanitizeCommitUser (user) {
|
||||
// let wlist = new RegExp('[^a-zA-Z0-9-_.\',& ' + appdata.regex.cjk + appdata.regex.arabic + ']', 'g')
|
||||
// return {
|
||||
// name: _.chain(user.name).replace(wlist, '').trim().value(),
|
||||
// email: appconfig.git.showUserEmail ? user.email : appconfig.git.serverEmail
|
||||
// }
|
||||
},
|
||||
/**
|
||||
* Generate a random token
|
||||
*
|
||||
* @param {any} length
|
||||
* @returns
|
||||
*/
|
||||
async generateToken (length) {
|
||||
return (await randomBytes(length)).toString('hex')
|
||||
},
|
||||
|
||||
extractJWT: passportJWT.ExtractJwt.fromExtractors([
|
||||
passportJWT.ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
(req) => {
|
||||
let token = null
|
||||
if (req && req.cookies) {
|
||||
token = req.cookies['jwt']
|
||||
}
|
||||
// Force uploads to use Auth headers
|
||||
if (req.path.toLowerCase() === '/u') {
|
||||
return null
|
||||
}
|
||||
return token
|
||||
}
|
||||
])
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
import util from 'node:util'
|
||||
import crypto from 'node:crypto'
|
||||
import passportJWT from 'passport-jwt'
|
||||
|
||||
const randomBytes = util.promisify(crypto.randomBytes)
|
||||
|
||||
export function sanitizeCommitUser (user) {
|
||||
// let wlist = new RegExp('[^a-zA-Z0-9-_.\',& ' + appdata.regex.cjk + appdata.regex.arabic + ']', 'g')
|
||||
// return {
|
||||
// name: _.chain(user.name).replace(wlist, '').trim().value(),
|
||||
// email: appconfig.git.showUserEmail ? user.email : appconfig.git.serverEmail
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random token
|
||||
*
|
||||
* @param {any} length
|
||||
* @returns
|
||||
*/
|
||||
export async function generateToken (length) {
|
||||
return (await randomBytes(length)).toString('hex')
|
||||
}
|
||||
|
||||
export const extractJWT = passportJWT.ExtractJwt.fromExtractors([
|
||||
passportJWT.ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
(req) => {
|
||||
let token = null
|
||||
if (req && req.cookies) {
|
||||
token = req.cookies['jwt']
|
||||
}
|
||||
// Force uploads to use Auth headers
|
||||
if (req.path.toLowerCase() === '/u') {
|
||||
return null
|
||||
}
|
||||
return token
|
||||
}
|
||||
])
|
@ -1,20 +0,0 @@
|
||||
## IMPORTANT
|
||||
|
||||
Localization files are not stored into files!
|
||||
|
||||
Contact us on Gitter to request access to the translation web service: https://gitter.im/Requarks/wiki
|
||||
|
||||
## Development Mode
|
||||
|
||||
If you need to add new keys and test them live, simply create a {LANG}.yml file in this folder containing the values you want to test. e.g.:
|
||||
|
||||
### en.yml
|
||||
```yml
|
||||
admin:
|
||||
api.title: 'API Access'
|
||||
auth.title: 'Authentication'
|
||||
```
|
||||
|
||||
The official localization keys will still be loaded first, but your local files will overwrite any existing keys (and add new ones).
|
||||
|
||||
Note that you must restart Wiki.js to load any changes made to the files, which happens automatically on save when in dev mode.
|
@ -1,46 +0,0 @@
|
||||
/* global WIKI */
|
||||
|
||||
/**
|
||||
* Security Middleware
|
||||
*
|
||||
* @param {Express Request} req Express request object
|
||||
* @param {Express Response} res Express response object
|
||||
* @param {Function} next next callback function
|
||||
* @return {any} void
|
||||
*/
|
||||
module.exports = function (req, res, next) {
|
||||
// -> Disable X-Powered-By
|
||||
req.app.disable('x-powered-by')
|
||||
|
||||
// -> Disable Frame Embedding
|
||||
if (WIKI.config.security.securityIframe) {
|
||||
res.set('X-Frame-Options', 'deny')
|
||||
}
|
||||
|
||||
// -> Re-enable XSS Fitler if disabled
|
||||
res.set('X-XSS-Protection', '1; mode=block')
|
||||
|
||||
// -> Disable MIME-sniffing
|
||||
res.set('X-Content-Type-Options', 'nosniff')
|
||||
|
||||
// -> Disable IE Compatibility Mode
|
||||
res.set('X-UA-Compatible', 'IE=edge')
|
||||
|
||||
// -> Disables referrer header when navigating to a different origin
|
||||
if (WIKI.config.security.securityReferrerPolicy) {
|
||||
res.set('Referrer-Policy', 'same-origin')
|
||||
}
|
||||
|
||||
// -> Enforce HSTS
|
||||
if (WIKI.config.security.securityHSTS) {
|
||||
res.set('Strict-Transport-Security', `max-age=${WIKI.config.security.securityHSTSDuration}; includeSubDomains`)
|
||||
}
|
||||
|
||||
// -> Prevent Open Redirect from user provided URL
|
||||
if (WIKI.config.security.securityOpenRedirect) {
|
||||
// Strips out all repeating / character in the provided URL
|
||||
req.url = req.url.replace(/(\/)(?=\/*\1)/g, '')
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
const _ = require('lodash')
|
||||
|
||||
/**
|
||||
* SEO Middleware
|
||||
*
|
||||
* @param {Express Request} req Express request object
|
||||
* @param {Express Response} res Express response object
|
||||
* @param {Function} next next callback function
|
||||
* @return {any} void
|
||||
*/
|
||||
module.exports = function (req, res, next) {
|
||||
if (req.path.length > 1 && _.endsWith(req.path, '/')) {
|
||||
let query = req.url.slice(req.path.length) || ''
|
||||
res.redirect(301, req.path.slice(0, -1) + query)
|
||||
} else {
|
||||
_.set(res.locals, 'pageMeta.url', `${WIKI.config.host}${req.path}`)
|
||||
return next()
|
||||
}
|
||||
}
|
@ -1,14 +1,14 @@
|
||||
/* global WIKI */
|
||||
|
||||
const Model = require('objection').Model
|
||||
const { DateTime } = require('luxon')
|
||||
const ms = require('ms')
|
||||
const jwt = require('jsonwebtoken')
|
||||
import { Model } from 'objection'
|
||||
import { DateTime } from 'luxon'
|
||||
import ms from 'ms'
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
/**
|
||||
* Users model
|
||||
*/
|
||||
module.exports = class ApiKey extends Model {
|
||||
export class ApiKey extends Model {
|
||||
static get tableName() { return 'apiKeys' }
|
||||
|
||||
static get jsonSchema () {
|
@ -1,9 +1,9 @@
|
||||
const Model = require('objection').Model
|
||||
import { Model } from 'objection'
|
||||
|
||||
/**
|
||||
* Hook model
|
||||
*/
|
||||
module.exports = class Hook extends Model {
|
||||
export class Hook extends Model {
|
||||
static get tableName () { return 'hooks' }
|
||||
|
||||
static get jsonAttributes () {
|
@ -0,0 +1,45 @@
|
||||
import { Analytics } from './analytics.mjs'
|
||||
import { ApiKey } from './apiKeys.mjs'
|
||||
import { Asset } from './assets.mjs'
|
||||
import { Authentication } from './authentication.mjs'
|
||||
import { CommentProvider } from './commentProviders.mjs'
|
||||
import { Comment } from './comments.mjs'
|
||||
import { Group } from './groups.mjs'
|
||||
import { Hook } from './hooks.mjs'
|
||||
import { Locale } from './locales.mjs'
|
||||
import { Navigation } from './navigation.mjs'
|
||||
import { PageHistory } from './pageHistory.mjs'
|
||||
import { PageLink } from './pageLinks.mjs'
|
||||
import { Page } from './pages.mjs'
|
||||
import { Renderer } from './renderers.mjs'
|
||||
import { Setting } from './settings.mjs'
|
||||
import { Site } from './sites.mjs'
|
||||
import { Storage } from './storage.mjs'
|
||||
import { Tag } from './tags.mjs'
|
||||
import { Tree } from './tree.mjs'
|
||||
import { UserKey } from './userKeys.mjs'
|
||||
import { User } from './users.mjs'
|
||||
|
||||
export default {
|
||||
analytics: Analytics,
|
||||
apiKeys: ApiKey,
|
||||
assets: Asset,
|
||||
authentication: Authentication,
|
||||
commentProviders: CommentProvider,
|
||||
comments: Comment,
|
||||
groups: Group,
|
||||
hooks: Hook,
|
||||
locales: Locale,
|
||||
navigation: Navigation,
|
||||
pageHistory: PageHistory,
|
||||
pageLinks: PageLink,
|
||||
pages: Page,
|
||||
renderers: Renderer,
|
||||
settings: Setting,
|
||||
sites: Site,
|
||||
storage: Storage,
|
||||
tags: Tag,
|
||||
tree: Tree,
|
||||
userKeys: UserKey,
|
||||
users: User
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
const Model = require('objection').Model
|
||||
import { Model } from 'objection'
|
||||
|
||||
/**
|
||||
* Locales model
|
||||
*/
|
||||
module.exports = class Locale extends Model {
|
||||
export class Locale extends Model {
|
||||
static get tableName() { return 'locales' }
|
||||
static get idColumn() { return 'code' }
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
||||
module.exports = async (payload) => {
|
||||
export async function task (payload) {
|
||||
WIKI.logger.info('Checking for latest version...')
|
||||
|
||||
try {
|
@ -1,6 +1,6 @@
|
||||
const { DateTime } = require('luxon')
|
||||
import { DateTime } from 'luxon'
|
||||
|
||||
module.exports = async (payload) => {
|
||||
export async function task (payload) {
|
||||
WIKI.logger.info('Cleaning scheduler job history...')
|
||||
|
||||
try {
|
@ -1,4 +1,4 @@
|
||||
module.exports = async (payload) => {
|
||||
export async function task (payload) {
|
||||
WIKI.logger.info('Fetching latest localization data...')
|
||||
|
||||
try {
|
@ -1,20 +1,20 @@
|
||||
const path = require('node:path')
|
||||
const fs = require('fs-extra')
|
||||
const { DateTime } = require('luxon')
|
||||
import path from 'node:path'
|
||||
import fse from 'fs-extra'
|
||||
import { DateTime } from 'luxon'
|
||||
|
||||
module.exports = async ({ payload }) => {
|
||||
export async function task ({ payload }) {
|
||||
WIKI.logger.info('Purging orphaned upload files...')
|
||||
|
||||
try {
|
||||
const uplTempPath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'uploads')
|
||||
await fs.ensureDir(uplTempPath)
|
||||
const ls = await fs.readdir(uplTempPath)
|
||||
await fse.ensureDir(uplTempPath)
|
||||
const ls = await fse.readdir(uplTempPath)
|
||||
const fifteenAgo = DateTime.now().minus({ minutes: 15 })
|
||||
|
||||
for (const f of ls) {
|
||||
const stat = fs.stat(path.join(uplTempPath, f))
|
||||
const stat = fse.stat(path.join(uplTempPath, f))
|
||||
if ((await stat).isFile && stat.ctime < fifteenAgo) {
|
||||
await fs.unlink(path.join(uplTempPath, f))
|
||||
await fse.unlink(path.join(uplTempPath, f))
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in new issue