system.mjs 14 KB

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