users.mjs 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903
  1. /* global WIKI */
  2. import { difference, find, first, flatten, flattenDeep, get, has, isArray, isEmpty, isNil, isString, last, set, toString, truncate, uniq } from 'lodash-es'
  3. import tfa from 'node-2fa'
  4. import jwt from 'jsonwebtoken'
  5. import { Model } from 'objection'
  6. import validate from 'validate.js'
  7. import qr from 'qr-image'
  8. import bcrypt from 'bcryptjs'
  9. import { Group } from './groups.mjs'
  10. /**
  11. * Users model
  12. */
  13. export class User extends Model {
  14. static get tableName() { return 'users' }
  15. static get jsonSchema () {
  16. return {
  17. type: 'object',
  18. required: ['email'],
  19. properties: {
  20. id: {type: 'string'},
  21. email: {type: 'string'},
  22. name: {type: 'string', minLength: 1, maxLength: 255},
  23. pictureUrl: {type: 'string'},
  24. isSystem: {type: 'boolean'},
  25. isActive: {type: 'boolean'},
  26. isVerified: {type: 'boolean'},
  27. createdAt: {type: 'string'},
  28. updatedAt: {type: 'string'}
  29. }
  30. }
  31. }
  32. static get jsonAttributes() {
  33. return ['auth', 'meta', 'prefs']
  34. }
  35. static get relationMappings() {
  36. return {
  37. groups: {
  38. relation: Model.ManyToManyRelation,
  39. modelClass: Group,
  40. join: {
  41. from: 'users.id',
  42. through: {
  43. from: 'userGroups.userId',
  44. to: 'userGroups.groupId'
  45. },
  46. to: 'groups.id'
  47. }
  48. }
  49. }
  50. }
  51. async $beforeUpdate(opt, context) {
  52. await super.$beforeUpdate(opt, context)
  53. this.updatedAt = new Date().toISOString()
  54. }
  55. async $beforeInsert(context) {
  56. await super.$beforeInsert(context)
  57. this.createdAt = new Date().toISOString()
  58. this.updatedAt = new Date().toISOString()
  59. }
  60. // ------------------------------------------------
  61. // Instance Methods
  62. // ------------------------------------------------
  63. async generateTFA(strategyId, siteId) {
  64. WIKI.logger.debug(`Generating new TFA secret for user ${this.id}...`)
  65. const site = WIKI.sites[siteId] ?? WIKI.sites[0] ?? { config: { title: 'Wiki' } }
  66. const tfaInfo = tfa.generateSecret({
  67. name: site.config.title,
  68. account: this.email
  69. })
  70. this.auth[strategyId].tfaSecret = tfaInfo.secret
  71. this.auth[strategyId].tfaIsActive = false
  72. await this.$query().patch({
  73. auth: this.auth
  74. })
  75. const safeTitle = site.config.title.replace(/[\s-.,=!@#$%?&*()+[\]{}/\\;<>]/g, '')
  76. return qr.imageSync(`otpauth://totp/${safeTitle}:${this.email}?secret=${tfaInfo.secret}`, { type: 'svg' })
  77. }
  78. async enableTFA(strategyId) {
  79. this.auth[strategyId].tfaIsActive = true
  80. return this.$query().patch({
  81. auth: this.auth
  82. })
  83. }
  84. async disableTFA(strategyId) {
  85. this.auth[strategyId].tfaIsActive = false
  86. return this.$query().patch({
  87. tfaIsActive: false,
  88. tfaSecret: ''
  89. })
  90. }
  91. verifyTFA(strategyId, code) {
  92. return tfa.verifyToken(this.auth[strategyId].tfaSecret, code)?.delta === 0
  93. }
  94. getPermissions () {
  95. return uniq(flatten(this.groups.map(g => g.permissions)))
  96. }
  97. getGroups() {
  98. return uniq(this.groups.map(g => g.id))
  99. }
  100. // ------------------------------------------------
  101. // Model Methods
  102. // ------------------------------------------------
  103. static async getById(id) {
  104. return WIKI.db.users.query().findById(id).withGraphFetched('groups').modifyGraph('groups', builder => {
  105. builder.select('groups.id', 'permissions')
  106. })
  107. }
  108. static async processProfile({ profile, providerKey }) {
  109. const provider = get(WIKI.auth.strategies, providerKey, {})
  110. provider.info = find(WIKI.data.authentication, ['key', provider.stategyKey])
  111. // Find existing user
  112. let user = await WIKI.db.users.query().findOne({
  113. providerId: toString(profile.id),
  114. providerKey
  115. })
  116. // Parse email
  117. let primaryEmail = ''
  118. if (isArray(profile.emails)) {
  119. const e = find(profile.emails, ['primary', true])
  120. primaryEmail = (e) ? e.value : first(profile.emails).value
  121. } else if (isArray(profile.email)) {
  122. primaryEmail = first(flattenDeep([profile.email]))
  123. } else if (isString(profile.email) && profile.email.length > 5) {
  124. primaryEmail = profile.email
  125. } else if (isString(profile.mail) && profile.mail.length > 5) {
  126. primaryEmail = profile.mail
  127. } else if (profile.user && profile.user.email && profile.user.email.length > 5) {
  128. primaryEmail = profile.user.email
  129. } else {
  130. throw new Error('Missing or invalid email address from profile.')
  131. }
  132. primaryEmail = primaryEmail.toLowerCase()
  133. // Find pending social user
  134. if (!user) {
  135. user = await WIKI.db.users.query().findOne({
  136. email: primaryEmail,
  137. providerId: null,
  138. providerKey
  139. })
  140. if (user) {
  141. user = await user.$query().patchAndFetch({
  142. providerId: toString(profile.id)
  143. })
  144. }
  145. }
  146. // Parse display name
  147. let displayName = ''
  148. if (isString(profile.displayName) && profile.displayName.length > 0) {
  149. displayName = profile.displayName
  150. } else if (isString(profile.name) && profile.name.length > 0) {
  151. displayName = profile.name
  152. } else {
  153. displayName = primaryEmail.split('@')[0]
  154. }
  155. // Parse picture URL / Data
  156. let pictureUrl = ''
  157. if (profile.picture && Buffer.isBuffer(profile.picture)) {
  158. pictureUrl = 'internal'
  159. } else {
  160. pictureUrl = truncate(get(profile, 'picture', get(user, 'pictureUrl', null)), {
  161. length: 255,
  162. omission: ''
  163. })
  164. }
  165. // Update existing user
  166. if (user) {
  167. if (!user.isActive) {
  168. throw new WIKI.Error.AuthAccountBanned()
  169. }
  170. if (user.isSystem) {
  171. throw new Error('This is a system reserved account and cannot be used.')
  172. }
  173. user = await user.$query().patchAndFetch({
  174. email: primaryEmail,
  175. name: displayName,
  176. pictureUrl: pictureUrl
  177. })
  178. if (pictureUrl === 'internal') {
  179. await WIKI.db.users.updateUserAvatarData(user.id, profile.picture)
  180. }
  181. return user
  182. }
  183. // Self-registration
  184. if (provider.selfRegistration) {
  185. // Check if email domain is whitelisted
  186. if (get(provider, 'domainWhitelist', []).length > 0) {
  187. const emailDomain = last(primaryEmail.split('@'))
  188. if (!provider.domainWhitelist.includes(emailDomain)) {
  189. throw new WIKI.Error.AuthRegistrationDomainUnauthorized()
  190. }
  191. }
  192. // Create account
  193. user = await WIKI.db.users.query().insertAndFetch({
  194. providerKey: providerKey,
  195. providerId: toString(profile.id),
  196. email: primaryEmail,
  197. name: displayName,
  198. pictureUrl: pictureUrl,
  199. locale: WIKI.config.lang.code,
  200. defaultEditor: 'markdown',
  201. tfaIsActive: false,
  202. isSystem: false,
  203. isActive: true,
  204. isVerified: true
  205. })
  206. // Assign to group(s)
  207. if (provider.autoEnrollGroups.length > 0) {
  208. await user.$relatedQuery('groups').relate(provider.autoEnrollGroups)
  209. }
  210. if (pictureUrl === 'internal') {
  211. await WIKI.db.users.updateUserAvatarData(user.id, profile.picture)
  212. }
  213. return user
  214. }
  215. throw new Error('You are not authorized to login.')
  216. }
  217. /**
  218. * Login a user
  219. */
  220. static async login ({ strategyId, siteId, username, password }, context) {
  221. if (has(WIKI.auth.strategies, strategyId)) {
  222. const selStrategy = WIKI.auth.strategies[strategyId]
  223. if (!selStrategy.isEnabled) {
  224. throw new WIKI.Error.AuthProviderInvalid()
  225. }
  226. const strInfo = find(WIKI.data.authentication, ['key', selStrategy.module])
  227. // Inject form user/pass
  228. if (strInfo.useForm) {
  229. set(context.req, 'body.email', username)
  230. set(context.req, 'body.password', password)
  231. set(context.req.params, 'strategy', strategyId)
  232. }
  233. // Authenticate
  234. return new Promise((resolve, reject) => {
  235. WIKI.auth.passport.authenticate(selStrategy.id, {
  236. session: !strInfo.useForm,
  237. scope: strInfo.scopes ? strInfo.scopes : null
  238. }, async (err, user, info) => {
  239. if (err) { return reject(err) }
  240. if (!user) { return reject(new WIKI.Error.AuthLoginFailed()) }
  241. try {
  242. const resp = await WIKI.db.users.afterLoginChecks(user, selStrategy.id, context, {
  243. siteId,
  244. skipTFA: !strInfo.useForm,
  245. skipChangePwd: !strInfo.useForm
  246. })
  247. resolve(resp)
  248. } catch (err) {
  249. reject(err)
  250. }
  251. })(context.req, context.res, () => {})
  252. })
  253. } else {
  254. throw new WIKI.Error.AuthProviderInvalid()
  255. }
  256. }
  257. /**
  258. * Perform post-login checks
  259. */
  260. static async afterLoginChecks (user, strategyId, context, { siteId, skipTFA, skipChangePwd } = { skipTFA: false, skipChangePwd: false, siteId: null }) {
  261. const str = WIKI.auth.strategies[strategyId]
  262. if (!str) {
  263. throw new Error('ERR_INVALID_STRATEGY')
  264. }
  265. // Get redirect target
  266. user.groups = await user.$relatedQuery('groups').select('groups.id', 'permissions', 'redirectOnLogin')
  267. let redirect = '/'
  268. if (user.groups && user.groups.length > 0) {
  269. for (const grp of user.groups) {
  270. if (!isEmpty(grp.redirectOnLogin) && grp.redirectOnLogin !== '/') {
  271. redirect = grp.redirectOnLogin
  272. break
  273. }
  274. }
  275. }
  276. // Get auth strategy flags
  277. const authStr = user.auth[strategyId] || {}
  278. // Is 2FA required?
  279. if (!skipTFA) {
  280. if (authStr.tfaIsActive && authStr.tfaSecret) {
  281. try {
  282. const tfaToken = await WIKI.db.userKeys.generateToken({
  283. kind: 'tfa',
  284. userId: user.id,
  285. meta: {
  286. strategyId
  287. }
  288. })
  289. return {
  290. nextAction: 'provideTfa',
  291. continuationToken: tfaToken,
  292. redirect
  293. }
  294. } catch (errc) {
  295. WIKI.logger.warn(errc)
  296. throw new WIKI.Error.AuthGenericError()
  297. }
  298. } else if (str.config?.enforceTfa || authStr.tfaRequired) {
  299. try {
  300. const tfaQRImage = await user.generateTFA(strategyId, siteId)
  301. const tfaToken = await WIKI.db.userKeys.generateToken({
  302. kind: 'tfaSetup',
  303. userId: user.id,
  304. meta: {
  305. strategyId
  306. }
  307. })
  308. return {
  309. nextAction: 'setupTfa',
  310. continuationToken: tfaToken,
  311. tfaQRImage,
  312. redirect
  313. }
  314. } catch (errc) {
  315. WIKI.logger.warn(errc)
  316. throw new WIKI.Error.AuthGenericError()
  317. }
  318. }
  319. }
  320. // Must Change Password?
  321. if (!skipChangePwd && authStr.mustChangePwd) {
  322. try {
  323. const pwdChangeToken = await WIKI.db.userKeys.generateToken({
  324. kind: 'changePwd',
  325. userId: user.id,
  326. meta: {
  327. strategyId
  328. }
  329. })
  330. return {
  331. nextAction: 'changePassword',
  332. continuationToken: pwdChangeToken,
  333. redirect
  334. }
  335. } catch (errc) {
  336. WIKI.logger.warn(errc)
  337. throw new WIKI.Error.AuthGenericError()
  338. }
  339. }
  340. return new Promise((resolve, reject) => {
  341. context.req.login(user, { session: false }, async errc => {
  342. if (errc) { return reject(errc) }
  343. const jwtToken = await WIKI.db.users.refreshToken(user, strategyId)
  344. resolve({
  345. nextAction: 'redirect',
  346. jwt: jwtToken.token,
  347. redirect
  348. })
  349. })
  350. })
  351. }
  352. /**
  353. * Generate a new token for a user
  354. */
  355. static async refreshToken (user) {
  356. if (isString(user)) {
  357. user = await WIKI.db.users.query().findById(user).withGraphFetched('groups').modifyGraph('groups', builder => {
  358. builder.select('groups.id', 'permissions')
  359. })
  360. if (!user) {
  361. WIKI.logger.warn(`Failed to refresh token for user ${user}: Not found.`)
  362. throw new WIKI.Error.AuthGenericError()
  363. }
  364. if (!user.isActive) {
  365. WIKI.logger.warn(`Failed to refresh token for user ${user}: Inactive.`)
  366. throw new WIKI.Error.AuthAccountBanned()
  367. }
  368. } else if (isNil(user.groups)) {
  369. user.groups = await user.$relatedQuery('groups').select('groups.id', 'permissions')
  370. }
  371. // Update Last Login Date
  372. // -> Bypass Objection.js to avoid updating the updatedAt field
  373. await WIKI.db.knex('users').where('id', user.id).update({ lastLoginAt: new Date().toISOString() })
  374. return {
  375. token: jwt.sign({
  376. id: user.id,
  377. email: user.email,
  378. groups: user.getGroups()
  379. }, {
  380. key: WIKI.config.auth.certs.private,
  381. passphrase: WIKI.config.auth.secret
  382. }, {
  383. algorithm: 'RS256',
  384. expiresIn: WIKI.config.auth.tokenExpiration,
  385. audience: WIKI.config.auth.audience,
  386. issuer: 'urn:wiki.js'
  387. }),
  388. user
  389. }
  390. }
  391. /**
  392. * Verify a TFA login
  393. */
  394. static async loginTFA ({ strategyId, siteId, securityCode, continuationToken, setup }, context) {
  395. if (securityCode.length === 6 && continuationToken.length > 1) {
  396. const { user, strategyId: expectedStrategyId } = await WIKI.db.userKeys.validateToken({
  397. kind: setup ? 'tfaSetup' : 'tfa',
  398. token: continuationToken,
  399. skipDelete: setup
  400. })
  401. if (strategyId !== expectedStrategyId) {
  402. throw new Error('ERR_INVALID_STRATEGY')
  403. }
  404. if (user) {
  405. if (user.verifyTFA(strategyId, securityCode)) {
  406. if (setup) {
  407. await user.enableTFA(strategyId)
  408. }
  409. return WIKI.db.users.afterLoginChecks(user, strategyId, context, { siteId, skipTFA: true })
  410. } else {
  411. throw new Error('ERR_TFA_INCORRECT_TOKEN')
  412. }
  413. }
  414. }
  415. throw new Error('ERR_TFA_INVALID_REQUEST')
  416. }
  417. /**
  418. * Change Password from a Mandatory Password Change after Login
  419. */
  420. static async loginChangePassword ({ strategyId, siteId, continuationToken, newPassword }, context) {
  421. if (!newPassword || newPassword.length < 8) {
  422. throw new Error('ERR_PASSWORD_TOO_SHORT')
  423. }
  424. const { user, strategyId: expectedStrategyId } = await WIKI.db.userKeys.validateToken({
  425. kind: 'changePwd',
  426. token: continuationToken
  427. })
  428. if (strategyId !== expectedStrategyId) {
  429. throw new Error('ERR_INVALID_STRATEGY')
  430. }
  431. if (user) {
  432. user.auth[strategyId].password = await bcrypt.hash(newPassword, 12)
  433. user.auth[strategyId].mustChangePwd = false
  434. await user.$query().patch({
  435. auth: user.auth
  436. })
  437. return WIKI.db.users.afterLoginChecks(user, strategyId, context, { siteId, skipChangePwd: true, skipTFA: true })
  438. } else {
  439. throw new Error('ERR_INVALID_USER')
  440. }
  441. }
  442. /**
  443. * Change Password from Profile
  444. */
  445. static async changePassword ({ strategyId, siteId, currentPassword, newPassword }, context) {
  446. const userId = context.req.user?.id
  447. if (!userId) {
  448. throw new Error('ERR_NOT_AUTHENTICATED')
  449. }
  450. const user = await WIKI.db.users.query().findById(userId)
  451. if (!user) {
  452. throw new Error('ERR_INVALID_USER')
  453. }
  454. if (!newPassword || newPassword.length < 8) {
  455. throw new Error('ERR_PASSWORD_TOO_SHORT')
  456. }
  457. if (!user.auth[strategyId]?.password) {
  458. throw new Error('ERR_INVALID_STRATEGY')
  459. }
  460. if (await bcrypt.compare(currentPassword, user.auth[strategyId].password) !== true) {
  461. throw new Error('ERR_INCORRECT_CURRENT_PASSWORD')
  462. }
  463. user.auth[strategyId].password = await bcrypt.hash(newPassword, 12)
  464. user.auth[strategyId].mustChangePwd = false
  465. await user.$query().patch({
  466. auth: user.auth
  467. })
  468. return true
  469. }
  470. /**
  471. * Send a password reset request
  472. */
  473. static async loginForgotPassword ({ email }, context) {
  474. const usr = await WIKI.db.users.query().where({
  475. email,
  476. providerKey: 'local'
  477. }).first()
  478. if (!usr) {
  479. WIKI.logger.debug(`Password reset attempt on nonexistant local account ${email}: [DISCARDED]`)
  480. return
  481. }
  482. const resetToken = await WIKI.db.userKeys.generateToken({
  483. userId: usr.id,
  484. kind: 'resetPwd'
  485. })
  486. await WIKI.mail.send({
  487. template: 'accountResetPwd',
  488. to: email,
  489. subject: `Password Reset Request`,
  490. data: {
  491. preheadertext: `A password reset was requested for ${WIKI.config.title}`,
  492. title: `A password reset was requested for ${WIKI.config.title}`,
  493. content: `Click the button below to reset your password. If you didn't request this password reset, simply discard this email.`,
  494. buttonLink: `${WIKI.config.host}/login-reset/${resetToken}`,
  495. buttonText: 'Reset Password'
  496. },
  497. text: `A password reset was requested for wiki ${WIKI.config.title}. Open the following link to proceed: ${WIKI.config.host}/login-reset/${resetToken}`
  498. })
  499. }
  500. /**
  501. * Create a new user
  502. *
  503. * @param {Object} param0 User Fields
  504. */
  505. static async createNewUser ({ email, password, name, groups, userInitiated = false, mustChangePassword = false, sendWelcomeEmail = false, sendWelcomeEmailFromSiteId }) {
  506. const localAuth = await WIKI.db.authentication.getStrategy('local')
  507. // Check if self-registration is enabled
  508. if (userInitiated && !localAuth.registration) {
  509. throw new Error('ERR_REGISTRATION_DISABLED')
  510. }
  511. // Input sanitization
  512. email = email.toLowerCase().trim()
  513. // Input validation
  514. const validation = validate({
  515. email,
  516. password,
  517. name
  518. }, {
  519. email: {
  520. email: true,
  521. length: {
  522. maximum: 255
  523. }
  524. },
  525. password: {
  526. presence: {
  527. allowEmpty: false
  528. },
  529. length: {
  530. minimum: 6
  531. }
  532. },
  533. name: {
  534. presence: {
  535. allowEmpty: false
  536. },
  537. length: {
  538. minimum: 2,
  539. maximum: 255
  540. }
  541. }
  542. }, { format: 'flat' })
  543. if (validation && validation.length > 0) {
  544. throw new Error(`ERR_INVALID_INPUT: ${validation[0]}`)
  545. }
  546. // Check if email address is allowed
  547. if (userInitiated && localAuth.allowedEmailRegex) {
  548. const emailCheckRgx = new RegExp(localAuth.allowedEmailRegex, 'i')
  549. if (!emailCheckRgx.test(email)) {
  550. throw new Error('ERR_EMAIL_ADDRESS_NOT_ALLOWED')
  551. }
  552. }
  553. // Check if email already exists
  554. const usr = await WIKI.db.users.query().findOne({ email })
  555. if (usr) {
  556. throw new Error('ERR_DUPLICATE_ACCOUNT_EMAIL')
  557. }
  558. // Check if site ID is provided when send welcome email is enabled
  559. if (sendWelcomeEmail && !sendWelcomeEmailFromSiteId) {
  560. throw new Error('ERR_INVALID_SITE')
  561. }
  562. WIKI.logger.debug(`Creating new user account for ${email}...`)
  563. // Create the account
  564. const newUsr = await WIKI.db.users.query().insert({
  565. email,
  566. name,
  567. auth: {
  568. [localAuth.id]: {
  569. password: await bcrypt.hash(password, 12),
  570. mustChangePwd: mustChangePassword,
  571. restrictLogin: false,
  572. tfaRequired: false,
  573. tfaSecret: ''
  574. }
  575. },
  576. hasAvatar: false,
  577. isSystem: false,
  578. isActive: true,
  579. isVerified: true,
  580. meta: {
  581. jobTitle: '',
  582. location: '',
  583. pronouns: ''
  584. },
  585. prefs: {
  586. cvd: 'none',
  587. timezone: WIKI.config.userDefaults.timezone || 'America/New_York',
  588. appearance: 'site',
  589. dateFormat: WIKI.config.userDefaults.dateFormat || 'YYYY-MM-DD',
  590. timeFormat: WIKI.config.userDefaults.timeFormat || '12h'
  591. }
  592. }).returning('*')
  593. // Assign to group(s)
  594. const groupsToEnroll = [WIKI.data.systemIds.usersGroupId]
  595. if (groups?.length > 0) {
  596. groupsToEnroll.push(...groups)
  597. }
  598. if (userInitiated && localAuth.autoEnrollGroups?.length > 0) {
  599. groupsToEnroll.push(...localAuth.autoEnrollGroups)
  600. }
  601. await newUsr.$relatedQuery('groups').relate(uniq(groupsToEnroll))
  602. // Verification Email
  603. if (userInitiated && localAuth.config?.emailValidation) {
  604. // Create verification token
  605. const verificationToken = await WIKI.db.userKeys.generateToken({
  606. kind: 'verify',
  607. userId: newUsr.id
  608. })
  609. // TODO: Handle multilingual text
  610. // Send verification email
  611. await WIKI.mail.send({
  612. template: 'UserVerify',
  613. to: email,
  614. subject: 'Verify your account',
  615. data: {
  616. preheadertext: 'Verify your account in order to gain access to the wiki.',
  617. title: 'Verify your account',
  618. content: 'Click the button below in order to verify your account and gain access to the wiki.',
  619. buttonLink: `${WIKI.config.mail.defaultBaseURL}/verify/${verificationToken}`,
  620. buttonText: 'Verify'
  621. },
  622. text: `You must open the following link in your browser to verify your account and gain access to the wiki: ${WIKI.config.mail.defaultBaseURL}/verify/${verificationToken}`
  623. })
  624. } else if (sendWelcomeEmail) {
  625. // TODO: Handle multilingual text
  626. const site = WIKI.sites[sendWelcomeEmailFromSiteId]
  627. if (site) {
  628. const siteUrl = site.hostname === '*' ? WIKI.config.mail.defaultBaseURL : `https://${site.hostname}`
  629. // Send welcome email
  630. await WIKI.mail.send({
  631. template: 'UserWelcome',
  632. to: email,
  633. subject: `Welcome to the wiki ${site.config.title}`,
  634. data: {
  635. siteTitle: site.config.title,
  636. preview: `You've been invited to the wiki ${site.config.title}`,
  637. title: `You've been invited to the wiki ${site.config.title}`,
  638. content: `Click the button below to access the wiki.`,
  639. email,
  640. password,
  641. buttonLink: `${siteUrl}/login`,
  642. buttonText: 'Login',
  643. logo: `${siteUrl}/_site/logo`
  644. },
  645. text: `You've been invited to the wiki ${site.config.title}: ${siteUrl}/login`
  646. })
  647. } else {
  648. WIKI.logger.warn('An invalid site ID was provided when creating user. No welcome email was sent.')
  649. throw new Error('ERR_INVALID_SITE')
  650. }
  651. }
  652. WIKI.logger.debug(`Created new user account for ${email} successfully.`)
  653. return newUsr
  654. }
  655. /**
  656. * Update an existing user
  657. *
  658. * @param {Object} param0 User ID and fields to update
  659. */
  660. static async updateUser (id, { email, name, groups, auth, isVerified, isActive, meta, prefs }) {
  661. const usr = await WIKI.db.users.query().findById(id)
  662. if (usr) {
  663. let usrData = {}
  664. if (!isEmpty(email) && email !== usr.email) {
  665. const dupUsr = await WIKI.db.users.query().select('id').where({ email }).first()
  666. if (dupUsr) {
  667. throw new Error('ERR_DUPLICATE_ACCOUNT_EMAIL')
  668. }
  669. usrData.email = email.toLowerCase()
  670. }
  671. if (!isEmpty(name) && name !== usr.name) {
  672. usrData.name = name.trim()
  673. }
  674. if (isArray(groups)) {
  675. const usrGroupsRaw = await usr.$relatedQuery('groups')
  676. const usrGroups = usrGroupsRaw.map(g => g.id)
  677. // Relate added groups
  678. const addUsrGroups = difference(groups, usrGroups)
  679. for (const grp of addUsrGroups) {
  680. await usr.$relatedQuery('groups').relate(grp)
  681. }
  682. // Unrelate removed groups
  683. const remUsrGroups = difference(usrGroups, groups)
  684. for (const grp of remUsrGroups) {
  685. await usr.$relatedQuery('groups').unrelate().where('groupId', grp)
  686. }
  687. }
  688. if (!isNil(auth?.tfaRequired)) {
  689. usr.auth[WIKI.data.systemIds.localAuthId].tfaRequired = auth.tfaRequired
  690. usrData.auth = usr.auth
  691. }
  692. if (!isNil(auth?.mustChangePwd)) {
  693. usr.auth[WIKI.data.systemIds.localAuthId].mustChangePwd = auth.mustChangePwd
  694. usrData.auth = usr.auth
  695. }
  696. if (!isNil(auth?.restrictLogin)) {
  697. usr.auth[WIKI.data.systemIds.localAuthId].restrictLogin = auth.restrictLogin
  698. usrData.auth = usr.auth
  699. }
  700. if (!isNil(isVerified)) {
  701. usrData.isVerified = isVerified
  702. }
  703. if (!isNil(isActive)) {
  704. usrData.isVerified = isActive
  705. }
  706. if (!isEmpty(meta)) {
  707. usrData.meta = meta
  708. }
  709. if (!isEmpty(prefs)) {
  710. usrData.prefs = prefs
  711. }
  712. await WIKI.db.users.query().patch(usrData).findById(id)
  713. } else {
  714. throw new WIKI.Error.UserNotFound()
  715. }
  716. }
  717. /**
  718. * Delete a User
  719. *
  720. * @param {*} id User ID
  721. */
  722. static async deleteUser (id, replaceId) {
  723. const usr = await WIKI.db.users.query().findById(id)
  724. if (usr) {
  725. await WIKI.db.assets.query().patch({ authorId: replaceId }).where('authorId', id)
  726. await WIKI.db.comments.query().patch({ authorId: replaceId }).where('authorId', id)
  727. await WIKI.db.pageHistory.query().patch({ authorId: replaceId }).where('authorId', id)
  728. await WIKI.db.pages.query().patch({ authorId: replaceId }).where('authorId', id)
  729. await WIKI.db.pages.query().patch({ creatorId: replaceId }).where('creatorId', id)
  730. await WIKI.db.userKeys.query().delete().where('userId', id)
  731. await WIKI.db.users.query().deleteById(id)
  732. } else {
  733. throw new WIKI.Error.UserNotFound()
  734. }
  735. }
  736. /**
  737. * Logout the current user
  738. */
  739. static async logout (context) {
  740. if (!context.req.user || context.req.user.id === WIKI.config.auth.guestUserId) {
  741. return '/'
  742. }
  743. if (context.req.user.strategyId && has(WIKI.auth.strategies, context.req.user.strategyId)) {
  744. const selStrategy = WIKI.auth.strategies[context.req.user.strategyId]
  745. if (!selStrategy.isEnabled) {
  746. throw new WIKI.Error.AuthProviderInvalid()
  747. }
  748. const provider = find(WIKI.data.authentication, ['key', selStrategy.module])
  749. if (provider.logout) {
  750. return provider.logout(provider.config)
  751. }
  752. }
  753. return '/'
  754. }
  755. static async getGuestUser () {
  756. const user = await WIKI.db.users.query().findById(WIKI.config.auth.guestUserId).withGraphJoined('groups').modifyGraph('groups', builder => {
  757. builder.select('groups.id', 'permissions')
  758. })
  759. if (!user) {
  760. WIKI.logger.error('CRITICAL ERROR: Guest user is missing!')
  761. process.exit(1)
  762. }
  763. user.permissions = user.getPermissions()
  764. return user
  765. }
  766. static async getRootUser () {
  767. let user = await WIKI.db.users.query().findById(WIKI.config.auth.rootAdminUserId)
  768. if (!user) {
  769. WIKI.logger.error('CRITICAL ERROR: Root Administrator user is missing!')
  770. process.exit(1)
  771. }
  772. user.permissions = ['manage:system']
  773. return user
  774. }
  775. /**
  776. * Add / Update User Avatar Data
  777. */
  778. static async updateUserAvatarData (userId, data) {
  779. try {
  780. WIKI.logger.debug(`Updating user ${userId} avatar data...`)
  781. if (data.length > 1024 * 1024) {
  782. throw new Error('Avatar image filesize is too large. 1MB max.')
  783. }
  784. const existing = await WIKI.db.knex('userAvatars').select('id').where('id', userId).first()
  785. if (existing) {
  786. await WIKI.db.knex('userAvatars').where({
  787. id: userId
  788. }).update({
  789. data
  790. })
  791. } else {
  792. await WIKI.db.knex('userAvatars').insert({
  793. id: userId,
  794. data
  795. })
  796. }
  797. } catch (err) {
  798. WIKI.logger.warn(`Failed to process binary thumbnail data for user ${userId}: ${err.message}`)
  799. }
  800. }
  801. static async getUserAvatarData (userId) {
  802. try {
  803. const usrData = await WIKI.db.knex('userAvatars').where('id', userId).first()
  804. if (usrData) {
  805. return usrData.data
  806. } else {
  807. return null
  808. }
  809. } catch (err) {
  810. WIKI.logger.warn(`Failed to process binary thumbnail data for user ${userId}`)
  811. }
  812. }
  813. }