mirror of https://github.com/requarks/wiki
parent
05fe495772
commit
dc25db1139
@ -0,0 +1,4 @@
|
|||||||
|
audit = false
|
||||||
|
fund = false
|
||||||
|
save-exact = true
|
||||||
|
save-prefix = ""
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* API Routes
|
||||||
|
*/
|
||||||
|
async function routes (app, options) {
|
||||||
|
app.register(import('./sites.mjs'), { prefix: '/sites' })
|
||||||
|
app.register(import('./system.mjs'), { prefix: '/system' })
|
||||||
|
app.register(import('./users.mjs'), { prefix: '/users' })
|
||||||
|
|
||||||
|
app.get('/', async (req, reply) => {
|
||||||
|
return { ok: true }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default routes
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
/**
|
||||||
|
* Sites API Routes
|
||||||
|
*/
|
||||||
|
async function routes (app, options) {
|
||||||
|
app.get('/', {
|
||||||
|
schema: {
|
||||||
|
summary: 'List all sites',
|
||||||
|
tags: ['Sites']
|
||||||
|
}
|
||||||
|
}, async (req, reply) => {
|
||||||
|
return { hello: 'world' }
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/:siteId', {
|
||||||
|
schema: {
|
||||||
|
summary: 'Get site info',
|
||||||
|
tags: ['Sites']
|
||||||
|
}
|
||||||
|
}, async (req, reply) => {
|
||||||
|
return { hello: 'world' }
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/', {
|
||||||
|
schema: {
|
||||||
|
summary: 'Create a new site',
|
||||||
|
tags: ['Sites'],
|
||||||
|
body: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['name', 'hostname'],
|
||||||
|
properties: {
|
||||||
|
name: { type: 'string' },
|
||||||
|
hostname: { type: 'string' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, async (req, reply) => {
|
||||||
|
return { hello: 'world' }
|
||||||
|
})
|
||||||
|
|
||||||
|
app.put('/:siteId', {
|
||||||
|
schema: {
|
||||||
|
summary: 'Update a site',
|
||||||
|
tags: ['Sites']
|
||||||
|
}
|
||||||
|
}, async (req, reply) => {
|
||||||
|
return { hello: 'world' }
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE SITE
|
||||||
|
*/
|
||||||
|
app.delete('/:siteId', {
|
||||||
|
schema: {
|
||||||
|
summary: 'Delete a site',
|
||||||
|
tags: ['Sites'],
|
||||||
|
params: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
siteId: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'uuid'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['siteId']
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
204: {
|
||||||
|
description: 'Site deleted successfully'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, async (req, reply) => {
|
||||||
|
try {
|
||||||
|
if (await WIKI.models.sites.deleteSite(req.params.siteId)) {
|
||||||
|
reply.code(204)
|
||||||
|
} else {
|
||||||
|
reply.badRequest('Site does not exist.')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
reply.send(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default routes
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* System API Routes
|
||||||
|
*/
|
||||||
|
async function routes (app, options) {
|
||||||
|
app.get('/info', {
|
||||||
|
schema: {
|
||||||
|
summary: 'System Info',
|
||||||
|
tags: ['System']
|
||||||
|
}
|
||||||
|
}, async (request, reply) => {
|
||||||
|
return { hello: 'world' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default routes
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Users API Routes
|
||||||
|
*/
|
||||||
|
async function routes (app, options) {
|
||||||
|
app.get('/', {
|
||||||
|
schema: {
|
||||||
|
summary: 'List all users',
|
||||||
|
tags: ['Users']
|
||||||
|
}
|
||||||
|
}, async (request, reply) => {
|
||||||
|
return { hello: 'world' }
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/:userId', {
|
||||||
|
schema: {
|
||||||
|
summary: 'Get user info',
|
||||||
|
tags: ['Users']
|
||||||
|
}
|
||||||
|
}, async (request, reply) => {
|
||||||
|
return { hello: 'world' }
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/', {
|
||||||
|
schema: {
|
||||||
|
summary: 'Create a new user',
|
||||||
|
tags: ['Users']
|
||||||
|
}
|
||||||
|
}, async (request, reply) => {
|
||||||
|
return { hello: 'world' }
|
||||||
|
})
|
||||||
|
|
||||||
|
app.put('/:userId', {
|
||||||
|
schema: {
|
||||||
|
summary: 'Update a user',
|
||||||
|
tags: ['Users']
|
||||||
|
}
|
||||||
|
}, async (request, reply) => {
|
||||||
|
return { hello: 'world' }
|
||||||
|
})
|
||||||
|
|
||||||
|
app.delete('/:userId', {
|
||||||
|
schema: {
|
||||||
|
summary: 'Delete a user',
|
||||||
|
tags: ['Users']
|
||||||
|
}
|
||||||
|
}, async (request, reply) => {
|
||||||
|
return { hello: 'world' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default routes
|
||||||
@ -0,0 +1,160 @@
|
|||||||
|
# ---------------------------------
|
||||||
|
# DO NOT EDIT THIS FILE!
|
||||||
|
# This is reserved for system use!
|
||||||
|
# ---------------------------------
|
||||||
|
name: Wiki.js
|
||||||
|
defaults:
|
||||||
|
config:
|
||||||
|
# File defaults
|
||||||
|
port: 80
|
||||||
|
db:
|
||||||
|
host: localhost
|
||||||
|
port: 5432
|
||||||
|
user: wikijs
|
||||||
|
pass: wikijsrocks
|
||||||
|
db: wiki
|
||||||
|
ssl: false
|
||||||
|
sslOptions:
|
||||||
|
auto: true
|
||||||
|
schema: wiki
|
||||||
|
ssl:
|
||||||
|
enabled: false
|
||||||
|
pool:
|
||||||
|
min: 1
|
||||||
|
bindIP: 0.0.0.0
|
||||||
|
logLevel: info
|
||||||
|
logFormat: default
|
||||||
|
offline: false
|
||||||
|
dataPath: ./data
|
||||||
|
bodyParserLimit: 5mb
|
||||||
|
scheduler:
|
||||||
|
workers: 3
|
||||||
|
pollingCheck: 5
|
||||||
|
scheduledCheck: 300
|
||||||
|
maxRetries: 2
|
||||||
|
retryBackoff: 60
|
||||||
|
historyExpiration: 90000
|
||||||
|
# DB defaults
|
||||||
|
api:
|
||||||
|
isEnabled: false
|
||||||
|
mail:
|
||||||
|
host: ''
|
||||||
|
secure: true
|
||||||
|
verifySSL: true
|
||||||
|
metrics:
|
||||||
|
isEnabled: false
|
||||||
|
auth:
|
||||||
|
autoLogin: false
|
||||||
|
enforce2FA: false
|
||||||
|
hideLocal: false
|
||||||
|
loginBgUrl: ''
|
||||||
|
audience: 'urn:wiki.js'
|
||||||
|
tokenExpiration: '30m'
|
||||||
|
tokenRenewal: '14d'
|
||||||
|
secret: 'abcdef1234567890abcdef1234567890abcdef'
|
||||||
|
security:
|
||||||
|
corsMode: 'OFF'
|
||||||
|
corsConfig: ''
|
||||||
|
enforceCsp: false
|
||||||
|
trustProxy: false
|
||||||
|
enforceHsts: false
|
||||||
|
disallowFloc: true
|
||||||
|
hstsDuration: 0
|
||||||
|
cspDirectives: ''
|
||||||
|
uploadScanSVG: true
|
||||||
|
disallowIframe: true
|
||||||
|
uploadMaxFiles: 20
|
||||||
|
authJwtAudience: 'urn:wiki.js'
|
||||||
|
authJwtExpiration: '30m'
|
||||||
|
uploadMaxFileSize: 10485760
|
||||||
|
forceAssetDownload: true
|
||||||
|
disallowOpenRedirect: true
|
||||||
|
authJwtRenewablePeriod: '14d'
|
||||||
|
enforceSameOriginReferrerPolicy: true
|
||||||
|
flags:
|
||||||
|
experimental: false
|
||||||
|
authDebug: false
|
||||||
|
sqlLog: false
|
||||||
|
userDefaults:
|
||||||
|
timezone: 'America/New_York'
|
||||||
|
dateFormat: 'YYYY-MM-DD'
|
||||||
|
timeFormat: '12h'
|
||||||
|
# System defaults
|
||||||
|
channel: NEXT
|
||||||
|
cors:
|
||||||
|
credentials: true
|
||||||
|
maxAge: 600
|
||||||
|
methods: 'GET,POST'
|
||||||
|
origin: true
|
||||||
|
maintainerEmail: security@requarks.io
|
||||||
|
tsDictMappings:
|
||||||
|
ar: arabic
|
||||||
|
hy: armenian
|
||||||
|
eu: basque
|
||||||
|
ca: catalan
|
||||||
|
da: danish
|
||||||
|
nl: dutch
|
||||||
|
en: english
|
||||||
|
fi: finnish
|
||||||
|
fr: french
|
||||||
|
de: german
|
||||||
|
el: greek
|
||||||
|
hi: hindi
|
||||||
|
hu: hungarian
|
||||||
|
id: indonesian
|
||||||
|
ga: irish
|
||||||
|
it: italian
|
||||||
|
lt: lithuanian
|
||||||
|
ne: nepali
|
||||||
|
no: norwegian
|
||||||
|
pt: portuguese
|
||||||
|
ro: romanian
|
||||||
|
ru: russian
|
||||||
|
sr: serbian
|
||||||
|
es: spanish
|
||||||
|
sv: swedish
|
||||||
|
ta: tamil
|
||||||
|
tr: turkish
|
||||||
|
yi: yiddish
|
||||||
|
editors:
|
||||||
|
asciidoc:
|
||||||
|
contentType: html
|
||||||
|
config: {}
|
||||||
|
markdown:
|
||||||
|
contentType: markdown
|
||||||
|
config:
|
||||||
|
allowHTML: true
|
||||||
|
linkify: true
|
||||||
|
lineBreaks: true
|
||||||
|
typographer: false
|
||||||
|
underline: false
|
||||||
|
tabWidth: 2
|
||||||
|
latexEngine: katex
|
||||||
|
kroki: true
|
||||||
|
plantuml: true
|
||||||
|
multimdTable: true
|
||||||
|
wysiwyg:
|
||||||
|
contentType: html
|
||||||
|
config: {}
|
||||||
|
systemIds:
|
||||||
|
localAuthId: '5a528c4c-0a82-4ad2-96a5-2b23811e6588'
|
||||||
|
guestsGroupId: '10000000-0000-4000-8000-000000000001'
|
||||||
|
usersGroupId: '20000000-0000-4000-8000-000000000002'
|
||||||
|
groups:
|
||||||
|
defaultPermissions:
|
||||||
|
- 'read:pages'
|
||||||
|
- 'read:assets'
|
||||||
|
- 'read:comments'
|
||||||
|
- 'write:comments'
|
||||||
|
defaultRules:
|
||||||
|
- name: Default Rule
|
||||||
|
mode: ALLOW
|
||||||
|
match: START
|
||||||
|
roles:
|
||||||
|
- 'read:pages'
|
||||||
|
- 'read:assets'
|
||||||
|
- 'read:comments'
|
||||||
|
- 'write:comments'
|
||||||
|
path: ''
|
||||||
|
locales: []
|
||||||
|
sites: []
|
||||||
@ -0,0 +1,152 @@
|
|||||||
|
import { defaultsDeep, get, isPlainObject } from 'lodash-es'
|
||||||
|
import chalk from 'chalk'
|
||||||
|
import cfgHelper from '../helpers/config.mjs'
|
||||||
|
import fs from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
import yaml from 'js-yaml'
|
||||||
|
import { v4 as uuid } from 'uuid'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* Load root config from disk
|
||||||
|
*/
|
||||||
|
async init (silent = false) {
|
||||||
|
const confPaths = {
|
||||||
|
config: path.join(WIKI.ROOTPATH, 'config.yml'),
|
||||||
|
data: path.join(WIKI.SERVERPATH, 'base.yml')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.CONFIG_FILE) {
|
||||||
|
confPaths.config = path.resolve(WIKI.ROOTPATH, process.env.CONFIG_FILE)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!silent) {
|
||||||
|
process.stdout.write(chalk.blue(`Loading configuration from ${confPaths.config}... `))
|
||||||
|
}
|
||||||
|
|
||||||
|
let appconfig = {}
|
||||||
|
let appdata = {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
appconfig = yaml.load(
|
||||||
|
cfgHelper.parseConfigValue(
|
||||||
|
await fs.readFile(confPaths.config, 'utf8')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
appdata = yaml.load(await fs.readFile(confPaths.data, 'utf8'))
|
||||||
|
if (!silent) {
|
||||||
|
console.info(chalk.green.bold('OK'))
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(chalk.red.bold('FAILED'))
|
||||||
|
console.error(err.message)
|
||||||
|
|
||||||
|
console.error(chalk.red.bold('>>> Unable to read configuration file! Did you create the config.yml file?'))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge with defaults
|
||||||
|
|
||||||
|
appconfig = defaultsDeep(appconfig, appdata.defaults.config)
|
||||||
|
|
||||||
|
// Override port
|
||||||
|
|
||||||
|
if (appconfig.port < 1) {
|
||||||
|
appconfig.port = process.env.PORT || 80
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.WIKI_PORT) {
|
||||||
|
appconfig.port = process.env.WIKI_PORT || 80
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load package info
|
||||||
|
|
||||||
|
const packageInfo = JSON.parse(await fs.readFile(path.join(WIKI.SERVERPATH, 'package.json'), 'utf-8'))
|
||||||
|
|
||||||
|
// Load DB Password from Docker Secret File
|
||||||
|
if (process.env.DB_PASS_FILE) {
|
||||||
|
if (!silent) {
|
||||||
|
console.info(chalk.blue('DB_PASS_FILE is defined. Will use secret from file.'))
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
appconfig.db.pass = await fs.readFile(process.env.DB_PASS_FILE, 'utf8').trim()
|
||||||
|
} catch (err) {
|
||||||
|
console.error(chalk.red.bold('>>> Failed to read Docker Secret File using path defined in DB_PASS_FILE env variable!'))
|
||||||
|
console.error(err.message)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WIKI.config = appconfig
|
||||||
|
WIKI.data = appdata
|
||||||
|
WIKI.version = packageInfo.version
|
||||||
|
WIKI.releaseDate = packageInfo.releaseDate
|
||||||
|
WIKI.devMode = (packageInfo.dev === true)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load config from DB
|
||||||
|
*/
|
||||||
|
async loadFromDb () {
|
||||||
|
WIKI.logger.info('Loading settings from DB...')
|
||||||
|
const conf = await WIKI.models.settings.getConfig()
|
||||||
|
if (conf) {
|
||||||
|
WIKI.config = defaultsDeep(conf, WIKI.config)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Save config to DB
|
||||||
|
*
|
||||||
|
* @param {Array} keys Array of keys to save
|
||||||
|
* @returns Promise
|
||||||
|
*/
|
||||||
|
async saveToDb (keys, propagate = true) {
|
||||||
|
try {
|
||||||
|
for (const key of keys) {
|
||||||
|
let value = get(WIKI.config, key, null)
|
||||||
|
if (!isPlainObject(value)) {
|
||||||
|
value = { v: value }
|
||||||
|
}
|
||||||
|
await WIKI.models.settings.updateConfig(key, value)
|
||||||
|
}
|
||||||
|
if (propagate) {
|
||||||
|
WIKI.events.outbound.emit('reloadConfig')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
WIKI.logger.error(`Failed to save configuration to DB: ${err.message}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Initialize DB tables with default values
|
||||||
|
*/
|
||||||
|
async initDbValues () {
|
||||||
|
const ids = {
|
||||||
|
groupAdminId: uuid(),
|
||||||
|
groupUserId: WIKI.data.systemIds.usersGroupId,
|
||||||
|
groupGuestId: WIKI.data.systemIds.guestsGroupId,
|
||||||
|
siteId: uuid(),
|
||||||
|
authModuleId: WIKI.data.systemIds.localAuthId,
|
||||||
|
userAdminId: uuid(),
|
||||||
|
userGuestId: uuid()
|
||||||
|
}
|
||||||
|
|
||||||
|
await WIKI.models.settings.init(ids)
|
||||||
|
await WIKI.models.sites.init(ids)
|
||||||
|
await WIKI.models.groups.init(ids)
|
||||||
|
await WIKI.models.authentication.init(ids)
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Subscribe to HA propagation events
|
||||||
|
*/
|
||||||
|
subscribeToEvents () {
|
||||||
|
WIKI.events.inbound.on('reloadConfig', async () => {
|
||||||
|
await WIKI.configSvc.loadFromDb()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,223 @@
|
|||||||
|
import { get, has, isEmpty, isPlainObject } from 'lodash-es'
|
||||||
|
import path from 'node:path'
|
||||||
|
import fs from 'node:fs/promises'
|
||||||
|
import { setTimeout } from 'node:timers/promises'
|
||||||
|
import { drizzle } from 'drizzle-orm/node-postgres'
|
||||||
|
import { migrate } from 'drizzle-orm/node-postgres/migrator'
|
||||||
|
import { Pool } from 'pg'
|
||||||
|
import PGPubSub from 'pg-pubsub'
|
||||||
|
import semver from 'semver'
|
||||||
|
|
||||||
|
import { createDeferred } from '../helpers/common.mjs'
|
||||||
|
// import migrationSource from '../db/migrator-source.mjs'
|
||||||
|
// const migrateFromLegacy = require('../db/legacy')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ORM DB module
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
pool: null,
|
||||||
|
listener: null,
|
||||||
|
config: null,
|
||||||
|
VERSION: null,
|
||||||
|
LEGACY: false,
|
||||||
|
onReady: createDeferred(),
|
||||||
|
connectAttempts: 0,
|
||||||
|
/**
|
||||||
|
* Initialize DB
|
||||||
|
*/
|
||||||
|
async init (workerMode = false) {
|
||||||
|
WIKI.logger.info('Checking DB configuration...')
|
||||||
|
|
||||||
|
// Fetch DB Config
|
||||||
|
|
||||||
|
this.config = (!isEmpty(process.env.DATABASE_URL))
|
||||||
|
? {
|
||||||
|
connectionString: process.env.DATABASE_URL
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
host: WIKI.config.db.host.toString(),
|
||||||
|
user: WIKI.config.db.user.toString(),
|
||||||
|
password: WIKI.config.db.pass.toString(),
|
||||||
|
database: WIKI.config.db.db.toString(),
|
||||||
|
port: WIKI.config.db.port
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle SSL Options
|
||||||
|
|
||||||
|
let dbUseSSL = (WIKI.config.db.ssl === true || WIKI.config.db.ssl === 'true' || WIKI.config.db.ssl === 1 || WIKI.config.db.ssl === '1')
|
||||||
|
let sslOptions = null
|
||||||
|
if (dbUseSSL && isPlainObject(this.config) && get(WIKI.config.db, 'sslOptions.auto', null) === false) {
|
||||||
|
sslOptions = WIKI.config.db.sslOptions
|
||||||
|
sslOptions.rejectUnauthorized = sslOptions.rejectUnauthorized !== false
|
||||||
|
if (sslOptions.ca && sslOptions.ca.indexOf('-----') !== 0) {
|
||||||
|
sslOptions.ca = await fs.readFile(path.resolve(WIKI.ROOTPATH, sslOptions.ca), 'utf-8')
|
||||||
|
}
|
||||||
|
if (sslOptions.cert) {
|
||||||
|
sslOptions.cert = await fs.readFile(path.resolve(WIKI.ROOTPATH, sslOptions.cert), 'utf-8')
|
||||||
|
}
|
||||||
|
if (sslOptions.key) {
|
||||||
|
sslOptions.key = await fs.readFile(path.resolve(WIKI.ROOTPATH, sslOptions.key), 'utf-8')
|
||||||
|
}
|
||||||
|
if (sslOptions.pfx) {
|
||||||
|
sslOptions.pfx = await fs.readFile(path.resolve(WIKI.ROOTPATH, sslOptions.pfx), 'utf-8')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sslOptions = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle inline SSL CA Certificate mode
|
||||||
|
if (!isEmpty(process.env.DB_SSL_CA)) {
|
||||||
|
const chunks = []
|
||||||
|
for (let i = 0, charsLength = process.env.DB_SSL_CA.length; i < charsLength; i += 64) {
|
||||||
|
chunks.push(process.env.DB_SSL_CA.substring(i, i + 64))
|
||||||
|
}
|
||||||
|
|
||||||
|
dbUseSSL = true
|
||||||
|
sslOptions = {
|
||||||
|
rejectUnauthorized: true,
|
||||||
|
ca: '-----BEGIN CERTIFICATE-----\n' + chunks.join('\n') + '\n-----END CERTIFICATE-----\n'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dbUseSSL && isPlainObject(this.config)) {
|
||||||
|
this.config.ssl = (sslOptions === true) ? { rejectUnauthorized: true } : sslOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Postgres Pool
|
||||||
|
|
||||||
|
this.pool = new Pool({
|
||||||
|
application_name: 'Wiki.js',
|
||||||
|
...this.config,
|
||||||
|
...workerMode ? { min: 0, max: 1 } : WIKI.config.pool,
|
||||||
|
options: `-c search_path=${WIKI.config.db.schema}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const db = drizzle({ client: this.pool })
|
||||||
|
|
||||||
|
// Connect
|
||||||
|
await this.connect(db)
|
||||||
|
|
||||||
|
// Check DB Version
|
||||||
|
const resVersion = await db.execute('SHOW server_version;')
|
||||||
|
const dbVersion = semver.coerce(resVersion.rows[0].server_version, { loose: true })
|
||||||
|
this.VERSION = dbVersion.version
|
||||||
|
if (dbVersion.major < 16) {
|
||||||
|
WIKI.logger.error(`Your PostgreSQL database version (${dbVersion.major}) is too old and unsupported by Wiki.js. Requires >= 16. Exiting...`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
WIKI.logger.info(`Using PostgreSQL v${dbVersion.version} [ OK ]`)
|
||||||
|
|
||||||
|
// DEV - Drop schema
|
||||||
|
if (WIKI.config.dev?.dropSchema) {
|
||||||
|
WIKI.logger.warn(`DEV MODE - Dropping schema ${WIKI.config.db.schema}...`)
|
||||||
|
await db.execute(`DROP SCHEMA IF EXISTS ${WIKI.config.db.schema} CASCADE;`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run Migrations
|
||||||
|
if (!workerMode) {
|
||||||
|
await this.syncSchemas(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Subscribe to database LISTEN / NOTIFY for multi-instances events
|
||||||
|
*/
|
||||||
|
async subscribeToNotifications () {
|
||||||
|
let connSettings = this.knex.client.connectionSettings
|
||||||
|
if (typeof connSettings === 'string') {
|
||||||
|
const encodedName = encodeURIComponent(`Wiki.js - ${WIKI.INSTANCE_ID}:PSUB`)
|
||||||
|
if (connSettings.indexOf('?') > 0) {
|
||||||
|
connSettings = `${connSettings}&ApplicationName=${encodedName}`
|
||||||
|
} else {
|
||||||
|
connSettings = `${connSettings}?ApplicationName=${encodedName}`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
connSettings.application_name = `Wiki.js - ${WIKI.INSTANCE_ID}:PSUB`
|
||||||
|
}
|
||||||
|
this.listener = new PGPubSub(connSettings, {
|
||||||
|
log (ev) {
|
||||||
|
WIKI.logger.debug(ev)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// -> Outbound events handling
|
||||||
|
|
||||||
|
this.listener.addChannel('wiki', payload => {
|
||||||
|
if (has(payload, 'event') && payload.source !== WIKI.INSTANCE_ID) {
|
||||||
|
WIKI.logger.info(`Received event ${payload.event} from instance ${payload.source}: [ OK ]`)
|
||||||
|
WIKI.events.inbound.emit(payload.event, payload.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
WIKI.events.outbound.onAny(this.notifyViaDB)
|
||||||
|
|
||||||
|
// -> Listen to inbound events
|
||||||
|
|
||||||
|
WIKI.auth.subscribeToEvents()
|
||||||
|
WIKI.configSvc.subscribeToEvents()
|
||||||
|
WIKI.db.pages.subscribeToEvents()
|
||||||
|
|
||||||
|
WIKI.logger.info('PG PubSub Listener initialized successfully: [ OK ]')
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Unsubscribe from database LISTEN / NOTIFY
|
||||||
|
*/
|
||||||
|
async unsubscribeToNotifications () {
|
||||||
|
if (this.listener) {
|
||||||
|
WIKI.events.outbound.offAny(this.notifyViaDB)
|
||||||
|
WIKI.events.inbound.removeAllListeners()
|
||||||
|
this.listener.close()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Publish event via database NOTIFY
|
||||||
|
*
|
||||||
|
* @param {string} event Event fired
|
||||||
|
* @param {object} value Payload of the event
|
||||||
|
*/
|
||||||
|
notifyViaDB (event, value) {
|
||||||
|
WIKI.db.listener.publish('wiki', {
|
||||||
|
source: WIKI.INSTANCE_ID,
|
||||||
|
event,
|
||||||
|
value
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Attempt initial connection
|
||||||
|
*/
|
||||||
|
async connect (db) {
|
||||||
|
try {
|
||||||
|
WIKI.logger.info('Connecting to database...')
|
||||||
|
await db.execute('SELECT 1 + 1;')
|
||||||
|
WIKI.logger.info('Database connection successful [ OK ]')
|
||||||
|
} catch (err) {
|
||||||
|
WIKI.logger.debug(err)
|
||||||
|
if (this.connectAttempts < 10) {
|
||||||
|
if (err.code) {
|
||||||
|
WIKI.logger.error(`Database connection error: ${err.code} ${err.address}:${err.port}`)
|
||||||
|
} else {
|
||||||
|
WIKI.logger.error(`Database connection error: ${err.message}`)
|
||||||
|
}
|
||||||
|
WIKI.logger.warn(`Will retry in 3 seconds... [Attempt ${++this.connectAttempts} of 10]`)
|
||||||
|
await setTimeout(3000)
|
||||||
|
await this.connect(db)
|
||||||
|
} else {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Migrate DB Schemas
|
||||||
|
*/
|
||||||
|
async syncSchemas (db) {
|
||||||
|
WIKI.logger.info('Ensuring DB schema exists...')
|
||||||
|
await db.execute(`CREATE SCHEMA IF NOT EXISTS ${WIKI.config.db.schema}`)
|
||||||
|
WIKI.logger.info('Ensuring DB migrations have been applied...')
|
||||||
|
return migrate(db, {
|
||||||
|
migrationsFolder: path.join(WIKI.SERVERPATH, 'db/migrations'),
|
||||||
|
migrationsSchema: WIKI.config.db.schema,
|
||||||
|
migrationsTable: 'migrations'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,119 @@
|
|||||||
|
import { padEnd } from 'lodash-es'
|
||||||
|
import eventemitter2 from 'eventemitter2'
|
||||||
|
import NodeCache from 'node-cache'
|
||||||
|
|
||||||
|
import asar from './asar.mjs'
|
||||||
|
import db from './db.mjs'
|
||||||
|
import extensions from './extensions.mjs'
|
||||||
|
import scheduler from './scheduler.mjs'
|
||||||
|
import servers from './servers.mjs'
|
||||||
|
import metrics from './metrics.mjs'
|
||||||
|
|
||||||
|
let isShuttingDown = false
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async init () {
|
||||||
|
WIKI.logger.info('=======================================')
|
||||||
|
WIKI.logger.info(`= Wiki.js ${padEnd(WIKI.version + ' ', 29, '=')}`)
|
||||||
|
WIKI.logger.info('=======================================')
|
||||||
|
WIKI.logger.info('Initializing...')
|
||||||
|
WIKI.logger.info(`Running node.js ${process.version}`)
|
||||||
|
|
||||||
|
WIKI.db = await db.init()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await WIKI.configSvc.loadFromDb()
|
||||||
|
await WIKI.configSvc.applyFlags()
|
||||||
|
} catch (err) {
|
||||||
|
WIKI.logger.error('Database Initialization Error: ' + err.message)
|
||||||
|
if (WIKI.IS_DEBUG) {
|
||||||
|
WIKI.logger.error(err)
|
||||||
|
}
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bootWeb()
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Pre-Web Boot Sequence
|
||||||
|
*/
|
||||||
|
async preBootWeb () {
|
||||||
|
try {
|
||||||
|
WIKI.cache = new NodeCache({ checkperiod: 0 })
|
||||||
|
WIKI.scheduler = await scheduler.init()
|
||||||
|
WIKI.servers = servers
|
||||||
|
WIKI.events = {
|
||||||
|
inbound: new eventemitter2.EventEmitter2(),
|
||||||
|
outbound: new eventemitter2.EventEmitter2()
|
||||||
|
}
|
||||||
|
WIKI.extensions = extensions
|
||||||
|
WIKI.asar = asar
|
||||||
|
WIKI.metrics = await metrics.init()
|
||||||
|
} catch (err) {
|
||||||
|
WIKI.logger.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Boot Web Process
|
||||||
|
*/
|
||||||
|
async bootWeb () {
|
||||||
|
try {
|
||||||
|
await this.preBootWeb()
|
||||||
|
await (await import('../web.mjs')).init()
|
||||||
|
this.postBootWeb()
|
||||||
|
} catch (err) {
|
||||||
|
WIKI.logger.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Post-Web Boot Sequence
|
||||||
|
*/
|
||||||
|
async postBootWeb () {
|
||||||
|
await WIKI.db.locales.refreshFromDisk()
|
||||||
|
|
||||||
|
await WIKI.db.analytics.refreshProvidersFromDisk()
|
||||||
|
await WIKI.db.authentication.refreshStrategiesFromDisk()
|
||||||
|
await WIKI.db.commentProviders.refreshProvidersFromDisk()
|
||||||
|
await WIKI.db.renderers.refreshRenderersFromDisk()
|
||||||
|
await WIKI.db.storage.refreshTargetsFromDisk()
|
||||||
|
|
||||||
|
await WIKI.extensions.init()
|
||||||
|
|
||||||
|
await WIKI.auth.activateStrategies()
|
||||||
|
await WIKI.db.commentProviders.initProvider()
|
||||||
|
await WIKI.db.locales.reloadCache()
|
||||||
|
await WIKI.db.sites.reloadCache()
|
||||||
|
await WIKI.db.storage.initTargets()
|
||||||
|
|
||||||
|
await WIKI.db.subscribeToNotifications()
|
||||||
|
await WIKI.scheduler.start()
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Graceful shutdown
|
||||||
|
*/
|
||||||
|
async shutdown (devMode = false) {
|
||||||
|
if (isShuttingDown) { return }
|
||||||
|
isShuttingDown = true
|
||||||
|
if (WIKI.servers) {
|
||||||
|
await WIKI.servers.stopServers()
|
||||||
|
}
|
||||||
|
if (WIKI.scheduler) {
|
||||||
|
await WIKI.scheduler.stop()
|
||||||
|
}
|
||||||
|
if (WIKI.models) {
|
||||||
|
await WIKI.db.unsubscribeToNotifications()
|
||||||
|
if (WIKI.db.knex) {
|
||||||
|
await WIKI.db.knex.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (WIKI.asar) {
|
||||||
|
await WIKI.asar.unload()
|
||||||
|
}
|
||||||
|
if (!devMode) {
|
||||||
|
WIKI.logger.info('Terminating process...')
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
-- Create PG Extensions --
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS ltree;
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
CREATE TABLE "authentication" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"module" varchar(255) NOT NULL,
|
||||||
|
"isEnabled" boolean DEFAULT false NOT NULL,
|
||||||
|
"displayName" varchar(255) DEFAULT '' NOT NULL,
|
||||||
|
"config" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||||
|
"registration" boolean DEFAULT false NOT NULL,
|
||||||
|
"allowedEmailRegex" varchar(255) DEFAULT '' NOT NULL,
|
||||||
|
"autoEnrollGroups" uuid[] DEFAULT '{}'
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "groups" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"name" varchar(255) NOT NULL,
|
||||||
|
"permissions" jsonb NOT NULL,
|
||||||
|
"rules" jsonb NOT NULL,
|
||||||
|
"redirectOnLogin" varchar(255) DEFAULT '' NOT NULL,
|
||||||
|
"redirectOnFirstLogin" varchar(255) DEFAULT '' NOT NULL,
|
||||||
|
"redirectOnLogout" varchar(255) DEFAULT '' NOT NULL,
|
||||||
|
"isSystem" boolean DEFAULT false NOT NULL,
|
||||||
|
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "settings" (
|
||||||
|
"key" varchar(255) PRIMARY KEY NOT NULL,
|
||||||
|
"value" jsonb DEFAULT '{}'::jsonb NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "sites" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"hostname" varchar(255) NOT NULL,
|
||||||
|
"isEnabled" boolean DEFAULT false NOT NULL,
|
||||||
|
"config" jsonb NOT NULL,
|
||||||
|
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "sites_hostname_unique" UNIQUE("hostname")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "users" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"email" varchar(255) NOT NULL,
|
||||||
|
"name" varchar(255) NOT NULL,
|
||||||
|
"auth" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||||
|
"meta" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||||
|
"passkeys" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||||
|
"prefs" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||||
|
"hasAvatar" boolean DEFAULT false NOT NULL,
|
||||||
|
"isActive" boolean DEFAULT false NOT NULL,
|
||||||
|
"isSystem" boolean DEFAULT false NOT NULL,
|
||||||
|
"isVerified" boolean DEFAULT false NOT NULL,
|
||||||
|
"lastLoginAt" timestamp,
|
||||||
|
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "users_email_unique" UNIQUE("email")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX "lastLoginAt_idx" ON "users" USING btree ("lastLoginAt");
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"id": "061e8c84-e05e-40b0-a074-7a56bd794fc7",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"views": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,379 @@
|
|||||||
|
{
|
||||||
|
"id": "8e212503-f07f-43d5-8e3d-9a3b9869b3cb",
|
||||||
|
"prevId": "061e8c84-e05e-40b0-a074-7a56bd794fc7",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.authentication": {
|
||||||
|
"name": "authentication",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"module": {
|
||||||
|
"name": "module",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"isEnabled": {
|
||||||
|
"name": "isEnabled",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"displayName": {
|
||||||
|
"name": "displayName",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "''"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"name": "config",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{}'::jsonb"
|
||||||
|
},
|
||||||
|
"registration": {
|
||||||
|
"name": "registration",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"allowedEmailRegex": {
|
||||||
|
"name": "allowedEmailRegex",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "''"
|
||||||
|
},
|
||||||
|
"autoEnrollGroups": {
|
||||||
|
"name": "autoEnrollGroups",
|
||||||
|
"type": "uuid[]",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "'{}'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.groups": {
|
||||||
|
"name": "groups",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"name": "permissions",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"name": "rules",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"redirectOnLogin": {
|
||||||
|
"name": "redirectOnLogin",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "''"
|
||||||
|
},
|
||||||
|
"redirectOnFirstLogin": {
|
||||||
|
"name": "redirectOnFirstLogin",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "''"
|
||||||
|
},
|
||||||
|
"redirectOnLogout": {
|
||||||
|
"name": "redirectOnLogout",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "''"
|
||||||
|
},
|
||||||
|
"isSystem": {
|
||||||
|
"name": "isSystem",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.settings": {
|
||||||
|
"name": "settings",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"key": {
|
||||||
|
"name": "key",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{}'::jsonb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.sites": {
|
||||||
|
"name": "sites",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"hostname": {
|
||||||
|
"name": "hostname",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"isEnabled": {
|
||||||
|
"name": "isEnabled",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"name": "config",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"sites_hostname_unique": {
|
||||||
|
"name": "sites_hostname_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"hostname"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.users": {
|
||||||
|
"name": "users",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"name": "auth",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{}'::jsonb"
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"name": "meta",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{}'::jsonb"
|
||||||
|
},
|
||||||
|
"passkeys": {
|
||||||
|
"name": "passkeys",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{}'::jsonb"
|
||||||
|
},
|
||||||
|
"prefs": {
|
||||||
|
"name": "prefs",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{}'::jsonb"
|
||||||
|
},
|
||||||
|
"hasAvatar": {
|
||||||
|
"name": "hasAvatar",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"isActive": {
|
||||||
|
"name": "isActive",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"isSystem": {
|
||||||
|
"name": "isSystem",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"isVerified": {
|
||||||
|
"name": "isVerified",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"lastLoginAt": {
|
||||||
|
"name": "lastLoginAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"lastLoginAt_idx": {
|
||||||
|
"name": "lastLoginAt_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "lastLoginAt",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"users_email_unique": {
|
||||||
|
"name": "users_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1768857200779,
|
||||||
|
"tag": "0000_init",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1769316465987,
|
||||||
|
"tag": "0001_main",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
import { boolean, index, jsonb, pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'
|
||||||
|
|
||||||
|
// AUTHENTICATION ----------------------
|
||||||
|
export const authenticationTable = pgTable('authentication', {
|
||||||
|
id: uuid().primaryKey().defaultRandom(),
|
||||||
|
module: varchar({ length: 255 }).notNull(),
|
||||||
|
isEnabled: boolean().notNull().default(false),
|
||||||
|
displayName: varchar({ length: 255 }).notNull().default(''),
|
||||||
|
config: jsonb().notNull().default({}),
|
||||||
|
registration: boolean().notNull().default(false),
|
||||||
|
allowedEmailRegex: varchar({ length: 255 }).notNull().default(''),
|
||||||
|
autoEnrollGroups: uuid().array().default([])
|
||||||
|
})
|
||||||
|
|
||||||
|
// GROUPS ------------------------------
|
||||||
|
export const groupsTable = pgTable('groups', {
|
||||||
|
id: uuid().primaryKey().defaultRandom(),
|
||||||
|
name: varchar({ length: 255 }).notNull(),
|
||||||
|
permissions: jsonb().notNull(),
|
||||||
|
rules: jsonb().notNull(),
|
||||||
|
redirectOnLogin: varchar({ length: 255 }).notNull().default(''),
|
||||||
|
redirectOnFirstLogin: varchar({ length: 255 }).notNull().default(''),
|
||||||
|
redirectOnLogout: varchar({ length: 255 }).notNull().default(''),
|
||||||
|
isSystem: boolean().notNull().default(false),
|
||||||
|
createdAt: timestamp().notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp().notNull().defaultNow()
|
||||||
|
})
|
||||||
|
|
||||||
|
// SETTINGS ----------------------------
|
||||||
|
export const settingsTable = pgTable('settings', {
|
||||||
|
key: varchar({ length: 255 }).notNull().primaryKey(),
|
||||||
|
value: jsonb().notNull().default({})
|
||||||
|
})
|
||||||
|
|
||||||
|
// SITES -------------------------------
|
||||||
|
export const sitesTable = pgTable('sites', {
|
||||||
|
id: uuid().primaryKey().defaultRandom(),
|
||||||
|
hostname: varchar({ length: 255 }).notNull().unique(),
|
||||||
|
isEnabled: boolean().notNull().default(false),
|
||||||
|
config: jsonb().notNull(),
|
||||||
|
createdAt: timestamp().notNull().defaultNow()
|
||||||
|
})
|
||||||
|
|
||||||
|
// USERS -------------------------------
|
||||||
|
export const usersTable = pgTable('users', {
|
||||||
|
id: uuid().primaryKey().defaultRandom(),
|
||||||
|
email: varchar({ length: 255 }).notNull().unique(),
|
||||||
|
name: varchar({ length: 255 }).notNull(),
|
||||||
|
auth: jsonb().notNull().default({}),
|
||||||
|
meta: jsonb().notNull().default({}),
|
||||||
|
passkeys: jsonb().notNull().default({}),
|
||||||
|
prefs: jsonb().notNull().default({}),
|
||||||
|
hasAvatar: boolean().notNull().default(false),
|
||||||
|
isActive: boolean().notNull().default(false),
|
||||||
|
isSystem: boolean().notNull().default(false),
|
||||||
|
isVerified: boolean().notNull().default(false),
|
||||||
|
lastLoginAt: timestamp(),
|
||||||
|
createdAt: timestamp().notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp().notNull().defaultNow()
|
||||||
|
}, (table) => [
|
||||||
|
index('lastLoginAt_idx').on(table.lastLoginAt)
|
||||||
|
])
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import neostandard from 'neostandard'
|
||||||
|
|
||||||
|
export default neostandard({
|
||||||
|
globals: {
|
||||||
|
document: false,
|
||||||
|
navigator: false,
|
||||||
|
window: false,
|
||||||
|
WIKI: true
|
||||||
|
},
|
||||||
|
ignores: [
|
||||||
|
'**/node_modules/**',
|
||||||
|
'**/*.min.js',
|
||||||
|
'coverage/**',
|
||||||
|
'repo/**',
|
||||||
|
'data/**',
|
||||||
|
'logs/**',
|
||||||
|
'locales/**'
|
||||||
|
]
|
||||||
|
})
|
||||||
@ -0,0 +1,112 @@
|
|||||||
|
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('.', '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode a tree path
|
||||||
|
*
|
||||||
|
* @param {string} str String to encode
|
||||||
|
* @returns Encoded tree path
|
||||||
|
*/
|
||||||
|
export function encodeTreePath (str) {
|
||||||
|
return str?.toLowerCase()?.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 : getTypeDefaultValue(value.type)
|
||||||
|
} else {
|
||||||
|
defaultValue = 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,
|
||||||
|
if: value.if ?? []
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDictNameFromLocale (locale) {
|
||||||
|
const loc = locale.length > 2 ? locale.substring(0, 2) : locale
|
||||||
|
if (loc in WIKI.config.search.dictOverrides) {
|
||||||
|
return WIKI.config.search.dictOverrides[loc]
|
||||||
|
} else {
|
||||||
|
return WIKI.data.tsDictMappings[loc] ?? 'simple'
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
import { replace } from 'lodash-es'
|
||||||
|
|
||||||
|
const isoDurationReg = /^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/
|
||||||
|
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* Parse configuration value for environment vars
|
||||||
|
*
|
||||||
|
* Replaces `$(ENV_VAR_NAME)` with value of `ENV_VAR_NAME` environment variable.
|
||||||
|
*
|
||||||
|
* Also supports defaults by if provided as `$(ENV_VAR_NAME:default)`
|
||||||
|
*
|
||||||
|
* @param {any} cfg Configuration value
|
||||||
|
* @returns Parse configuration value
|
||||||
|
*/
|
||||||
|
parseConfigValue (cfg) {
|
||||||
|
return replace(
|
||||||
|
cfg,
|
||||||
|
/\$\(([A-Z0-9_]+)(?::(.+))?\)/g,
|
||||||
|
(fm, m, d) => { return process.env[m] || d }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
isValidDurationString (val) {
|
||||||
|
return isoDurationReg.test(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,391 @@
|
|||||||
|
// ===========================================
|
||||||
|
// Wiki.js Server
|
||||||
|
// Licensed under AGPLv3
|
||||||
|
// ===========================================
|
||||||
|
|
||||||
|
import { existsSync } from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { DateTime } from 'luxon'
|
||||||
|
import semver from 'semver'
|
||||||
|
import { customAlphabet } from 'nanoid'
|
||||||
|
import { padEnd } from 'lodash-es'
|
||||||
|
|
||||||
|
import fastify from 'fastify'
|
||||||
|
import fastifyCompress from '@fastify/compress'
|
||||||
|
import fastifyCors from '@fastify/cors'
|
||||||
|
import fastifyCookie from '@fastify/cookie'
|
||||||
|
import fastifyFavicon from 'fastify-favicon'
|
||||||
|
import fastifyFormBody from '@fastify/formbody'
|
||||||
|
import fastifyHelmet from '@fastify/helmet'
|
||||||
|
import { Authenticator } from '@fastify/passport'
|
||||||
|
import fastifySensible from '@fastify/sensible'
|
||||||
|
import fastifySession from '@fastify/session'
|
||||||
|
import fastifyStatic from '@fastify/static'
|
||||||
|
import fastifySwagger from '@fastify/swagger'
|
||||||
|
import fastifySwaggerUi from '@fastify/swagger-ui'
|
||||||
|
import fastifyView from '@fastify/view'
|
||||||
|
import gracefulServer from '@gquittet/graceful-server'
|
||||||
|
import ajvFormats from 'ajv-formats'
|
||||||
|
import pug from 'pug'
|
||||||
|
|
||||||
|
import configSvc from './core/config.mjs'
|
||||||
|
import dbManager from './core/db.mjs'
|
||||||
|
import logger from './core/logger.mjs'
|
||||||
|
|
||||||
|
const nanoid = customAlphabet('1234567890abcdef', 10)
|
||||||
|
|
||||||
|
if (!semver.satisfies(process.version, '>=24')) {
|
||||||
|
console.error('ERROR: Node.js 24.x or later required!')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existsSync('./package.json')) {
|
||||||
|
console.error('ERROR: Must run server from the parent directory!')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const WIKI = {
|
||||||
|
IS_DEBUG: process.env.NODE_ENV === 'development',
|
||||||
|
ROOTPATH: process.cwd(),
|
||||||
|
INSTANCE_ID: nanoid(10),
|
||||||
|
SERVERPATH: path.join(process.cwd(), 'backend'),
|
||||||
|
configSvc,
|
||||||
|
sites: {},
|
||||||
|
sitesMappings: {},
|
||||||
|
startedAt: DateTime.utc(),
|
||||||
|
storage: {
|
||||||
|
defs: [],
|
||||||
|
modules: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
global.WIKI = WIKI
|
||||||
|
|
||||||
|
if (WIKI.IS_DEBUG) {
|
||||||
|
process.on('warning', (warning) => {
|
||||||
|
console.log(warning.stack)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await WIKI.configSvc.init()
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Init Logger
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
WIKI.logger = logger.init()
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Init Server
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
WIKI.logger.info('=======================================')
|
||||||
|
WIKI.logger.info(`= Wiki.js ${padEnd(WIKI.version + ' ', 29, '=')}`)
|
||||||
|
WIKI.logger.info('=======================================')
|
||||||
|
WIKI.logger.info('Initializing...')
|
||||||
|
WIKI.logger.info(`Running node.js ${process.version} [ OK ]`)
|
||||||
|
|
||||||
|
WIKI.dbManager = (await import('./core/db.mjs')).default
|
||||||
|
WIKI.db = await dbManager.init()
|
||||||
|
WIKI.models = (await import('./models/index.mjs')).default
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (await WIKI.configSvc.loadFromDb()) {
|
||||||
|
WIKI.logger.info('Settings merged with DB successfully [ OK ]')
|
||||||
|
} else {
|
||||||
|
WIKI.logger.warn('No settings found in DB. Initializing with defaults...')
|
||||||
|
await WIKI.configSvc.initDbValues()
|
||||||
|
|
||||||
|
if (!(await WIKI.configSvc.loadFromDb())) {
|
||||||
|
throw new Error('Settings table is empty! Could not initialize [ ERROR ]')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
WIKI.logger.error('Database Initialization Error: ' + err.message)
|
||||||
|
if (WIKI.IS_DEBUG) {
|
||||||
|
WIKI.logger.error(err)
|
||||||
|
}
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Init HTTP Server
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
async function initHTTPServer () {
|
||||||
|
// ----------------------------------------
|
||||||
|
// Load core modules
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
// WIKI.auth = auth.init()
|
||||||
|
// WIKI.mail = mail.init()
|
||||||
|
// WIKI.system = system.init()
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Initialize Fastify App
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
const app = fastify({
|
||||||
|
ajv: {
|
||||||
|
plugins: [
|
||||||
|
[ajvFormats, {}]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
bodyLimit: WIKI.config.bodyParserLimit || 5242880, // 5mb
|
||||||
|
logger: true,
|
||||||
|
trustProxy: WIKI.config.security.securityTrustProxy ?? false,
|
||||||
|
routerOptions: {
|
||||||
|
ignoreTrailingSlash: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
WIKI.app = app
|
||||||
|
WIKI.server = gracefulServer(app.server, {
|
||||||
|
livenessEndpoint: '/_live',
|
||||||
|
readinessEndpoint: '/_ready',
|
||||||
|
kubernetes: Boolean(process.env.KUBERNETES_SERVICE_HOST)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.register(fastifySensible)
|
||||||
|
app.register(fastifyCompress, { global: true })
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Handle graceful server shutdown
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
WIKI.server.on(gracefulServer.SHUTTING_DOWN, () => {
|
||||||
|
WIKI.logger.info('Shutting down HTTP Server... [ PENDING ]')
|
||||||
|
})
|
||||||
|
|
||||||
|
WIKI.server.on(gracefulServer.SHUTDOWN, (err) => {
|
||||||
|
WIKI.logger.info(`HTTP Server has exited: [ STOPPED ] (${err.message})`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Security
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
app.register(fastifyHelmet, {
|
||||||
|
contentSecurityPolicy: false, // TODO: Make it configurable
|
||||||
|
strictTransportSecurity: WIKI.config.security.securityHSTS
|
||||||
|
? {
|
||||||
|
maxAge: WIKI.config.security.securityHSTSDuration,
|
||||||
|
includeSubDomains: true
|
||||||
|
}
|
||||||
|
: false
|
||||||
|
})
|
||||||
|
|
||||||
|
app.register(fastifyCors, {
|
||||||
|
origin: '*', // TODO: Make it configurable
|
||||||
|
methods: ['GET', 'HEAD', 'POST', 'OPTIONS']
|
||||||
|
})
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Public Assets
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
app.register(fastifyFavicon, {
|
||||||
|
path: path.join(WIKI.ROOTPATH, 'assets'),
|
||||||
|
name: 'favicon.ico'
|
||||||
|
})
|
||||||
|
app.register(fastifyStatic, {
|
||||||
|
prefix: '/_assets/',
|
||||||
|
root: path.join(WIKI.ROOTPATH, 'assets/_assets'),
|
||||||
|
index: false,
|
||||||
|
maxAge: '7d',
|
||||||
|
decorateReply: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Blocks
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
app.register(fastifyStatic, {
|
||||||
|
prefix: '/_blocks/',
|
||||||
|
root: path.join(WIKI.ROOTPATH, 'blocks/compiled'),
|
||||||
|
index: false,
|
||||||
|
maxAge: '7d'
|
||||||
|
})
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Passport Authentication
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
app.register(fastifyCookie, {
|
||||||
|
secret: WIKI.config.auth.secret,
|
||||||
|
hook: 'onRequest'
|
||||||
|
})
|
||||||
|
app.register(fastifySession, {
|
||||||
|
secret: WIKI.config.auth.secret,
|
||||||
|
saveUninitialized: false,
|
||||||
|
store: {
|
||||||
|
get (sessionId, clb) {
|
||||||
|
|
||||||
|
},
|
||||||
|
set (sessionId, clb) {
|
||||||
|
|
||||||
|
},
|
||||||
|
destroy (sessionId, clb) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const fastifyPassport = new Authenticator()
|
||||||
|
app.register(fastifyPassport.initialize())
|
||||||
|
app.register(fastifyPassport.secureSession())
|
||||||
|
|
||||||
|
// app.use(WIKI.auth.passport.initialize())
|
||||||
|
// app.use(WIKI.auth.authenticate)
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// API Routes
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
app.register(fastifySwagger, {
|
||||||
|
hideUntagged: true,
|
||||||
|
openapi: {
|
||||||
|
openapi: '3.0.0',
|
||||||
|
info: {
|
||||||
|
title: 'Wiki.js API',
|
||||||
|
version: WIKI.config.version
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
securitySchemes: {
|
||||||
|
apiKeyAuth: {
|
||||||
|
type: 'apiKey',
|
||||||
|
in: 'header',
|
||||||
|
name: 'X-API-Key'
|
||||||
|
},
|
||||||
|
bearerAuth: {
|
||||||
|
type: 'http',
|
||||||
|
scheme: 'bearer',
|
||||||
|
bearerFormat: 'JWT'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
security: [
|
||||||
|
{ apiKeyAuth: [] },
|
||||||
|
{ bearerAuth: [] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
app.register(fastifySwaggerUi, {
|
||||||
|
routePrefix: '/_swagger'
|
||||||
|
})
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// SEO
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
app.addHook('onRequest', (req, reply, done) => {
|
||||||
|
console.log(req.raw.url)
|
||||||
|
const [urlPath, urlQuery] = req.raw.url.split('?')
|
||||||
|
if (urlPath.length > 1 && urlPath.endsWith('/')) {
|
||||||
|
const newPath = urlPath.slice(0, -1)
|
||||||
|
reply.redirect(urlQuery ? `${newPath}?${urlQuery}` : newPath, 301)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// View Engine Setup
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
app.register(fastifyView, {
|
||||||
|
engine: {
|
||||||
|
pug
|
||||||
|
}
|
||||||
|
})
|
||||||
|
app.register(fastifyFormBody, {
|
||||||
|
bodyLimit: 1048576 // 1mb
|
||||||
|
})
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// View accessible data
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
// app.locals.analyticsCode = {}
|
||||||
|
// app.locals.basedir = WIKI.ROOTPATH
|
||||||
|
// app.locals.config = WIKI.config
|
||||||
|
// app.locals.pageMeta = {
|
||||||
|
// title: '',
|
||||||
|
// description: WIKI.config.description,
|
||||||
|
// image: '',
|
||||||
|
// url: '/'
|
||||||
|
// }
|
||||||
|
// app.locals.devMode = WIKI.devMode
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Routing
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
// app.addHook('onRequest', async (req, reply, done) => {
|
||||||
|
// const currentSite = await WIKI.db.sites.getSiteByHostname({ hostname: req.hostname })
|
||||||
|
// if (!currentSite) {
|
||||||
|
// return reply.code(404).send('Site Not Found')
|
||||||
|
// }
|
||||||
|
|
||||||
|
// req.locals.siteConfig = {
|
||||||
|
// id: currentSite.id,
|
||||||
|
// title: currentSite.config.title,
|
||||||
|
// darkMode: currentSite.config.theme.dark,
|
||||||
|
// lang: currentSite.config.locales.primary,
|
||||||
|
// rtl: false, // TODO: handle RTL
|
||||||
|
// company: currentSite.config.company,
|
||||||
|
// contentLicense: currentSite.config.contentLicense
|
||||||
|
// }
|
||||||
|
// req.locals.theming = {
|
||||||
|
|
||||||
|
// }
|
||||||
|
// req.locals.langs = await WIKI.db.locales.getNavLocales({ cache: true })
|
||||||
|
// req.locals.analyticsCode = await WIKI.db.analytics.getCode({ cache: true })
|
||||||
|
// done()
|
||||||
|
// })
|
||||||
|
|
||||||
|
app.register(import('./api/index.mjs'), { prefix: '/_api' })
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Error handling
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
app.setErrorHandler((error, req, reply) => {
|
||||||
|
if (error instanceof fastify.errorCodes.FST_ERR_BAD_STATUS_CODE) {
|
||||||
|
WIKI.logger.warn(error)
|
||||||
|
reply.status(500).send({ ok: false })
|
||||||
|
} else {
|
||||||
|
reply.send(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Bind HTTP Server
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
try {
|
||||||
|
WIKI.logger.info(`Starting HTTP Server on port ${WIKI.config.port} [ STARTING ]`)
|
||||||
|
await app.listen({ port: WIKI.config.port, host: WIKI.config.bindIP })
|
||||||
|
WIKI.logger.info('HTTP Server: [ RUNNING ]')
|
||||||
|
WIKI.server.setReady()
|
||||||
|
} catch (err) {
|
||||||
|
app.log.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Register exit handler
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
// process.on('SIGINT', () => {
|
||||||
|
// WIKI.kernel.shutdown()
|
||||||
|
// })
|
||||||
|
// process.on('message', (msg) => {
|
||||||
|
// if (msg === 'shutdown') {
|
||||||
|
// WIKI.kernel.shutdown()
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Start HTTP Server
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
initHTTPServer()
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
import fs from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
import yaml from 'js-yaml'
|
||||||
|
import { parseModuleProps } from '../helpers/common.mjs'
|
||||||
|
import { authenticationTable } from '../db/schema.mjs'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentication model
|
||||||
|
*/
|
||||||
|
class Authentication {
|
||||||
|
async getStrategy (module) {
|
||||||
|
return WIKI.db.authentication.query().findOne({ module })
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStrategies ({ enabledOnly = false } = {}) {
|
||||||
|
return WIKI.db.authentication.query().where(enabledOnly ? { isEnabled: true } : {})
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshStrategiesFromDisk () {
|
||||||
|
try {
|
||||||
|
// -> Fetch definitions from disk
|
||||||
|
const authenticationDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/authentication'))
|
||||||
|
WIKI.data.authentication = []
|
||||||
|
for (const dir of authenticationDirs) {
|
||||||
|
const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/authentication', dir, 'definition.yml'), 'utf8')
|
||||||
|
const defParsed = yaml.load(def)
|
||||||
|
if (!defParsed.isAvailable) { continue }
|
||||||
|
defParsed.key = dir
|
||||||
|
defParsed.props = parseModuleProps(defParsed.props)
|
||||||
|
WIKI.data.authentication.push(defParsed)
|
||||||
|
WIKI.logger.debug(`Loaded authentication module definition ${dir}: [ OK ]`)
|
||||||
|
}
|
||||||
|
|
||||||
|
WIKI.logger.info(`Loaded ${WIKI.data.authentication.length} authentication module definitions: [ OK ]`)
|
||||||
|
} catch (err) {
|
||||||
|
WIKI.logger.error('Failed to scan or load authentication providers: [ FAILED ]')
|
||||||
|
WIKI.logger.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async init (ids) {
|
||||||
|
await WIKI.db.insert(authenticationTable).values({
|
||||||
|
id: ids.authModuleId,
|
||||||
|
module: 'local',
|
||||||
|
isEnabled: true,
|
||||||
|
displayName: 'Local Authentication',
|
||||||
|
config: {
|
||||||
|
emailValidation: true,
|
||||||
|
enforceTfa: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authentication = new Authentication()
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
import { v4 as uuid } from 'uuid'
|
||||||
|
import { groupsTable } from '../db/schema.mjs'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups model
|
||||||
|
*/
|
||||||
|
class Groups {
|
||||||
|
async init (ids) {
|
||||||
|
WIKI.logger.info('Inserting default groups...')
|
||||||
|
|
||||||
|
await WIKI.db.insert(groupsTable).values([
|
||||||
|
{
|
||||||
|
id: ids.groupAdminId,
|
||||||
|
name: 'Administrators',
|
||||||
|
permissions: ['manage:system'],
|
||||||
|
rules: [],
|
||||||
|
isSystem: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: ids.groupUserId,
|
||||||
|
name: 'Users',
|
||||||
|
permissions: ['read:pages', 'read:assets', 'read:comments'],
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
id: uuid(),
|
||||||
|
name: 'Default Rule',
|
||||||
|
roles: ['read:pages', 'read:assets', 'read:comments'],
|
||||||
|
match: 'START',
|
||||||
|
mode: 'ALLOW',
|
||||||
|
path: '',
|
||||||
|
locales: [],
|
||||||
|
sites: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
isSystem: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: ids.groupGuestId,
|
||||||
|
name: 'Guests',
|
||||||
|
permissions: ['read:pages', 'read:assets', 'read:comments'],
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
id: uuid(),
|
||||||
|
name: 'Default Rule',
|
||||||
|
roles: ['read:pages', 'read:assets', 'read:comments'],
|
||||||
|
match: 'START',
|
||||||
|
mode: 'DENY',
|
||||||
|
path: '',
|
||||||
|
locales: [],
|
||||||
|
sites: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
isSystem: true
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const groups = new Groups()
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import { authentication } from './authentication.mjs'
|
||||||
|
import { groups } from './groups.mjs'
|
||||||
|
import { settings } from './settings.mjs'
|
||||||
|
import { sites } from './sites.mjs'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
authentication,
|
||||||
|
groups,
|
||||||
|
settings,
|
||||||
|
sites
|
||||||
|
}
|
||||||
@ -0,0 +1,182 @@
|
|||||||
|
import { settingsTable } from '../db/schema.mjs'
|
||||||
|
import { has, reduce, set } from 'lodash-es'
|
||||||
|
import { pem2jwk } from 'pem-jwk'
|
||||||
|
import crypto from 'node:crypto'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings model
|
||||||
|
*/
|
||||||
|
class Settings {
|
||||||
|
/**
|
||||||
|
* Fetch settings from DB
|
||||||
|
* @returns {Promise<Object>} Settings
|
||||||
|
*/
|
||||||
|
async getConfig () {
|
||||||
|
const settings = await WIKI.db.select().from(settingsTable)
|
||||||
|
if (settings.length > 0) {
|
||||||
|
return reduce(settings, (res, val, key) => {
|
||||||
|
set(res, val.key, (has(val.value, 'v')) ? val.value.v : val.value)
|
||||||
|
return res
|
||||||
|
}, {})
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply settings to DB
|
||||||
|
* @param {string} key Setting key
|
||||||
|
* @param {Object} value Setting value object
|
||||||
|
*/
|
||||||
|
async updateConfig (key, value) {
|
||||||
|
await WIKI.models.insert(settingsTable)
|
||||||
|
.values({ key, value })
|
||||||
|
.onConflictDoUpdate({ target: settingsTable.key, set: { value } })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize settings table
|
||||||
|
* @param {Object} ids Generated IDs
|
||||||
|
*/
|
||||||
|
async init (ids) {
|
||||||
|
WIKI.logger.info('Generating certificates...')
|
||||||
|
const secret = crypto.randomBytes(32).toString('hex')
|
||||||
|
const certs = crypto.generateKeyPairSync('rsa', {
|
||||||
|
modulusLength: 2048,
|
||||||
|
publicKeyEncoding: {
|
||||||
|
type: 'pkcs1',
|
||||||
|
format: 'pem'
|
||||||
|
},
|
||||||
|
privateKeyEncoding: {
|
||||||
|
type: 'pkcs1',
|
||||||
|
format: 'pem',
|
||||||
|
cipher: 'aes-256-cbc',
|
||||||
|
passphrase: secret
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
WIKI.logger.info('Inserting default settings...')
|
||||||
|
await WIKI.db.insert(settingsTable).values([
|
||||||
|
{
|
||||||
|
key: 'api',
|
||||||
|
value: {
|
||||||
|
isEnabled: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'auth',
|
||||||
|
value: {
|
||||||
|
audience: 'urn:wiki.js',
|
||||||
|
tokenExpiration: '30m',
|
||||||
|
tokenRenewal: '14d',
|
||||||
|
certs: {
|
||||||
|
jwk: pem2jwk(certs.publicKey),
|
||||||
|
public: certs.publicKey,
|
||||||
|
private: certs.privateKey
|
||||||
|
},
|
||||||
|
secret,
|
||||||
|
rootAdminGroupId: ids.groupAdminId,
|
||||||
|
rootAdminUserId: ids.userAdminId,
|
||||||
|
guestUserId: ids.userGuestId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'flags',
|
||||||
|
value: {
|
||||||
|
experimental: false,
|
||||||
|
authDebug: false,
|
||||||
|
sqlLog: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'icons',
|
||||||
|
value: {
|
||||||
|
fa: {
|
||||||
|
isActive: true,
|
||||||
|
config: {
|
||||||
|
version: 6,
|
||||||
|
license: 'free',
|
||||||
|
token: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
la: {
|
||||||
|
isActive: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'mail',
|
||||||
|
value: {
|
||||||
|
senderName: '',
|
||||||
|
senderEmail: '',
|
||||||
|
defaultBaseURL: 'https://wiki.example.com',
|
||||||
|
host: '',
|
||||||
|
port: 465,
|
||||||
|
name: '',
|
||||||
|
secure: true,
|
||||||
|
verifySSL: true,
|
||||||
|
user: '',
|
||||||
|
pass: '',
|
||||||
|
useDKIM: false,
|
||||||
|
dkimDomainName: '',
|
||||||
|
dkimKeySelector: '',
|
||||||
|
dkimPrivateKey: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'metrics',
|
||||||
|
value: {
|
||||||
|
isEnabled: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'search',
|
||||||
|
value: {
|
||||||
|
termHighlighting: true,
|
||||||
|
dictOverrides: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'security',
|
||||||
|
value: {
|
||||||
|
corsConfig: '',
|
||||||
|
corsMode: 'OFF',
|
||||||
|
cspDirectives: '',
|
||||||
|
disallowFloc: true,
|
||||||
|
disallowIframe: true,
|
||||||
|
disallowOpenRedirect: true,
|
||||||
|
enforceCsp: false,
|
||||||
|
enforceHsts: false,
|
||||||
|
enforceSameOriginReferrerPolicy: true,
|
||||||
|
forceAssetDownload: true,
|
||||||
|
hstsDuration: 0,
|
||||||
|
trustProxy: false,
|
||||||
|
authJwtAudience: 'urn:wiki.js',
|
||||||
|
authJwtExpiration: '30m',
|
||||||
|
authJwtRenewablePeriod: '14d',
|
||||||
|
uploadMaxFileSize: 10485760,
|
||||||
|
uploadMaxFiles: 20,
|
||||||
|
uploadScanSVG: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'update',
|
||||||
|
value: {
|
||||||
|
lastCheckedAt: null,
|
||||||
|
version: WIKI.version,
|
||||||
|
versionDate: WIKI.releaseDate
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'userDefaults',
|
||||||
|
value: {
|
||||||
|
timezone: 'America/New_York',
|
||||||
|
dateFormat: 'YYYY-MM-DD',
|
||||||
|
timeFormat: '12h'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const settings = new Settings()
|
||||||
@ -0,0 +1,277 @@
|
|||||||
|
import { defaultsDeep, keyBy } from 'lodash-es'
|
||||||
|
import { sitesTable } from '../db/schema.mjs'
|
||||||
|
import { eq } from 'drizzle-orm'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sites model
|
||||||
|
*/
|
||||||
|
class Sites {
|
||||||
|
async getSiteByHostname ({ hostname, forceReload = false }) {
|
||||||
|
if (forceReload) {
|
||||||
|
await WIKI.db.sites.reloadCache()
|
||||||
|
}
|
||||||
|
const siteId = WIKI.sitesMappings[hostname] || WIKI.sitesMappings['*']
|
||||||
|
if (siteId) {
|
||||||
|
return WIKI.sites[siteId]
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async reloadCache () {
|
||||||
|
WIKI.logger.info('Reloading site configurations...')
|
||||||
|
const sites = await WIKI.db.sites.query().orderBy('id')
|
||||||
|
WIKI.sites = keyBy(sites, 'id')
|
||||||
|
WIKI.sitesMappings = {}
|
||||||
|
for (const site of sites) {
|
||||||
|
WIKI.sitesMappings[site.hostname] = site.id
|
||||||
|
}
|
||||||
|
WIKI.logger.info(`Loaded ${sites.length} site configurations [ OK ]`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async createSite (hostname, config) {
|
||||||
|
const newSite = await WIKI.db.sites.query().insertAndFetch({
|
||||||
|
hostname,
|
||||||
|
isEnabled: true,
|
||||||
|
config: defaultsDeep(config, {
|
||||||
|
title: 'My Wiki Site',
|
||||||
|
description: '',
|
||||||
|
company: '',
|
||||||
|
contentLicense: '',
|
||||||
|
footerExtra: '',
|
||||||
|
pageExtensions: ['md', 'html', 'txt'],
|
||||||
|
pageCasing: true,
|
||||||
|
discoverable: false,
|
||||||
|
defaults: {
|
||||||
|
tocDepth: {
|
||||||
|
min: 1,
|
||||||
|
max: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
browse: true,
|
||||||
|
ratings: false,
|
||||||
|
ratingsMode: 'off',
|
||||||
|
comments: false,
|
||||||
|
contributions: false,
|
||||||
|
profile: true,
|
||||||
|
search: true
|
||||||
|
},
|
||||||
|
logoUrl: '',
|
||||||
|
logoText: true,
|
||||||
|
sitemap: true,
|
||||||
|
robots: {
|
||||||
|
index: true,
|
||||||
|
follow: true
|
||||||
|
},
|
||||||
|
locales: {
|
||||||
|
primary: 'en',
|
||||||
|
active: ['en']
|
||||||
|
},
|
||||||
|
assets: {
|
||||||
|
logo: false,
|
||||||
|
logoExt: 'svg',
|
||||||
|
favicon: false,
|
||||||
|
faviconExt: 'svg',
|
||||||
|
loginBg: false
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
dark: false,
|
||||||
|
codeBlocksTheme: 'github-dark',
|
||||||
|
colorPrimary: '#1976D2',
|
||||||
|
colorSecondary: '#02C39A',
|
||||||
|
colorAccent: '#FF9800',
|
||||||
|
colorHeader: '#000000',
|
||||||
|
colorSidebar: '#1976D2',
|
||||||
|
injectCSS: '',
|
||||||
|
injectHead: '',
|
||||||
|
injectBody: '',
|
||||||
|
contentWidth: 'full',
|
||||||
|
sidebarPosition: 'left',
|
||||||
|
tocPosition: 'right',
|
||||||
|
showSharingMenu: true,
|
||||||
|
showPrintBtn: true,
|
||||||
|
baseFont: 'roboto',
|
||||||
|
contentFont: 'roboto'
|
||||||
|
},
|
||||||
|
editors: {
|
||||||
|
asciidoc: {
|
||||||
|
isActive: true,
|
||||||
|
config: {}
|
||||||
|
},
|
||||||
|
markdown: {
|
||||||
|
isActive: true,
|
||||||
|
config: {
|
||||||
|
allowHTML: true,
|
||||||
|
kroki: false,
|
||||||
|
krokiServerUrl: 'https://kroki.io',
|
||||||
|
latexEngine: 'katex',
|
||||||
|
lineBreaks: true,
|
||||||
|
linkify: true,
|
||||||
|
multimdTable: true,
|
||||||
|
plantuml: false,
|
||||||
|
plantumlServerUrl: 'https://www.plantuml.com/plantuml/',
|
||||||
|
quotes: 'english',
|
||||||
|
tabWidth: 2,
|
||||||
|
typographer: false,
|
||||||
|
underline: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
wysiwyg: {
|
||||||
|
isActive: true,
|
||||||
|
config: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
uploads: {
|
||||||
|
conflictBehavior: 'overwrite',
|
||||||
|
normalizeFilename: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
WIKI.logger.debug(`Creating new root navigation for site ${newSite.id}`)
|
||||||
|
|
||||||
|
await WIKI.db.navigation.query().insert({
|
||||||
|
id: newSite.id,
|
||||||
|
siteId: newSite.id,
|
||||||
|
items: []
|
||||||
|
})
|
||||||
|
|
||||||
|
WIKI.logger.debug(`Creating new DB storage for site ${newSite.id}`)
|
||||||
|
|
||||||
|
await WIKI.db.storage.query().insert({
|
||||||
|
module: 'db',
|
||||||
|
siteId: newSite.id,
|
||||||
|
isEnabled: true,
|
||||||
|
contentTypes: {
|
||||||
|
activeTypes: ['pages', 'images', 'documents', 'others', 'large'],
|
||||||
|
largeThreshold: '5MB'
|
||||||
|
},
|
||||||
|
assetDelivery: {
|
||||||
|
streaming: true,
|
||||||
|
directAccess: false
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
current: 'ok'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return newSite
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSite (id, patch) {
|
||||||
|
return WIKI.db.sites.query().findById(id).patch(patch)
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteSite (id) {
|
||||||
|
// await WIKI.db.storage.query().delete().where('siteId', id)
|
||||||
|
const deletedResult = await WIKI.db.delete(sitesTable).where(eq(sitesTable.id, id))
|
||||||
|
return Boolean(deletedResult.rowCount > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
async init (ids) {
|
||||||
|
WIKI.logger.info('Inserting default site...')
|
||||||
|
|
||||||
|
await WIKI.db.insert(sitesTable).values({
|
||||||
|
id: ids.siteId,
|
||||||
|
hostname: '*',
|
||||||
|
isEnabled: true,
|
||||||
|
config: {
|
||||||
|
title: 'Default Site',
|
||||||
|
description: '',
|
||||||
|
company: '',
|
||||||
|
contentLicense: '',
|
||||||
|
footerExtra: '',
|
||||||
|
pageExtensions: ['md', 'html', 'txt'],
|
||||||
|
pageCasing: true,
|
||||||
|
discoverable: false,
|
||||||
|
defaults: {
|
||||||
|
tocDepth: {
|
||||||
|
min: 1,
|
||||||
|
max: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
browse: true,
|
||||||
|
ratings: false,
|
||||||
|
ratingsMode: 'off',
|
||||||
|
comments: false,
|
||||||
|
contributions: false,
|
||||||
|
profile: true,
|
||||||
|
reasonForChange: 'required',
|
||||||
|
search: true
|
||||||
|
},
|
||||||
|
logoText: true,
|
||||||
|
sitemap: true,
|
||||||
|
robots: {
|
||||||
|
index: true,
|
||||||
|
follow: true
|
||||||
|
},
|
||||||
|
authStrategies: [{ id: ids.authModuleId, order: 0, isVisible: true }],
|
||||||
|
locales: {
|
||||||
|
primary: 'en',
|
||||||
|
active: ['en']
|
||||||
|
},
|
||||||
|
assets: {
|
||||||
|
logo: false,
|
||||||
|
logoExt: 'svg',
|
||||||
|
favicon: false,
|
||||||
|
faviconExt: 'svg',
|
||||||
|
loginBg: false
|
||||||
|
},
|
||||||
|
editors: {
|
||||||
|
asciidoc: {
|
||||||
|
isActive: true,
|
||||||
|
config: {}
|
||||||
|
},
|
||||||
|
markdown: {
|
||||||
|
isActive: true,
|
||||||
|
config: {
|
||||||
|
allowHTML: true,
|
||||||
|
kroki: false,
|
||||||
|
krokiServerUrl: 'https://kroki.io',
|
||||||
|
latexEngine: 'katex',
|
||||||
|
lineBreaks: true,
|
||||||
|
linkify: true,
|
||||||
|
multimdTable: true,
|
||||||
|
plantuml: false,
|
||||||
|
plantumlServerUrl: 'https://www.plantuml.com/plantuml/',
|
||||||
|
quotes: 'english',
|
||||||
|
tabWidth: 2,
|
||||||
|
typographer: false,
|
||||||
|
underline: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
wysiwyg: {
|
||||||
|
isActive: true,
|
||||||
|
config: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
dark: false,
|
||||||
|
codeBlocksTheme: 'github-dark',
|
||||||
|
colorPrimary: '#1976D2',
|
||||||
|
colorSecondary: '#02C39A',
|
||||||
|
colorAccent: '#FF9800',
|
||||||
|
colorHeader: '#000000',
|
||||||
|
colorSidebar: '#1976D2',
|
||||||
|
injectCSS: '',
|
||||||
|
injectHead: '',
|
||||||
|
injectBody: '',
|
||||||
|
contentWidth: 'full',
|
||||||
|
sidebarPosition: 'left',
|
||||||
|
tocPosition: 'right',
|
||||||
|
showSharingMenu: true,
|
||||||
|
showPrintBtn: true,
|
||||||
|
baseFont: 'roboto',
|
||||||
|
contentFont: 'roboto'
|
||||||
|
},
|
||||||
|
uploads: {
|
||||||
|
conflictBehavior: 'overwrite',
|
||||||
|
normalizeFilename: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sites = new Sites()
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"name": "wiki-backend",
|
||||||
|
"version": "3.0.0",
|
||||||
|
"releaseDate": "2026-01-01T01:01:01.000Z",
|
||||||
|
"description": "The most powerful and extensible open source Wiki software",
|
||||||
|
"main": "index.mjs",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"dev": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "cd .. && node backend",
|
||||||
|
"dev": "cd .. && nodemon backend --watch backend --ext mjs,js,json",
|
||||||
|
"ncu": "ncu -i",
|
||||||
|
"ncu-u": "ncu -u",
|
||||||
|
"db-generate": "drizzle-kit generate --dialect=postgresql --schema=./db/schema.mjs --out=./db/migrations --name=main"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/requarks/wiki.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"wiki",
|
||||||
|
"wikis",
|
||||||
|
"docs",
|
||||||
|
"documentation",
|
||||||
|
"markdown",
|
||||||
|
"guides",
|
||||||
|
"knowledge base"
|
||||||
|
],
|
||||||
|
"author": "Nicolas Giard",
|
||||||
|
"license": "AGPL-3.0",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/requarks/wiki/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/requarks/wiki#readme",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=24.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fastify/compress": "8.3.1",
|
||||||
|
"@fastify/cookie": "11.0.2",
|
||||||
|
"@fastify/cors": "11.2.0",
|
||||||
|
"@fastify/formbody": "8.0.2",
|
||||||
|
"@fastify/helmet": "13.0.2",
|
||||||
|
"@fastify/passport": "3.0.2",
|
||||||
|
"@fastify/sensible": "6.0.4",
|
||||||
|
"@fastify/session": "11.1.1",
|
||||||
|
"@fastify/static": "9.0.0",
|
||||||
|
"@fastify/swagger": "9.6.1",
|
||||||
|
"@fastify/swagger-ui": "5.2.4",
|
||||||
|
"@fastify/view": "11.1.1",
|
||||||
|
"@gquittet/graceful-server": "6.0.2",
|
||||||
|
"ajv-formats": "3.0.1",
|
||||||
|
"bcryptjs": "3.0.3",
|
||||||
|
"drizzle-orm": "0.45.1",
|
||||||
|
"fastify": "5.7.1",
|
||||||
|
"fastify-favicon": "5.0.0",
|
||||||
|
"lodash-es": "4.17.22",
|
||||||
|
"luxon": "3.7.2",
|
||||||
|
"nanoid": "5.1.6",
|
||||||
|
"pem-jwk": "2.0.0",
|
||||||
|
"pg": "8.16.3",
|
||||||
|
"pg-pubsub": "0.8.1",
|
||||||
|
"pug": "3.0.3",
|
||||||
|
"semver": "7.7.3",
|
||||||
|
"uuid": "13.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"drizzle-kit": "0.31.8",
|
||||||
|
"eslint": "9.32.0",
|
||||||
|
"eslint-plugin-import": "2.32.0",
|
||||||
|
"eslint-plugin-node": "11.1.0",
|
||||||
|
"eslint-plugin-promise": "7.2.1",
|
||||||
|
"neostandard": "0.12.2",
|
||||||
|
"nodemon": "3.1.10",
|
||||||
|
"npm-check-updates": "19.3.1"
|
||||||
|
},
|
||||||
|
"collective": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/wikijs",
|
||||||
|
"logo": "https://opencollective.com/opencollective/logo.txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in new issue