users.js 25 KB

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