users.js 27 KB

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