users.js 27 KB

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