system.mjs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. import _ from 'lodash-es'
  2. import util from 'node:util'
  3. import getosSync from 'getos'
  4. import os from 'node:os'
  5. import { filesize } from 'filesize'
  6. import path from 'node:path'
  7. import fs from 'fs-extra'
  8. import { DateTime } from 'luxon'
  9. import { generateError, generateSuccess } from '../../helpers/graph.mjs'
  10. const getos = util.promisify(getosSync)
  11. export default {
  12. Query: {
  13. /**
  14. * Metrics Endpoint State
  15. */
  16. metricsState () {
  17. return WIKI.config.metrics?.isEnabled ?? false
  18. },
  19. /**
  20. * System Flags
  21. */
  22. systemFlags () {
  23. return WIKI.config.flags
  24. },
  25. /**
  26. * System Info
  27. */
  28. async systemInfo (obj, args, context) {
  29. if (!WIKI.auth.checkAccess(context.req.user, ['read:dashboard', 'manage:sites'])) {
  30. throw new Error('ERR_FORBIDDEN')
  31. }
  32. return {}
  33. },
  34. /**
  35. * System Extensions
  36. */
  37. async systemExtensions (obj, args, context) {
  38. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  39. throw new Error('ERR_FORBIDDEN')
  40. }
  41. const exts = Object.values(WIKI.extensions.ext).map(ext => _.pick(ext, ['key', 'title', 'description', 'isInstalled', 'isInstallable']))
  42. for (const ext of exts) {
  43. ext.isCompatible = await WIKI.extensions.ext[ext.key].isCompatible()
  44. }
  45. return exts
  46. },
  47. /**
  48. * List System Instances
  49. */
  50. async systemInstances (obj, args, context) {
  51. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  52. throw new Error('ERR_FORBIDDEN')
  53. }
  54. const instRaw = await WIKI.db.knex('pg_stat_activity')
  55. .select([
  56. 'usename',
  57. 'client_addr',
  58. 'application_name',
  59. 'backend_start',
  60. 'state_change'
  61. ])
  62. .where('datname', WIKI.db.knex.client.connectionSettings.database)
  63. .andWhereLike('application_name', 'Wiki.js%')
  64. const insts = {}
  65. for (const inst of instRaw) {
  66. const instId = inst.application_name.substring(10, 20)
  67. const conType = inst.application_name.includes(':MAIN') ? 'main' : 'sub'
  68. const curInst = insts[instId] ?? {
  69. activeConnections: 0,
  70. activeListeners: 0,
  71. dbFirstSeen: inst.backend_start,
  72. dbLastSeen: inst.state_change
  73. }
  74. insts[instId] = {
  75. id: instId,
  76. activeConnections: conType === 'main' ? curInst.activeConnections + 1 : curInst.activeConnections,
  77. activeListeners: conType === 'sub' ? curInst.activeListeners + 1 : curInst.activeListeners,
  78. dbUser: inst.usename,
  79. dbFirstSeen: curInst.dbFirstSeen > inst.backend_start ? inst.backend_start : curInst.dbFirstSeen,
  80. dbLastSeen: curInst.dbLastSeen < inst.state_change ? inst.state_change : curInst.dbLastSeen,
  81. ip: inst.client_addr
  82. }
  83. }
  84. return _.values(insts)
  85. },
  86. /**
  87. * System Security Settings
  88. */
  89. systemSecurity (obj, args, context) {
  90. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  91. throw new Error('ERR_FORBIDDEN')
  92. }
  93. return WIKI.config.security
  94. },
  95. /**
  96. * List System Jobs
  97. */
  98. async systemJobs (obj, args, context) {
  99. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  100. throw new Error('ERR_FORBIDDEN')
  101. }
  102. const results = args.states?.length > 0
  103. ? await WIKI.db.knex('jobHistory').whereIn('state', args.states.map(s => s.toLowerCase())).orderBy('startedAt', 'desc')
  104. : await WIKI.db.knex('jobHistory').orderBy('startedAt', 'desc')
  105. return results.map(r => ({
  106. ...r,
  107. state: r.state.toUpperCase()
  108. }))
  109. },
  110. /**
  111. * List Scheduled Jobs
  112. */
  113. async systemJobsScheduled (obj, args, context) {
  114. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  115. throw new Error('ERR_FORBIDDEN')
  116. }
  117. return WIKI.db.knex('jobSchedule').orderBy('task')
  118. },
  119. /**
  120. * List Upcoming Jobs
  121. */
  122. async systemJobsUpcoming (obj, args, context) {
  123. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  124. throw new Error('ERR_FORBIDDEN')
  125. }
  126. return WIKI.db.knex('jobs').orderBy([
  127. { column: 'waitUntil', order: 'asc', nulls: 'first' },
  128. { column: 'createdAt', order: 'asc' }
  129. ])
  130. },
  131. /**
  132. * Search Settings
  133. */
  134. systemSearch (obj, args, context) {
  135. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  136. throw new Error('ERR_FORBIDDEN')
  137. }
  138. return {
  139. ...WIKI.config.search,
  140. dictOverrides: JSON.stringify(WIKI.config.search.dictOverrides, null, 2)
  141. }
  142. }
  143. },
  144. Mutation: {
  145. async cancelJob (obj, args, context) {
  146. try {
  147. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  148. throw new Error('ERR_FORBIDDEN')
  149. }
  150. WIKI.logger.info(`Admin requested cancelling job ${args.id}...`)
  151. const result = await WIKI.db.knex('jobs')
  152. .where('id', args.id)
  153. .del()
  154. if (result === 1) {
  155. WIKI.logger.info(`Cancelled job ${args.id} [ OK ]`)
  156. } else {
  157. throw new Error('Job has already entered active state or does not exist.')
  158. }
  159. return {
  160. operation: generateSuccess('Cancelled job successfully.')
  161. }
  162. } catch (err) {
  163. if (err.message !== 'ERR_FORBIDDEN') {
  164. WIKI.logger.warn(err)
  165. }
  166. return generateError(err)
  167. }
  168. },
  169. async checkForUpdates (obj, args, context) {
  170. try {
  171. if (!WIKI.auth.checkAccess(context.req.user, ['read:dashboard', 'manage:system'])) {
  172. throw new Error('ERR_FORBIDDEN')
  173. }
  174. const renderJob = await WIKI.scheduler.addJob({
  175. task: 'checkVersion',
  176. maxRetries: 0,
  177. promise: true
  178. })
  179. await renderJob.promise
  180. return {
  181. operation: generateSuccess('Checked for latest version successfully.'),
  182. current: WIKI.version,
  183. latest: WIKI.config.update.version,
  184. latestDate: WIKI.config.update.versionDate
  185. }
  186. } catch (err) {
  187. if (err.message !== 'ERR_FORBIDDEN') {
  188. WIKI.logger.warn(err)
  189. }
  190. return generateError(err)
  191. }
  192. },
  193. async disconnectWS (obj, args, context) {
  194. try {
  195. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  196. throw new Error('ERR_FORBIDDEN')
  197. }
  198. WIKI.servers.ws.disconnectSockets(true)
  199. WIKI.logger.info('All active websocket connections have been terminated.')
  200. return {
  201. operation: generateSuccess('All websocket connections closed successfully.')
  202. }
  203. } catch (err) {
  204. if (err.message !== 'ERR_FORBIDDEN') {
  205. WIKI.logger.warn(err)
  206. }
  207. return generateError(err)
  208. }
  209. },
  210. async installExtension (obj, args, context) {
  211. try {
  212. await WIKI.extensions.ext[args.key].install()
  213. // TODO: broadcast ext install
  214. return {
  215. operation: generateSuccess('Extension installed successfully')
  216. }
  217. } catch (err) {
  218. return generateError(err)
  219. }
  220. },
  221. async rebuildSearchIndex (obj, args, context) {
  222. try {
  223. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  224. throw new Error('ERR_FORBIDDEN')
  225. }
  226. await WIKI.scheduler.addJob({
  227. task: 'rebuildSearchIndex',
  228. maxRetries: 0
  229. })
  230. return {
  231. operation: generateSuccess('Search index rebuild has been scheduled and will start shortly.')
  232. }
  233. } catch (err) {
  234. return generateError(err)
  235. }
  236. },
  237. async retryJob (obj, args, context) {
  238. try {
  239. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  240. throw new Error('ERR_FORBIDDEN')
  241. }
  242. WIKI.logger.info(`Admin requested rescheduling of job ${args.id}...`)
  243. const job = await WIKI.db.knex('jobHistory')
  244. .where('id', args.id)
  245. .first()
  246. if (!job) {
  247. throw new Error('No such job found.')
  248. } else if (job.state === 'interrupted') {
  249. throw new Error('Cannot reschedule a task that has been interrupted. It will automatically be retried shortly.')
  250. } else if (job.state === 'failed' && job.attempt < job.maxRetries) {
  251. throw new Error('Cannot reschedule a task that has not reached its maximum retry attempts.')
  252. }
  253. await WIKI.db.knex('jobs')
  254. .insert({
  255. id: job.id,
  256. task: job.task,
  257. useWorker: job.useWorker,
  258. payload: job.payload,
  259. retries: job.attempt,
  260. maxRetries: job.maxRetries,
  261. isScheduled: job.wasScheduled,
  262. createdBy: WIKI.INSTANCE_ID
  263. })
  264. WIKI.logger.info(`Job ${args.id} has been rescheduled [ OK ]`)
  265. return {
  266. operation: generateSuccess('Job rescheduled successfully.')
  267. }
  268. } catch (err) {
  269. WIKI.logger.warn(err)
  270. return generateError(err)
  271. }
  272. },
  273. /**
  274. * Set Metrics endpoint state
  275. */
  276. async setMetricsState (obj, args, context) {
  277. try {
  278. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  279. throw new Error('ERR_FORBIDDEN')
  280. }
  281. WIKI.config.metrics.isEnabled = args.enabled
  282. await WIKI.configSvc.saveToDb(['metrics'])
  283. return {
  284. operation: generateSuccess('Metrics endpoint state changed successfully')
  285. }
  286. } catch (err) {
  287. return generateError(err)
  288. }
  289. },
  290. async updateSystemFlags (obj, args, context) {
  291. try {
  292. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  293. throw new Error('ERR_FORBIDDEN')
  294. }
  295. WIKI.config.flags = {
  296. ...WIKI.config.flags,
  297. ...args.flags
  298. }
  299. await WIKI.configSvc.applyFlags()
  300. await WIKI.configSvc.saveToDb(['flags'])
  301. return {
  302. operation: generateSuccess('System Flags applied successfully')
  303. }
  304. } catch (err) {
  305. WIKI.logger.warn(err)
  306. return generateError(err)
  307. }
  308. },
  309. async updateSystemSearch (obj, args, context) {
  310. try {
  311. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  312. throw new Error('ERR_FORBIDDEN')
  313. }
  314. WIKI.config.search = {
  315. ...WIKI.config.search,
  316. termHighlighting: args.termHighlighting ?? WIKI.config.search.termHighlighting,
  317. dictOverrides: args.dictOverrides ? JSON.parse(args.dictOverrides) : WIKI.config.search.dictOverrides
  318. }
  319. // TODO: broadcast config update
  320. await WIKI.configSvc.saveToDb(['search'])
  321. return {
  322. operation: generateSuccess('System Search configuration applied successfully')
  323. }
  324. } catch (err) {
  325. WIKI.logger.warn(err)
  326. return generateError(err)
  327. }
  328. },
  329. async updateSystemSecurity (obj, args, context) {
  330. try {
  331. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  332. throw new Error('ERR_FORBIDDEN')
  333. }
  334. WIKI.config.security = _.defaultsDeep(_.omit(args, ['__typename']), WIKI.config.security)
  335. // TODO: broadcast config update
  336. await WIKI.configSvc.saveToDb(['security'])
  337. return {
  338. operation: generateSuccess('System Security configuration applied successfully')
  339. }
  340. } catch (err) {
  341. WIKI.logger.warn(err)
  342. return generateError(err)
  343. }
  344. }
  345. },
  346. SystemInfo: {
  347. configFile () {
  348. return path.join(process.cwd(), 'config.yml')
  349. },
  350. cpuCores () {
  351. return os.cpus().length
  352. },
  353. currentVersion () {
  354. return WIKI.version
  355. },
  356. dbHost () {
  357. return WIKI.config.db.host
  358. },
  359. dbVersion () {
  360. return WIKI.db.VERSION
  361. },
  362. hostname () {
  363. return os.hostname()
  364. },
  365. httpPort () {
  366. return WIKI.servers.servers.http ? _.get(WIKI.servers.servers.http.address(), 'port', 0) : 0
  367. },
  368. httpRedirection () {
  369. return _.get(WIKI.config, 'server.sslRedir', false)
  370. },
  371. httpsPort () {
  372. return WIKI.servers.servers.https ? _.get(WIKI.servers.servers.https.address(), 'port', 0) : 0
  373. },
  374. isMailConfigured () {
  375. return WIKI.config?.mail?.host?.length > 2
  376. },
  377. async isSchedulerHealthy () {
  378. const results = await WIKI.db.knex('jobHistory').count('* as total').whereIn('state', ['failed', 'interrupted']).andWhere('startedAt', '>=', DateTime.utc().minus({ days: 1 }).toISO()).first()
  379. return _.toSafeInteger(results?.total) === 0
  380. },
  381. latestVersion () {
  382. return WIKI.config.update.version
  383. },
  384. latestVersionReleaseDate () {
  385. return DateTime.fromISO(WIKI.config.update.versionDate).toJSDate()
  386. },
  387. nodeVersion () {
  388. return process.version.substring(1)
  389. },
  390. async operatingSystem () {
  391. let osLabel = `${os.type()} (${os.platform()}) ${os.release()} ${os.arch()}`
  392. if (os.platform() === 'linux') {
  393. const osInfo = await getos()
  394. osLabel = `${os.type()} - ${osInfo.dist} (${osInfo.codename || os.platform()}) ${osInfo.release || os.release()} ${os.arch()}`
  395. }
  396. return osLabel
  397. },
  398. async platform () {
  399. const isDockerized = await fs.pathExists('/.dockerenv')
  400. if (isDockerized) {
  401. return 'docker'
  402. }
  403. return os.platform()
  404. },
  405. ramTotal () {
  406. return filesize(os.totalmem())
  407. },
  408. sslDomain () {
  409. return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === 'letsencrypt' ? WIKI.config.ssl.domain : null
  410. },
  411. sslExpirationDate () {
  412. return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === 'letsencrypt' ? _.get(WIKI.config.letsencrypt, 'payload.expires', null) : null
  413. },
  414. sslProvider () {
  415. return WIKI.config.ssl.enabled ? WIKI.config.ssl.provider : null
  416. },
  417. sslStatus () {
  418. return 'OK'
  419. },
  420. sslSubscriberEmail () {
  421. return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === 'letsencrypt' ? WIKI.config.ssl.subscriberEmail : null
  422. },
  423. async upgradeCapable () {
  424. return !_.isNil(process.env.UPGRADE_COMPANION)
  425. },
  426. workingDirectory () {
  427. return process.cwd()
  428. },
  429. async groupsTotal () {
  430. const total = await WIKI.db.groups.query().count('* as total').first()
  431. return _.toSafeInteger(total.total)
  432. },
  433. async pagesTotal () {
  434. const total = await WIKI.db.pages.query().count('* as total').first()
  435. return _.toSafeInteger(total.total)
  436. },
  437. async tagsTotal () {
  438. const total = await WIKI.db.tags.query().count('* as total').first()
  439. return _.toSafeInteger(total.total)
  440. },
  441. async usersTotal () {
  442. const total = await WIKI.db.users.query().count('* as total').first()
  443. return _.toSafeInteger(total.total)
  444. },
  445. async loginsPastDay () {
  446. const total = await WIKI.db.users.query().count('* as total').whereRaw('"lastLoginAt" >= NOW() - INTERVAL \'1 DAY\'').first()
  447. return _.toSafeInteger(total.total)
  448. }
  449. }
  450. }