system.mjs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  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. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  213. throw new Error('ERR_FORBIDDEN')
  214. }
  215. await WIKI.extensions.ext[args.key].install()
  216. // TODO: broadcast ext install
  217. return {
  218. operation: generateSuccess('Extension installed successfully')
  219. }
  220. } catch (err) {
  221. return generateError(err)
  222. }
  223. },
  224. async rebuildSearchIndex (obj, args, context) {
  225. try {
  226. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  227. throw new Error('ERR_FORBIDDEN')
  228. }
  229. await WIKI.scheduler.addJob({
  230. task: 'rebuildSearchIndex',
  231. maxRetries: 0
  232. })
  233. return {
  234. operation: generateSuccess('Search index rebuild has been scheduled and will start shortly.')
  235. }
  236. } catch (err) {
  237. return generateError(err)
  238. }
  239. },
  240. async retryJob (obj, args, context) {
  241. try {
  242. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  243. throw new Error('ERR_FORBIDDEN')
  244. }
  245. WIKI.logger.info(`Admin requested rescheduling of job ${args.id}...`)
  246. const job = await WIKI.db.knex('jobHistory')
  247. .where('id', args.id)
  248. .first()
  249. if (!job) {
  250. throw new Error('No such job found.')
  251. } else if (job.state === 'interrupted') {
  252. throw new Error('Cannot reschedule a task that has been interrupted. It will automatically be retried shortly.')
  253. } else if (job.state === 'failed' && job.attempt < job.maxRetries) {
  254. throw new Error('Cannot reschedule a task that has not reached its maximum retry attempts.')
  255. }
  256. await WIKI.db.knex('jobs')
  257. .insert({
  258. id: job.id,
  259. task: job.task,
  260. useWorker: job.useWorker,
  261. payload: job.payload,
  262. retries: job.attempt,
  263. maxRetries: job.maxRetries,
  264. isScheduled: job.wasScheduled,
  265. createdBy: WIKI.INSTANCE_ID
  266. })
  267. WIKI.logger.info(`Job ${args.id} has been rescheduled [ OK ]`)
  268. return {
  269. operation: generateSuccess('Job rescheduled successfully.')
  270. }
  271. } catch (err) {
  272. WIKI.logger.warn(err)
  273. return generateError(err)
  274. }
  275. },
  276. /**
  277. * Set Metrics endpoint state
  278. */
  279. async setMetricsState (obj, args, context) {
  280. try {
  281. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  282. throw new Error('ERR_FORBIDDEN')
  283. }
  284. WIKI.config.metrics.isEnabled = args.enabled
  285. await WIKI.configSvc.saveToDb(['metrics'])
  286. return {
  287. operation: generateSuccess('Metrics endpoint state changed successfully')
  288. }
  289. } catch (err) {
  290. return generateError(err)
  291. }
  292. },
  293. async updateSystemFlags (obj, args, context) {
  294. try {
  295. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  296. throw new Error('ERR_FORBIDDEN')
  297. }
  298. WIKI.config.flags = {
  299. ...WIKI.config.flags,
  300. ...args.flags
  301. }
  302. await WIKI.configSvc.applyFlags()
  303. await WIKI.configSvc.saveToDb(['flags'])
  304. return {
  305. operation: generateSuccess('System Flags applied successfully')
  306. }
  307. } catch (err) {
  308. WIKI.logger.warn(err)
  309. return generateError(err)
  310. }
  311. },
  312. async updateSystemSearch (obj, args, context) {
  313. try {
  314. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  315. throw new Error('ERR_FORBIDDEN')
  316. }
  317. WIKI.config.search = {
  318. ...WIKI.config.search,
  319. termHighlighting: args.termHighlighting ?? WIKI.config.search.termHighlighting,
  320. dictOverrides: args.dictOverrides ? JSON.parse(args.dictOverrides) : WIKI.config.search.dictOverrides
  321. }
  322. // TODO: broadcast config update
  323. await WIKI.configSvc.saveToDb(['search'])
  324. return {
  325. operation: generateSuccess('System Search configuration applied successfully')
  326. }
  327. } catch (err) {
  328. WIKI.logger.warn(err)
  329. return generateError(err)
  330. }
  331. },
  332. async updateSystemSecurity (obj, args, context) {
  333. try {
  334. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  335. throw new Error('ERR_FORBIDDEN')
  336. }
  337. WIKI.config.security = _.defaultsDeep(_.omit(args, ['__typename']), WIKI.config.security)
  338. // TODO: broadcast config update
  339. await WIKI.configSvc.saveToDb(['security'])
  340. return {
  341. operation: generateSuccess('System Security configuration applied successfully')
  342. }
  343. } catch (err) {
  344. WIKI.logger.warn(err)
  345. return generateError(err)
  346. }
  347. }
  348. },
  349. SystemInfo: {
  350. configFile () {
  351. return path.join(process.cwd(), 'config.yml')
  352. },
  353. cpuCores () {
  354. return os.cpus().length
  355. },
  356. currentVersion () {
  357. return WIKI.version
  358. },
  359. dbHost () {
  360. return WIKI.config.db.host
  361. },
  362. dbVersion () {
  363. return WIKI.db.VERSION
  364. },
  365. hostname () {
  366. return os.hostname()
  367. },
  368. httpPort () {
  369. return WIKI.servers.servers.http ? _.get(WIKI.servers.servers.http.address(), 'port', 0) : 0
  370. },
  371. httpRedirection () {
  372. return _.get(WIKI.config, 'server.sslRedir', false)
  373. },
  374. httpsPort () {
  375. return WIKI.servers.servers.https ? _.get(WIKI.servers.servers.https.address(), 'port', 0) : 0
  376. },
  377. isMailConfigured () {
  378. return WIKI.config?.mail?.host?.length > 2
  379. },
  380. async isSchedulerHealthy () {
  381. const results = await WIKI.db.knex('jobHistory').count('* as total').whereIn('state', ['failed', 'interrupted']).andWhere('startedAt', '>=', DateTime.utc().minus({ days: 1 }).toISO()).first()
  382. return _.toSafeInteger(results?.total) === 0
  383. },
  384. latestVersion () {
  385. return WIKI.config.update.version
  386. },
  387. latestVersionReleaseDate () {
  388. return DateTime.fromISO(WIKI.config.update.versionDate).toJSDate()
  389. },
  390. nodeVersion () {
  391. return process.version.substring(1)
  392. },
  393. async operatingSystem () {
  394. let osLabel = `${os.type()} (${os.platform()}) ${os.release()} ${os.arch()}`
  395. if (os.platform() === 'linux') {
  396. const osInfo = await getos()
  397. osLabel = `${os.type()} - ${osInfo.dist} (${osInfo.codename || os.platform()}) ${osInfo.release || os.release()} ${os.arch()}`
  398. }
  399. return osLabel
  400. },
  401. async platform () {
  402. const isDockerized = await fs.pathExists('/.dockerenv')
  403. if (isDockerized) {
  404. return 'docker'
  405. }
  406. return os.platform()
  407. },
  408. ramTotal () {
  409. return filesize(os.totalmem())
  410. },
  411. sslDomain () {
  412. return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === 'letsencrypt' ? WIKI.config.ssl.domain : null
  413. },
  414. sslExpirationDate () {
  415. return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === 'letsencrypt' ? _.get(WIKI.config.letsencrypt, 'payload.expires', null) : null
  416. },
  417. sslProvider () {
  418. return WIKI.config.ssl.enabled ? WIKI.config.ssl.provider : null
  419. },
  420. sslStatus () {
  421. return 'OK'
  422. },
  423. sslSubscriberEmail () {
  424. return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === 'letsencrypt' ? WIKI.config.ssl.subscriberEmail : null
  425. },
  426. async upgradeCapable () {
  427. return !_.isNil(process.env.UPGRADE_COMPANION)
  428. },
  429. workingDirectory () {
  430. return process.cwd()
  431. },
  432. async groupsTotal () {
  433. const total = await WIKI.db.groups.query().count('* as total').first()
  434. return _.toSafeInteger(total.total)
  435. },
  436. async pagesTotal () {
  437. const total = await WIKI.db.pages.query().count('* as total').first()
  438. return _.toSafeInteger(total.total)
  439. },
  440. async tagsTotal () {
  441. const total = await WIKI.db.tags.query().count('* as total').first()
  442. return _.toSafeInteger(total.total)
  443. },
  444. async usersTotal () {
  445. const total = await WIKI.db.users.query().count('* as total').first()
  446. return _.toSafeInteger(total.total)
  447. },
  448. async loginsPastDay () {
  449. const total = await WIKI.db.users.query().count('* as total').whereRaw('"lastLoginAt" >= NOW() - INTERVAL \'1 DAY\'').first()
  450. return _.toSafeInteger(total.total)
  451. }
  452. }
  453. }