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