user.mjs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. import { generateError, generateSuccess } from '../../helpers/graph.mjs'
  2. import _, { isNil } from 'lodash-es'
  3. import path from 'node:path'
  4. import fs from 'fs-extra'
  5. import { DateTime } from 'luxon'
  6. export default {
  7. Query: {
  8. /**
  9. * FETCH ALL USERS
  10. */
  11. async users (obj, args, context, info) {
  12. // -> Sanitize limit
  13. let limit = args.pageSize ?? 20
  14. if (limit < 1 || limit > 1000) {
  15. limit = 1000
  16. }
  17. // -> Sanitize offset
  18. let offset = args.page ?? 1
  19. if (offset < 1) {
  20. offset = 1
  21. }
  22. // -> Fetch Users
  23. return WIKI.db.users.query()
  24. .select('id', 'email', 'name', 'isSystem', 'isActive', 'createdAt', 'lastLoginAt')
  25. .where(builder => {
  26. if (args.filter) {
  27. builder.where('email', 'like', `%${args.filter}%`)
  28. .orWhere('name', 'like', `%${args.filter}%`)
  29. }
  30. })
  31. .orderBy(args.orderBy ?? 'name', args.orderByDirection ?? 'asc')
  32. .offset((offset - 1) * limit)
  33. .limit(limit)
  34. },
  35. /**
  36. * FETCH A SINGLE USER
  37. */
  38. async userById (obj, args, context, info) {
  39. const usr = await WIKI.db.users.query().findById(args.id)
  40. if (!usr) {
  41. throw new Error('ERR_INVALID_USER')
  42. }
  43. // const str = _.get(WIKI.auth.strategies, usr.providerKey)
  44. // str.strategy = _.find(WIKI.data.authentication, ['key', str.strategyKey])
  45. // usr.providerName = str.displayName
  46. // usr.providerIs2FACapable = _.get(str, 'strategy.useForm', false)
  47. usr.auth = _.mapValues(usr.auth, (auth, providerKey) => {
  48. if (auth.password) {
  49. auth.password = 'redacted'
  50. }
  51. if (auth.tfaSecret) {
  52. auth.tfaSecret = 'redacted'
  53. }
  54. return auth
  55. })
  56. usr.passkeys = usr.passkeys.authenticators?.map(a => ({
  57. id: a.id,
  58. createdAt: DateTime.fromISO(a.createdAt).toJSDate(),
  59. name: a.name,
  60. siteHostname: a.rpID
  61. })) ?? []
  62. return usr
  63. },
  64. // async profile (obj, args, context, info) {
  65. // if (!context.req.user || context.req.user.id < 1 || context.req.user.id === 2) {
  66. // throw new WIKI.Error.AuthRequired()
  67. // }
  68. // const usr = await WIKI.db.users.query().findById(context.req.user.id)
  69. // if (!usr.isActive) {
  70. // throw new WIKI.Error.AuthAccountBanned()
  71. // }
  72. // const providerInfo = _.get(WIKI.auth.strategies, usr.providerKey, {})
  73. // usr.providerName = providerInfo.displayName || 'Unknown'
  74. // usr.lastLoginAt = usr.lastLoginAt || usr.updatedAt
  75. // usr.password = ''
  76. // usr.providerId = ''
  77. // usr.tfaSecret = ''
  78. // return usr
  79. // },
  80. async userDefaults (obj, args, context) {
  81. return WIKI.config.userDefaults
  82. },
  83. async lastLogins (obj, args, context, info) {
  84. return WIKI.db.users.query()
  85. .select('id', 'name', 'lastLoginAt')
  86. .whereNotNull('lastLoginAt')
  87. .orderBy('lastLoginAt', 'desc')
  88. .limit(10)
  89. },
  90. async userPermissions (obj, args, context) {
  91. if (!context.req.user || context.req.user.id === WIKI.auth.guest.id) {
  92. throw new WIKI.Error.AuthRequired()
  93. }
  94. const currentUser = await WIKI.db.users.getById(context.req.user.id)
  95. return currentUser.getPermissions()
  96. },
  97. async userPermissionsAtPath (obj, args, context) {
  98. return []
  99. }
  100. },
  101. Mutation: {
  102. async createUser (obj, args) {
  103. try {
  104. await WIKI.db.users.createNewUser({ ...args, isVerified: true })
  105. return {
  106. operation: generateSuccess('User created successfully')
  107. }
  108. } catch (err) {
  109. return generateError(err)
  110. }
  111. },
  112. async deleteUser (obj, args) {
  113. try {
  114. if (args.id <= 2) {
  115. throw new WIKI.Error.UserDeleteProtected()
  116. }
  117. await WIKI.db.users.deleteUser(args.id, args.replaceId)
  118. WIKI.auth.revokeUserTokens({ id: args.id, kind: 'u' })
  119. WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'u' })
  120. return {
  121. operation: generateSuccess('User deleted successfully')
  122. }
  123. } catch (err) {
  124. if (err.message.indexOf('foreign') >= 0) {
  125. return generateError(new WIKI.Error.UserDeleteForeignConstraint())
  126. } else {
  127. return generateError(err)
  128. }
  129. }
  130. },
  131. async updateUser (obj, args) {
  132. try {
  133. await WIKI.db.users.updateUser(args.id, args.patch)
  134. return {
  135. operation: generateSuccess('User updated successfully')
  136. }
  137. } catch (err) {
  138. return generateError(err)
  139. }
  140. },
  141. async verifyUser (obj, args) {
  142. try {
  143. await WIKI.db.users.query().patch({ isVerified: true }).findById(args.id)
  144. return {
  145. operation: generateSuccess('User verified successfully')
  146. }
  147. } catch (err) {
  148. return generateError(err)
  149. }
  150. },
  151. async activateUser (obj, args) {
  152. try {
  153. await WIKI.db.users.query().patch({ isActive: true }).findById(args.id)
  154. return {
  155. operation: generateSuccess('User activated successfully')
  156. }
  157. } catch (err) {
  158. return generateError(err)
  159. }
  160. },
  161. async deactivateUser (obj, args) {
  162. try {
  163. if (args.id <= 2) {
  164. throw new Error('Cannot deactivate system accounts.')
  165. }
  166. await WIKI.db.users.query().patch({ isActive: false }).findById(args.id)
  167. WIKI.auth.revokeUserTokens({ id: args.id, kind: 'u' })
  168. WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'u' })
  169. return {
  170. operation: generateSuccess('User deactivated successfully')
  171. }
  172. } catch (err) {
  173. return generateError(err)
  174. }
  175. },
  176. async enableUserTFA (obj, args) {
  177. try {
  178. await WIKI.db.users.query().patch({ tfaIsActive: true, tfaSecret: null }).findById(args.id)
  179. return {
  180. operation: generateSuccess('User 2FA enabled successfully')
  181. }
  182. } catch (err) {
  183. return generateError(err)
  184. }
  185. },
  186. async disableUserTFA (obj, args) {
  187. try {
  188. await WIKI.db.users.query().patch({ tfaIsActive: false, tfaSecret: null }).findById(args.id)
  189. return {
  190. operation: generateSuccess('User 2FA disabled successfully')
  191. }
  192. } catch (err) {
  193. return generateError(err)
  194. }
  195. },
  196. async changeUserPassword (obj, args, context) {
  197. try {
  198. if (args.newPassword?.length < 8) {
  199. throw new Error('ERR_PASSWORD_TOO_SHORT')
  200. }
  201. const usr = await WIKI.db.users.query().findById(args.id)
  202. if (!usr) {
  203. throw new Error('ERR_USER_NOT_FOUND')
  204. }
  205. const localAuth = await WIKI.db.authentication.getStrategy('local')
  206. usr.auth[localAuth.id].password = await bcrypt.hash(args.newPassword, 12)
  207. if (!isNil(args.mustChangePassword)) {
  208. usr.auth[localAuth.id].mustChangePwd = args.mustChangePassword
  209. }
  210. await WIKI.db.users.query().patch({
  211. auth: usr.auth
  212. }).findById(args.id)
  213. return {
  214. operation: generateSuccess('User password updated successfully')
  215. }
  216. } catch (err) {
  217. return generateError(err)
  218. }
  219. },
  220. async updateProfile (obj, args, context) {
  221. try {
  222. if (!context.req.user || context.req.user.id === WIKI.auth.guest.id) {
  223. throw new WIKI.Error.AuthRequired()
  224. }
  225. const usr = await WIKI.db.users.query().findById(context.req.user.id)
  226. if (!usr.isActive) {
  227. throw new WIKI.Error.AuthAccountBanned()
  228. }
  229. if (!usr.isVerified) {
  230. throw new WIKI.Error.AuthAccountNotVerified()
  231. }
  232. if (args.dateFormat && !['', 'DD/MM/YYYY', 'DD.MM.YYYY', 'MM/DD/YYYY', 'YYYY-MM-DD', 'YYYY/MM/DD'].includes(args.dateFormat)) {
  233. throw new WIKI.Error.InputInvalid()
  234. }
  235. if (args.appearance && !['site', 'light', 'dark'].includes(args.appearance)) {
  236. throw new WIKI.Error.InputInvalid()
  237. }
  238. await WIKI.db.users.query().findById(usr.id).patch({
  239. name: args.name?.trim() ?? usr.name,
  240. meta: {
  241. ...usr.meta,
  242. location: args.location?.trim() ?? usr.meta.location,
  243. jobTitle: args.jobTitle?.trim() ?? usr.meta.jobTitle,
  244. pronouns: args.pronouns?.trim() ?? usr.meta.pronouns
  245. },
  246. prefs: {
  247. ...usr.prefs,
  248. timezone: args.timezone || usr.prefs.timezone,
  249. dateFormat: args.dateFormat ?? usr.prefs.dateFormat,
  250. timeFormat: args.timeFormat ?? usr.prefs.timeFormat,
  251. appearance: args.appearance || usr.prefs.appearance,
  252. cvd: args.cvd || usr.prefs.cvd
  253. }
  254. })
  255. return {
  256. operation: generateSuccess('User profile updated successfully')
  257. }
  258. } catch (err) {
  259. return generateError(err)
  260. }
  261. },
  262. // async changePassword (obj, args, context) {
  263. // try {
  264. // if (!context.req.user || context.req.user.id < 1 || context.req.user.id === 2) {
  265. // throw new WIKI.Error.AuthRequired()
  266. // }
  267. // const usr = await WIKI.db.users.query().findById(context.req.user.id)
  268. // if (!usr.isActive) {
  269. // throw new WIKI.Error.AuthAccountBanned()
  270. // }
  271. // if (!usr.isVerified) {
  272. // throw new WIKI.Error.AuthAccountNotVerified()
  273. // }
  274. // if (usr.providerKey !== 'local') {
  275. // throw new WIKI.Error.AuthProviderInvalid()
  276. // }
  277. // try {
  278. // await usr.verifyPassword(args.current)
  279. // } catch (err) {
  280. // throw new WIKI.Error.AuthPasswordInvalid()
  281. // }
  282. // await WIKI.db.users.updateUser({
  283. // id: usr.id,
  284. // newPassword: args.new
  285. // })
  286. // const newToken = await WIKI.db.users.refreshToken(usr)
  287. // return {
  288. // responseResult: generateSuccess('Password changed successfully'),
  289. // jwt: newToken.token
  290. // }
  291. // } catch (err) {
  292. // return generateError(err)
  293. // }
  294. // },
  295. /**
  296. * UPLOAD USER AVATAR
  297. */
  298. async uploadUserAvatar (obj, args) {
  299. try {
  300. const { filename, mimetype, createReadStream } = await args.image
  301. const lowercaseFilename = filename.toLowerCase()
  302. WIKI.logger.debug(`Processing user ${args.id} avatar ${lowercaseFilename} of type ${mimetype}...`)
  303. if (!WIKI.extensions.ext.sharp.isInstalled) {
  304. throw new Error('This feature requires the Sharp extension but it is not installed. Contact your wiki administrator.')
  305. }
  306. if (!['.png', '.jpg', '.webp', '.gif'].some(s => lowercaseFilename.endsWith(s))) {
  307. throw new Error('Invalid File Extension. Must be png, jpg, webp or gif.')
  308. }
  309. const destFolder = path.resolve(
  310. process.cwd(),
  311. WIKI.config.dataPath,
  312. `assets`
  313. )
  314. const destPath = path.join(destFolder, `userav-${args.id}.jpg`)
  315. await fs.ensureDir(destFolder)
  316. // -> Resize
  317. await WIKI.extensions.ext.sharp.resize({
  318. format: 'jpg',
  319. inputStream: createReadStream(),
  320. outputPath: destPath,
  321. width: 180,
  322. height: 180
  323. })
  324. // -> Set avatar flag for this user in the DB
  325. await WIKI.db.users.query().findById(args.id).patch({ hasAvatar: true })
  326. // -> Save image data to DB
  327. const imgBuffer = await fs.readFile(destPath)
  328. await WIKI.db.knex('userAvatars').insert({
  329. id: args.id,
  330. data: imgBuffer
  331. }).onConflict('id').merge()
  332. WIKI.logger.debug(`Processed user ${args.id} avatar successfully.`)
  333. return {
  334. operation: generateSuccess('User avatar uploaded successfully')
  335. }
  336. } catch (err) {
  337. WIKI.logger.warn(err)
  338. return generateError(err)
  339. }
  340. },
  341. /**
  342. * CLEAR USER AVATAR
  343. */
  344. async clearUserAvatar (obj, args) {
  345. try {
  346. WIKI.logger.debug(`Clearing user ${args.id} avatar...`)
  347. await WIKI.db.users.query().findById(args.id).patch({ hasAvatar: false })
  348. await WIKI.db.knex('userAvatars').where({ id: args.id }).del()
  349. WIKI.logger.debug(`Cleared user ${args.id} avatar successfully.`)
  350. return {
  351. operation: generateSuccess('User avatar cleared successfully')
  352. }
  353. } catch (err) {
  354. WIKI.logger.warn(err)
  355. return generateError(err)
  356. }
  357. },
  358. /**
  359. * UPDATE USER DEFAULTS
  360. */
  361. async updateUserDefaults (obj, args, context) {
  362. WIKI.config.userDefaults = {
  363. timezone: args.timezone,
  364. dateFormat: args.dateFormat,
  365. timeFormat: args.timeFormat
  366. }
  367. await WIKI.configSvc.saveToDb(['userDefaults'])
  368. return {
  369. operation: generateSuccess('User defaults saved successfully')
  370. }
  371. }
  372. },
  373. User: {
  374. async auth (usr) {
  375. const authStrategies = await WIKI.db.authentication.getStrategies({ enabledOnly: true })
  376. return _.transform(usr.auth, (result, value, key) => {
  377. const authStrategy = _.find(authStrategies, ['id', key])
  378. const authModule = _.find(WIKI.data.authentication, ['key', authStrategy.module])
  379. if (!authStrategy || !authModule) { return }
  380. result.push({
  381. authId: key,
  382. authName: authStrategy.displayName,
  383. strategyKey: authStrategy.module,
  384. strategyIcon: authModule.icon,
  385. config: authStrategy.module === 'local' ? {
  386. isPasswordSet: value.password?.length > 0,
  387. isTfaSetup: value.tfaIsActive && value.tfaSecret?.length > 0,
  388. isTfaRequired: (value.tfaRequired || authStrategy.config.enforceTfa) ?? false,
  389. mustChangePwd: value.mustChangePwd ?? false,
  390. restrictLogin: value.restrictLogin ?? false
  391. } : value
  392. })
  393. }, [])
  394. },
  395. groups (usr) {
  396. return usr.$relatedQuery('groups')
  397. }
  398. }
  399. // UserProfile: {
  400. // async groups (usr) {
  401. // const usrGroups = await usr.$relatedQuery('groups')
  402. // return usrGroups.map(g => g.name)
  403. // },
  404. // async pagesTotal (usr) {
  405. // const result = await WIKI.db.pages.query().count('* as total').where('creatorId', usr.id).first()
  406. // return _.toSafeInteger(result.total)
  407. // }
  408. // }
  409. }