mirror of https://github.com/requarks/wiki
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
938 lines
28 KiB
938 lines
28 KiB
import path from 'node:path'
|
|
import os from 'node:os'
|
|
import { filesize } from 'filesize'
|
|
import { isNil } from 'es-toolkit/predicate'
|
|
import { gte, sql } from 'drizzle-orm'
|
|
import {
|
|
groups as groupsTable,
|
|
hooks as hooksTable,
|
|
pages as pagesTable,
|
|
tags as tagsTable,
|
|
users as usersTable
|
|
} from '../db/schema.ts'
|
|
import type { FastifyInstance } from 'fastify'
|
|
|
|
/**
|
|
* Every instance connected to this database, with how it is using the connection pool.
|
|
*
|
|
* There is no instance registry: an instance is only known by the connections it holds, which it
|
|
* labels `Wiki.js - <instance id>:<purpose>`. Two of those purposes hold a listener rather than
|
|
* doing query work, so they are counted apart.
|
|
*
|
|
* Shared by the list route and the dashboard count, so that the number on the dashboard is the
|
|
* number of rows the instances page shows.
|
|
*/
|
|
async function getInstances(): Promise<Record<string, any>[]> {
|
|
const instRaw = await WIKI.db.execute(
|
|
sql`SELECT usename, client_addr, application_name, backend_start, state_change FROM pg_stat_activity WHERE datname = ${WIKI.dbManager.dbName} AND application_name LIKE 'Wiki.js%'`
|
|
)
|
|
const insts: Record<string, any> = {}
|
|
for (const inst of instRaw.rows as any[]) {
|
|
const instId = inst.application_name.substring(10, 20)
|
|
const conType = [':MAIN', ':WORKER'].some((ct) => inst.application_name.endsWith(ct))
|
|
? 'main'
|
|
: 'sub'
|
|
// -> `db.execute()` with a raw SQL template returns timestamps as postgres-format strings
|
|
// (e.g. `2026-07-25 13:17:36.230177+00`) rather than Dates, which is what the previous
|
|
// `DateTime.fromSQL()` call was for. Temporal.Instant.from parses that format as-is,
|
|
// including the space separator and the hour-only `+00` offset. Rendered with
|
|
// millisecond precision to match the timestamps produced elsewhere.
|
|
inst.backend_start = Temporal.Instant.from(inst.backend_start).toString({
|
|
smallestUnit: 'millisecond'
|
|
})
|
|
inst.state_change = Temporal.Instant.from(inst.state_change).toString({
|
|
smallestUnit: 'millisecond'
|
|
})
|
|
const curInst = insts[instId] ?? {
|
|
activeConnections: 0,
|
|
activeListeners: 0,
|
|
dbFirstSeen: inst.backend_start,
|
|
dbLastSeen: inst.state_change
|
|
}
|
|
insts[instId] = {
|
|
id: instId,
|
|
activeConnections:
|
|
conType === 'main' ? curInst.activeConnections + 1 : curInst.activeConnections,
|
|
activeListeners: conType === 'sub' ? curInst.activeListeners + 1 : curInst.activeListeners,
|
|
dbUser: inst.usename,
|
|
dbFirstSeen:
|
|
curInst.dbFirstSeen > inst.backend_start ? inst.backend_start : curInst.dbFirstSeen,
|
|
dbLastSeen: curInst.dbLastSeen < inst.state_change ? inst.state_change : curInst.dbLastSeen,
|
|
ip: inst.client_addr
|
|
}
|
|
}
|
|
return Object.values(insts)
|
|
}
|
|
|
|
/**
|
|
* System API Routes
|
|
*/
|
|
async function routes(app: FastifyInstance) {
|
|
/**
|
|
* SYSTEM INFO
|
|
*/
|
|
app.get(
|
|
'/info',
|
|
{
|
|
config: {
|
|
permissions: ['access:admin']
|
|
},
|
|
schema: {
|
|
summary: 'System Info',
|
|
tags: ['System'],
|
|
response: {
|
|
200: {
|
|
description: 'System Info',
|
|
type: 'object',
|
|
properties: {
|
|
activeWorkers: {
|
|
type: 'number',
|
|
description:
|
|
'Jobs running right now on every instance combined, one worker slot each.'
|
|
},
|
|
configFile: {
|
|
type: 'string'
|
|
},
|
|
cpuCores: {
|
|
type: 'number'
|
|
},
|
|
currentVersion: {
|
|
type: 'string'
|
|
},
|
|
dbHost: {
|
|
type: 'string'
|
|
},
|
|
groupsTotal: {
|
|
type: 'number'
|
|
},
|
|
hostname: {
|
|
type: 'string'
|
|
},
|
|
httpPort: {
|
|
type: 'number'
|
|
},
|
|
instancesTotal: {
|
|
type: 'number',
|
|
description: 'Instances currently connected to this database.'
|
|
},
|
|
isMailConfigured: {
|
|
type: 'boolean'
|
|
},
|
|
isApiEnabled: {
|
|
type: 'boolean',
|
|
description: 'Whether API keys are accepted.'
|
|
},
|
|
isMetricsEnabled: {
|
|
type: 'boolean',
|
|
description: 'Whether the Prometheus metrics endpoint is turned on.'
|
|
},
|
|
isSchedulerHealthy: {
|
|
type: 'boolean',
|
|
description:
|
|
'False when no instance has refreshed the scheduler cron lock recently, i.e. scheduled jobs are no longer being queued.'
|
|
},
|
|
latestVersion: {
|
|
type: 'string'
|
|
},
|
|
latestVersionReleaseDate: {
|
|
type: 'string',
|
|
format: 'date-time'
|
|
},
|
|
loginsPastDay: {
|
|
type: 'number'
|
|
},
|
|
nodeVersion: {
|
|
type: 'string'
|
|
},
|
|
operatingSystem: {
|
|
type: 'string'
|
|
},
|
|
pagesTotal: {
|
|
type: 'number'
|
|
},
|
|
platform: {
|
|
type: 'string'
|
|
},
|
|
ramTotal: {
|
|
type: 'string'
|
|
},
|
|
tagsTotal: {
|
|
type: 'string'
|
|
},
|
|
upgradeCapable: {
|
|
type: 'boolean'
|
|
},
|
|
usersTotal: {
|
|
type: 'number'
|
|
},
|
|
webhooksTotal: {
|
|
type: 'number'
|
|
},
|
|
workingDirectory: {
|
|
type: 'string'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
async () => {
|
|
return {
|
|
activeWorkers: await WIKI.models.jobs.countActive(),
|
|
configFile: path.join(process.cwd(), 'config.yml'),
|
|
cpuCores: os.cpus().length,
|
|
currentVersion: WIKI.version,
|
|
dbHost: WIKI.config.db.host,
|
|
dbVersion: WIKI.dbManager.VERSION,
|
|
groupsTotal: await WIKI.db.$count(groupsTable),
|
|
hostname: os.hostname(),
|
|
httpPort: 0,
|
|
instancesTotal: (await getInstances()).length,
|
|
isApiEnabled: WIKI.config.api.isEnabled === true,
|
|
isMailConfigured: WIKI.config?.mail?.host?.length > 2,
|
|
isMetricsEnabled: WIKI.config.metrics.isEnabled === true,
|
|
isSchedulerHealthy: await WIKI.models.jobs.isHealthy(),
|
|
latestVersion: WIKI.config.update.version,
|
|
latestVersionReleaseDate: WIKI.config.update.versionDate,
|
|
loginsPastDay: await WIKI.db.$count(
|
|
usersTable,
|
|
gte(usersTable.lastLoginAt, sql`NOW() - INTERVAL '1 DAY'`)
|
|
),
|
|
nodeVersion: process.version.substring(1),
|
|
operatingSystem: `${os.type()} (${os.platform()}) ${os.release()} ${os.arch()}`,
|
|
pagesTotal: await WIKI.db.$count(pagesTable),
|
|
platform: os.platform(),
|
|
ramTotal: filesize(os.totalmem()),
|
|
tagsTotal: await WIKI.db.$count(tagsTable),
|
|
upgradeCapable: !isNil(process.env.UPGRADE_COMPANION),
|
|
usersTotal: await WIKI.db.$count(usersTable),
|
|
webhooksTotal: await WIKI.db.$count(hooksTable),
|
|
workingDirectory: process.cwd()
|
|
}
|
|
}
|
|
)
|
|
|
|
/**
|
|
* SYSTEM FLAGS
|
|
*/
|
|
app.get(
|
|
'/flags',
|
|
{
|
|
config: {
|
|
publicAccess: true
|
|
},
|
|
schema: {
|
|
summary: 'System Flags',
|
|
description:
|
|
'Readable without authentication: the frontend needs `experimental` before anyone has logged in, to know which unfinished features to reveal. A flag must therefore never carry anything sensitive.',
|
|
tags: ['System'],
|
|
response: {
|
|
200: { $ref: 'SystemFlags#' }
|
|
}
|
|
}
|
|
},
|
|
async () => {
|
|
return WIKI.models.flags.getFlags()
|
|
}
|
|
)
|
|
|
|
/**
|
|
* UPDATE SYSTEM FLAGS
|
|
*/
|
|
app.put<{ Body: Record<string, any> }>(
|
|
'/flags',
|
|
{
|
|
config: {
|
|
permissions: ['manage:system']
|
|
},
|
|
schema: {
|
|
summary: 'Update the system flags',
|
|
description:
|
|
'Accepts any subset of the flags. All of them take effect immediately, without a restart: `authDebug` and `sqlLog` write to the server log at info level, and `experimental` is picked up by the frontend on its next load.',
|
|
tags: ['System'],
|
|
body: { $ref: 'SystemFlags#' },
|
|
response: {
|
|
200: {
|
|
description: 'System flags updated successfully',
|
|
type: 'object',
|
|
properties: {
|
|
ok: {
|
|
type: 'boolean'
|
|
},
|
|
message: {
|
|
type: 'string'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
async (req, reply) => {
|
|
const patch = WIKI.models.flags.pickFlags(req.body)
|
|
if (Object.keys(patch).length < 1) {
|
|
return reply.badRequest('No system flags provided to update.')
|
|
}
|
|
if (!(await WIKI.models.flags.updateFlags(patch))) {
|
|
return reply.internalServerError('Failed to save the system flags.')
|
|
}
|
|
|
|
return {
|
|
ok: true,
|
|
message: 'System flags updated successfully.'
|
|
}
|
|
}
|
|
)
|
|
|
|
/**
|
|
* GET SECURITY CONFIGURATION
|
|
*/
|
|
app.get(
|
|
'/security',
|
|
{
|
|
config: {
|
|
permissions: ['manage:system']
|
|
},
|
|
schema: {
|
|
summary: 'Get the security configuration',
|
|
description:
|
|
'The JWT fields come from the `auth` settings, which are the ones actually in force. Most of the rest is applied when the HTTP server starts, so changing it takes effect on the next restart.',
|
|
tags: ['System'],
|
|
response: {
|
|
200: { $ref: 'SecurityConfig#' }
|
|
}
|
|
}
|
|
},
|
|
async () => {
|
|
return WIKI.models.security.getConfig()
|
|
}
|
|
)
|
|
|
|
/**
|
|
* UPDATE SECURITY CONFIGURATION
|
|
*/
|
|
app.put<{ Body: Record<string, any> }>(
|
|
'/security',
|
|
{
|
|
config: {
|
|
permissions: ['manage:system']
|
|
},
|
|
schema: {
|
|
summary: 'Update the security configuration',
|
|
description:
|
|
'Accepts any subset of the fields. Changing the JWT audience invalidates every API key already issued, since a key carries the audience it was signed with. Header, CORS and proxy settings are read when the HTTP server starts and therefore apply after a restart.',
|
|
tags: ['System'],
|
|
body: { $ref: 'SecurityConfig#' },
|
|
response: {
|
|
200: {
|
|
description: 'Security configuration updated successfully',
|
|
type: 'object',
|
|
properties: {
|
|
ok: {
|
|
type: 'boolean'
|
|
},
|
|
message: {
|
|
type: 'string'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
async (req, reply) => {
|
|
const patch = WIKI.models.security.pickFields(req.body)
|
|
if (Object.keys(patch).length < 1) {
|
|
return reply.badRequest('No security settings provided to update.')
|
|
}
|
|
|
|
const invalid = WIKI.models.security.validate(patch)
|
|
if (invalid) {
|
|
return reply.badRequest(invalid)
|
|
}
|
|
|
|
if (!(await WIKI.models.security.updateConfig(patch))) {
|
|
return reply.internalServerError('Failed to save the security configuration.')
|
|
}
|
|
|
|
return {
|
|
ok: true,
|
|
message: 'Security configuration updated successfully.'
|
|
}
|
|
}
|
|
)
|
|
|
|
/**
|
|
* GET SEARCH CONFIGURATION
|
|
*/
|
|
app.get(
|
|
'/search',
|
|
{
|
|
config: {
|
|
permissions: ['manage:system']
|
|
},
|
|
schema: {
|
|
summary: 'Get the search configuration',
|
|
description:
|
|
'Search is postgres full-text. `availableDictionaries` lists the text search configurations this database has, which is what a locale may be mapped to.',
|
|
tags: ['System'],
|
|
response: {
|
|
200: {
|
|
description: 'Search configuration',
|
|
type: 'object',
|
|
properties: {
|
|
termHighlighting: {
|
|
type: 'boolean'
|
|
},
|
|
dictOverrides: {
|
|
type: 'object',
|
|
description:
|
|
'Locale code to postgres dictionary, e.g. `{ "en": "english" }`. Overrides the built-in mapping.',
|
|
additionalProperties: { type: 'string' }
|
|
},
|
|
availableDictionaries: {
|
|
type: 'array',
|
|
description: 'Dictionary names this postgres installation knows.',
|
|
items: { type: 'string' }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
async () => {
|
|
return {
|
|
...WIKI.models.search.getConfig(),
|
|
availableDictionaries: await WIKI.models.search.getAvailableDictionaries()
|
|
}
|
|
}
|
|
)
|
|
|
|
/**
|
|
* UPDATE SEARCH CONFIGURATION
|
|
*/
|
|
app.put<{ Body: { termHighlighting?: boolean; dictOverrides?: Record<string, string> } }>(
|
|
'/search',
|
|
{
|
|
config: {
|
|
permissions: ['manage:system']
|
|
},
|
|
schema: {
|
|
summary: 'Update the search configuration',
|
|
description:
|
|
'Every dictionary named in `dictOverrides` must exist in this database, otherwise indexing would fail later, long after the setting was accepted. Changing a mapping affects pages the next time they are indexed — rebuild the index to apply it to existing content.',
|
|
tags: ['System'],
|
|
body: {
|
|
type: 'object',
|
|
properties: {
|
|
termHighlighting: {
|
|
type: 'boolean'
|
|
},
|
|
dictOverrides: {
|
|
type: 'object',
|
|
description: 'Locale code to postgres dictionary. Replaces the stored mapping.',
|
|
additionalProperties: { type: 'string' }
|
|
}
|
|
}
|
|
},
|
|
response: {
|
|
200: {
|
|
description: 'Search configuration updated successfully',
|
|
type: 'object',
|
|
properties: {
|
|
ok: {
|
|
type: 'boolean'
|
|
},
|
|
message: {
|
|
type: 'string'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
async (req, reply) => {
|
|
if (req.body.termHighlighting === undefined && req.body.dictOverrides === undefined) {
|
|
return reply.badRequest('No search settings provided to update.')
|
|
}
|
|
|
|
if (req.body.dictOverrides) {
|
|
const available = await WIKI.models.search.getAvailableDictionaries()
|
|
for (const [locale, dictionary] of Object.entries(req.body.dictOverrides)) {
|
|
if (!/^[a-z]{2,3}(?:[-_][A-Za-z]{2,4})?$/.test(locale)) {
|
|
return reply.badRequest(`"${locale}" is not a valid locale code.`)
|
|
}
|
|
if (!available.includes(dictionary)) {
|
|
return reply.badRequest(
|
|
`"${dictionary}" is not a text search dictionary in this database.`
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
const previousConfig = WIKI.config.search
|
|
WIKI.config.search = {
|
|
...previousConfig,
|
|
...(req.body.termHighlighting !== undefined && {
|
|
termHighlighting: req.body.termHighlighting
|
|
}),
|
|
...(req.body.dictOverrides !== undefined && { dictOverrides: req.body.dictOverrides })
|
|
}
|
|
|
|
if (!(await WIKI.configSvc.saveToDb(['search']))) {
|
|
WIKI.config.search = previousConfig
|
|
return reply.internalServerError('Failed to save the search configuration.')
|
|
}
|
|
|
|
return {
|
|
ok: true,
|
|
message: 'Search configuration updated successfully.'
|
|
}
|
|
}
|
|
)
|
|
|
|
/**
|
|
* REBUILD SEARCH INDEX
|
|
*/
|
|
app.post(
|
|
'/search/rebuild',
|
|
{
|
|
config: {
|
|
permissions: ['manage:system']
|
|
},
|
|
schema: {
|
|
summary: 'Rebuild the search index',
|
|
description:
|
|
'Queues a job that recomputes the search vector of every page from its stored content, using the dictionary mapping in force. Runs in the background: the response only says the job was queued.',
|
|
tags: ['System'],
|
|
response: {
|
|
200: {
|
|
description: 'Rebuild queued successfully',
|
|
type: 'object',
|
|
properties: {
|
|
ok: {
|
|
type: 'boolean'
|
|
},
|
|
message: {
|
|
type: 'string'
|
|
},
|
|
id: {
|
|
type: 'string',
|
|
format: 'uuid',
|
|
description: 'ID of the queued job, which the scheduler view lists.'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
async (req, reply) => {
|
|
const added = await WIKI.scheduler.addJob({ task: 'rebuildSearchIndex' })
|
|
if (!added?.id) {
|
|
return reply.internalServerError('The scheduler could not queue the rebuild.')
|
|
}
|
|
return {
|
|
ok: true,
|
|
message: 'Search index rebuild queued successfully.',
|
|
id: added.id
|
|
}
|
|
}
|
|
)
|
|
|
|
/**
|
|
* LIST EXTENSIONS
|
|
*/
|
|
app.get(
|
|
'/extensions',
|
|
{
|
|
config: {
|
|
permissions: ['manage:system']
|
|
},
|
|
schema: {
|
|
summary: 'List optional extensions',
|
|
description:
|
|
'Third-party tooling that unlocks extra functionality, with whether each one is present on this system. Detection runs per request, so installing a tool shows up without a restart.',
|
|
tags: ['System'],
|
|
response: {
|
|
200: {
|
|
description: 'List of extensions',
|
|
type: 'array',
|
|
items: { $ref: 'Extension#' }
|
|
}
|
|
}
|
|
}
|
|
},
|
|
async () => {
|
|
return WIKI.models.extensions.getExtensions()
|
|
}
|
|
)
|
|
|
|
/**
|
|
* INSTALL EXTENSION
|
|
*/
|
|
app.post<{ Params: { extensionKey: string } }>(
|
|
'/extensions/:extensionKey/install',
|
|
{
|
|
config: {
|
|
permissions: ['manage:system']
|
|
},
|
|
schema: {
|
|
summary: 'Install or reinstall an extension',
|
|
description:
|
|
'Only extensions flagged `isInstallable` can be installed from here — the npm packages, which are Sharp and Puppeteer. For Sharp this is mostly a repair: it already ships as an optional dependency, and refetching it replaces a prebuilt binary that is missing or does not match this OS and architecture. Puppeteer is not shipped at all, so this is a first install, and it fetches a Chromium build of a few hundred megabytes unless the server points at one it already has through `PUPPETEER_EXECUTABLE_PATH`. Git and Pandoc come from the operating system and answer 409 pointing at the documentation. Runs npm and can take minutes — allow the request a correspondingly long timeout.',
|
|
tags: ['System'],
|
|
params: {
|
|
type: 'object',
|
|
properties: {
|
|
extensionKey: {
|
|
type: 'string',
|
|
maxLength: 255
|
|
}
|
|
},
|
|
required: ['extensionKey']
|
|
},
|
|
response: {
|
|
200: {
|
|
description: 'Extension installed successfully',
|
|
type: 'object',
|
|
properties: {
|
|
ok: {
|
|
type: 'boolean'
|
|
},
|
|
message: {
|
|
type: 'string'
|
|
},
|
|
restartRequired: {
|
|
type: 'boolean',
|
|
description:
|
|
'True when this server already tried and failed to load the module. Node replays a failed module load for the life of the process, so the repaired files cannot be used until the server restarts.'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
async (req, reply) => {
|
|
const definition = WIKI.models.extensions.getDefinition(req.params.extensionKey)
|
|
if (!definition) {
|
|
return reply.notFound('Extension does not exist.')
|
|
}
|
|
if (!WIKI.models.extensions.isCompatible(definition)) {
|
|
return reply.conflict('This extension is not compatible with this system.')
|
|
}
|
|
if (definition.isInstallable !== true) {
|
|
return reply.conflict(
|
|
`${definition.title} must be installed manually. See the documentation for instructions.`
|
|
)
|
|
}
|
|
|
|
try {
|
|
await WIKI.models.extensions.install(definition)
|
|
} catch (err: any) {
|
|
// -> The message carries npm's own output, which is the only thing that explains a failure
|
|
// like a missing build toolchain. An administrator is the only caller.
|
|
return reply.internalServerError(err.message)
|
|
}
|
|
|
|
// -> A fresh install is usable at once, since nothing has tried to load it yet. Repairing one this
|
|
// process already choked on is a different story, and saying so beats leaving an administrator
|
|
// to wonder why nothing changed.
|
|
const restartRequired = WIKI.models.extensions.hasLoadFailed(definition)
|
|
|
|
return {
|
|
ok: true,
|
|
message: restartRequired
|
|
? `${definition.title} was reinstalled, but this server has to be restarted before it can use it.`
|
|
: `${definition.title} installed successfully.`,
|
|
restartRequired
|
|
}
|
|
}
|
|
)
|
|
|
|
/**
|
|
* GET API STATE
|
|
*/
|
|
app.get(
|
|
'/api',
|
|
{
|
|
config: {
|
|
permissions: ['manage:system']
|
|
},
|
|
schema: {
|
|
summary: 'Get the API state',
|
|
description:
|
|
'Whether API keys are accepted. While this is off, every request presenting a key is rejected, no matter how valid the key is.',
|
|
tags: ['System'],
|
|
response: {
|
|
200: {
|
|
description: 'API state',
|
|
type: 'object',
|
|
properties: {
|
|
isEnabled: {
|
|
type: 'boolean'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
async () => {
|
|
return { isEnabled: WIKI.config.api.isEnabled === true }
|
|
}
|
|
)
|
|
|
|
/**
|
|
* SET API STATE
|
|
*/
|
|
app.put<{ Body: { isEnabled: boolean } }>(
|
|
'/api',
|
|
{
|
|
config: {
|
|
permissions: ['manage:system']
|
|
},
|
|
schema: {
|
|
summary: 'Turn the API on or off',
|
|
description:
|
|
'Turning it off stops every API key from authenticating, without revoking any of them. Session-authenticated requests, i.e. the admin area itself, are unaffected.',
|
|
tags: ['System'],
|
|
body: {
|
|
type: 'object',
|
|
required: ['isEnabled'],
|
|
properties: {
|
|
isEnabled: {
|
|
type: 'boolean'
|
|
}
|
|
}
|
|
},
|
|
response: {
|
|
200: {
|
|
description: 'API state updated successfully',
|
|
type: 'object',
|
|
properties: {
|
|
ok: {
|
|
type: 'boolean'
|
|
},
|
|
message: {
|
|
type: 'string'
|
|
},
|
|
isEnabled: {
|
|
type: 'boolean'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
async (req, reply) => {
|
|
const previousConfig = WIKI.config.api
|
|
WIKI.config.api = { ...previousConfig, isEnabled: req.body.isEnabled }
|
|
|
|
if (!(await WIKI.configSvc.saveToDb(['api']))) {
|
|
WIKI.config.api = previousConfig
|
|
return reply.internalServerError('Failed to save the API state.')
|
|
}
|
|
|
|
return {
|
|
ok: true,
|
|
message: req.body.isEnabled ? 'API enabled successfully.' : 'API disabled successfully.',
|
|
isEnabled: req.body.isEnabled
|
|
}
|
|
}
|
|
)
|
|
|
|
/**
|
|
* GET METRICS ENDPOINT STATE
|
|
*/
|
|
app.get(
|
|
'/metrics',
|
|
{
|
|
config: {
|
|
permissions: ['manage:system']
|
|
},
|
|
schema: {
|
|
summary: 'Get the metrics endpoint state',
|
|
description:
|
|
'Whether the Prometheus metrics endpoint is turned on. The endpoint itself is not implemented yet — see the description of the PUT counterpart.',
|
|
tags: ['System'],
|
|
response: {
|
|
200: {
|
|
description: 'Metrics endpoint state',
|
|
type: 'object',
|
|
properties: {
|
|
isEnabled: {
|
|
type: 'boolean'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
async () => {
|
|
return { isEnabled: WIKI.config.metrics.isEnabled === true }
|
|
}
|
|
)
|
|
|
|
/**
|
|
* SET METRICS ENDPOINT STATE
|
|
*/
|
|
app.put<{ Body: { isEnabled: boolean } }>(
|
|
'/metrics',
|
|
{
|
|
config: {
|
|
permissions: ['manage:system']
|
|
},
|
|
schema: {
|
|
summary: 'Turn the metrics endpoint on or off',
|
|
description:
|
|
'Stores the state and nothing more, for now: the `/metrics` endpoint it governs is not implemented, and its documented `read:metrics` bearer authentication depends on API keys, which are not implemented either.',
|
|
tags: ['System'],
|
|
body: {
|
|
type: 'object',
|
|
required: ['isEnabled'],
|
|
properties: {
|
|
isEnabled: {
|
|
type: 'boolean'
|
|
}
|
|
}
|
|
},
|
|
response: {
|
|
200: {
|
|
description: 'Metrics endpoint state updated successfully',
|
|
type: 'object',
|
|
properties: {
|
|
ok: {
|
|
type: 'boolean'
|
|
},
|
|
message: {
|
|
type: 'string'
|
|
},
|
|
isEnabled: {
|
|
type: 'boolean'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
async (req, reply) => {
|
|
const previousConfig = WIKI.config.metrics
|
|
WIKI.config.metrics = { ...previousConfig, isEnabled: req.body.isEnabled }
|
|
|
|
if (!(await WIKI.configSvc.saveToDb(['metrics']))) {
|
|
WIKI.config.metrics = previousConfig
|
|
return reply.internalServerError('Failed to save the metrics endpoint state.')
|
|
}
|
|
|
|
return {
|
|
ok: true,
|
|
message: req.body.isEnabled
|
|
? 'Metrics endpoint enabled successfully.'
|
|
: 'Metrics endpoint disabled successfully.',
|
|
isEnabled: req.body.isEnabled
|
|
}
|
|
}
|
|
)
|
|
|
|
/**
|
|
* LIST SYSTEM INSTANCES
|
|
*/
|
|
app.get(
|
|
'/instances',
|
|
{
|
|
config: {
|
|
permissions: ['manage:system']
|
|
},
|
|
schema: {
|
|
summary: 'List System Instances',
|
|
tags: ['System'],
|
|
response: {
|
|
200: {
|
|
description: 'List of all system instances',
|
|
type: 'array',
|
|
items: {
|
|
type: 'object',
|
|
properties: {
|
|
id: {
|
|
type: 'string'
|
|
},
|
|
activeConnections: {
|
|
type: 'number'
|
|
},
|
|
activeListeners: {
|
|
type: 'number'
|
|
},
|
|
dbUser: {
|
|
type: 'string'
|
|
},
|
|
dbFirstSeen: {
|
|
type: 'string',
|
|
format: 'date-time'
|
|
},
|
|
dbLastSeen: {
|
|
type: 'string',
|
|
format: 'date-time'
|
|
},
|
|
ip: {
|
|
type: 'string'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
async () => {
|
|
return getInstances()
|
|
}
|
|
)
|
|
|
|
/**
|
|
* CHECK FOR UPDATE
|
|
*/
|
|
app.post(
|
|
'/checkForUpdate',
|
|
{
|
|
config: {
|
|
permissions: ['access:admin']
|
|
},
|
|
schema: {
|
|
summary: 'Check for Updates',
|
|
tags: ['System'],
|
|
response: {
|
|
200: {
|
|
description: 'Update Info',
|
|
type: 'object',
|
|
properties: {
|
|
current: {
|
|
type: 'string'
|
|
},
|
|
latest: {
|
|
type: 'string'
|
|
},
|
|
latestDate: {
|
|
type: 'string',
|
|
format: 'date-time'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
async () => {
|
|
const renderJob = await WIKI.scheduler.addJob({
|
|
task: 'checkVersion',
|
|
maxRetries: 0,
|
|
promise: true
|
|
})
|
|
// NOTE: `addJob` resolves to undefined if enqueueing failed, in which case this throws —
|
|
// preserving the existing behavior.
|
|
await renderJob!.promise
|
|
return {
|
|
current: WIKI.version,
|
|
latest: WIKI.config.update.version,
|
|
latestDate: WIKI.config.update.versionDate
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
export default routes
|