You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

933 lines
27 KiB

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