users.mjs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885
  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 }) {
  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. WIKI.logger.debug(`Creating new user account for ${email}...`)
  559. // Create the account
  560. const newUsr = await WIKI.db.users.query().insert({
  561. email,
  562. name,
  563. auth: {
  564. [localAuth.id]: {
  565. password: await bcrypt.hash(password, 12),
  566. mustChangePwd: mustChangePassword,
  567. restrictLogin: false,
  568. tfaRequired: false,
  569. tfaSecret: ''
  570. }
  571. },
  572. hasAvatar: false,
  573. isSystem: false,
  574. isActive: true,
  575. isVerified: true,
  576. meta: {
  577. jobTitle: '',
  578. location: '',
  579. pronouns: ''
  580. },
  581. prefs: {
  582. cvd: 'none',
  583. timezone: WIKI.config.userDefaults.timezone || 'America/New_York',
  584. appearance: 'site',
  585. dateFormat: WIKI.config.userDefaults.dateFormat || 'YYYY-MM-DD',
  586. timeFormat: WIKI.config.userDefaults.timeFormat || '12h'
  587. }
  588. }).returning('*')
  589. // Assign to group(s)
  590. const groupsToEnroll = [WIKI.data.systemIds.usersGroupId]
  591. if (groups?.length > 0) {
  592. groupsToEnroll.push(...groups)
  593. }
  594. if (userInitiated && localAuth.autoEnrollGroups?.length > 0) {
  595. groupsToEnroll.push(...localAuth.autoEnrollGroups)
  596. }
  597. await newUsr.$relatedQuery('groups').relate(uniq(groupsToEnroll))
  598. // Verification Email
  599. if (userInitiated && localAuth.config?.emailValidation) {
  600. // Create verification token
  601. const verificationToken = await WIKI.db.userKeys.generateToken({
  602. kind: 'verify',
  603. userId: newUsr.id
  604. })
  605. // Send verification email
  606. await WIKI.mail.send({
  607. template: 'accountVerify',
  608. to: email,
  609. subject: 'Verify your account',
  610. data: {
  611. preheadertext: 'Verify your account in order to gain access to the wiki.',
  612. title: 'Verify your account',
  613. content: 'Click the button below in order to verify your account and gain access to the wiki.',
  614. buttonLink: `${WIKI.config.host}/verify/${verificationToken}`,
  615. buttonText: 'Verify'
  616. },
  617. text: `You must open the following link in your browser to verify your account and gain access to the wiki: ${WIKI.config.host}/verify/${verificationToken}`
  618. })
  619. } else if (sendWelcomeEmail) {
  620. // Send welcome email
  621. await WIKI.mail.send({
  622. template: 'accountWelcome',
  623. to: email,
  624. subject: `Welcome to the wiki ${WIKI.config.title}`,
  625. data: {
  626. preheadertext: `You've been invited to the wiki ${WIKI.config.title}`,
  627. title: `You've been invited to the wiki ${WIKI.config.title}`,
  628. content: `Click the button below to access the wiki.`,
  629. buttonLink: `${WIKI.config.host}/login`,
  630. buttonText: 'Login'
  631. },
  632. text: `You've been invited to the wiki ${WIKI.config.title}: ${WIKI.config.host}/login`
  633. })
  634. }
  635. WIKI.logger.debug(`Created new user account for ${email} successfully.`)
  636. return newUsr
  637. }
  638. /**
  639. * Update an existing user
  640. *
  641. * @param {Object} param0 User ID and fields to update
  642. */
  643. static async updateUser (id, { email, name, groups, auth, isVerified, isActive, meta, prefs }) {
  644. const usr = await WIKI.db.users.query().findById(id)
  645. if (usr) {
  646. let usrData = {}
  647. if (!isEmpty(email) && email !== usr.email) {
  648. const dupUsr = await WIKI.db.users.query().select('id').where({ email }).first()
  649. if (dupUsr) {
  650. throw new Error('ERR_DUPLICATE_ACCOUNT_EMAIL')
  651. }
  652. usrData.email = email.toLowerCase()
  653. }
  654. if (!isEmpty(name) && name !== usr.name) {
  655. usrData.name = name.trim()
  656. }
  657. if (isArray(groups)) {
  658. const usrGroupsRaw = await usr.$relatedQuery('groups')
  659. const usrGroups = usrGroupsRaw.map(g => g.id)
  660. // Relate added groups
  661. const addUsrGroups = difference(groups, usrGroups)
  662. for (const grp of addUsrGroups) {
  663. await usr.$relatedQuery('groups').relate(grp)
  664. }
  665. // Unrelate removed groups
  666. const remUsrGroups = difference(usrGroups, groups)
  667. for (const grp of remUsrGroups) {
  668. await usr.$relatedQuery('groups').unrelate().where('groupId', grp)
  669. }
  670. }
  671. if (!isNil(auth?.tfaRequired)) {
  672. usr.auth[WIKI.data.systemIds.localAuthId].tfaRequired = auth.tfaRequired
  673. usrData.auth = usr.auth
  674. }
  675. if (!isNil(auth?.mustChangePwd)) {
  676. usr.auth[WIKI.data.systemIds.localAuthId].mustChangePwd = auth.mustChangePwd
  677. usrData.auth = usr.auth
  678. }
  679. if (!isNil(auth?.restrictLogin)) {
  680. usr.auth[WIKI.data.systemIds.localAuthId].restrictLogin = auth.restrictLogin
  681. usrData.auth = usr.auth
  682. }
  683. if (!isNil(isVerified)) {
  684. usrData.isVerified = isVerified
  685. }
  686. if (!isNil(isActive)) {
  687. usrData.isVerified = isActive
  688. }
  689. if (!isEmpty(meta)) {
  690. usrData.meta = meta
  691. }
  692. if (!isEmpty(prefs)) {
  693. usrData.prefs = prefs
  694. }
  695. await WIKI.db.users.query().patch(usrData).findById(id)
  696. } else {
  697. throw new WIKI.Error.UserNotFound()
  698. }
  699. }
  700. /**
  701. * Delete a User
  702. *
  703. * @param {*} id User ID
  704. */
  705. static async deleteUser (id, replaceId) {
  706. const usr = await WIKI.db.users.query().findById(id)
  707. if (usr) {
  708. await WIKI.db.assets.query().patch({ authorId: replaceId }).where('authorId', id)
  709. await WIKI.db.comments.query().patch({ authorId: replaceId }).where('authorId', id)
  710. await WIKI.db.pageHistory.query().patch({ authorId: replaceId }).where('authorId', id)
  711. await WIKI.db.pages.query().patch({ authorId: replaceId }).where('authorId', id)
  712. await WIKI.db.pages.query().patch({ creatorId: replaceId }).where('creatorId', id)
  713. await WIKI.db.userKeys.query().delete().where('userId', id)
  714. await WIKI.db.users.query().deleteById(id)
  715. } else {
  716. throw new WIKI.Error.UserNotFound()
  717. }
  718. }
  719. /**
  720. * Logout the current user
  721. */
  722. static async logout (context) {
  723. if (!context.req.user || context.req.user.id === WIKI.config.auth.guestUserId) {
  724. return '/'
  725. }
  726. if (context.req.user.strategyId && has(WIKI.auth.strategies, context.req.user.strategyId)) {
  727. const selStrategy = WIKI.auth.strategies[context.req.user.strategyId]
  728. if (!selStrategy.isEnabled) {
  729. throw new WIKI.Error.AuthProviderInvalid()
  730. }
  731. const provider = find(WIKI.data.authentication, ['key', selStrategy.module])
  732. if (provider.logout) {
  733. return provider.logout(provider.config)
  734. }
  735. }
  736. return '/'
  737. }
  738. static async getGuestUser () {
  739. const user = await WIKI.db.users.query().findById(WIKI.config.auth.guestUserId).withGraphJoined('groups').modifyGraph('groups', builder => {
  740. builder.select('groups.id', 'permissions')
  741. })
  742. if (!user) {
  743. WIKI.logger.error('CRITICAL ERROR: Guest user is missing!')
  744. process.exit(1)
  745. }
  746. user.permissions = user.getPermissions()
  747. return user
  748. }
  749. static async getRootUser () {
  750. let user = await WIKI.db.users.query().findById(WIKI.config.auth.rootAdminUserId)
  751. if (!user) {
  752. WIKI.logger.error('CRITICAL ERROR: Root Administrator user is missing!')
  753. process.exit(1)
  754. }
  755. user.permissions = ['manage:system']
  756. return user
  757. }
  758. /**
  759. * Add / Update User Avatar Data
  760. */
  761. static async updateUserAvatarData (userId, data) {
  762. try {
  763. WIKI.logger.debug(`Updating user ${userId} avatar data...`)
  764. if (data.length > 1024 * 1024) {
  765. throw new Error('Avatar image filesize is too large. 1MB max.')
  766. }
  767. const existing = await WIKI.db.knex('userAvatars').select('id').where('id', userId).first()
  768. if (existing) {
  769. await WIKI.db.knex('userAvatars').where({
  770. id: userId
  771. }).update({
  772. data
  773. })
  774. } else {
  775. await WIKI.db.knex('userAvatars').insert({
  776. id: userId,
  777. data
  778. })
  779. }
  780. } catch (err) {
  781. WIKI.logger.warn(`Failed to process binary thumbnail data for user ${userId}: ${err.message}`)
  782. }
  783. }
  784. static async getUserAvatarData (userId) {
  785. try {
  786. const usrData = await WIKI.db.knex('userAvatars').where('id', userId).first()
  787. if (usrData) {
  788. return usrData.data
  789. } else {
  790. return null
  791. }
  792. } catch (err) {
  793. WIKI.logger.warn(`Failed to process binary thumbnail data for user ${userId}`)
  794. }
  795. }
  796. }