2
0

users.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728
  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 bcryptRegexp = /^\$2[ayb]\$[0-9]{2}\$[A-Za-z0-9./]{53}$/
  9. /**
  10. * Users model
  11. */
  12. module.exports = class User extends Model {
  13. static get tableName() { return 'users' }
  14. static get jsonSchema () {
  15. return {
  16. type: 'object',
  17. required: ['email'],
  18. properties: {
  19. id: {type: 'integer'},
  20. email: {type: 'string', format: 'email'},
  21. name: {type: 'string', minLength: 1, maxLength: 255},
  22. providerId: {type: 'string'},
  23. password: {type: 'string'},
  24. role: {type: 'string', enum: ['admin', 'guest', 'user']},
  25. tfaIsActive: {type: 'boolean', default: false},
  26. tfaSecret: {type: 'string'},
  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 enableTFA() {
  108. let tfaInfo = tfa.generateSecret({
  109. name: WIKI.config.site.title
  110. })
  111. return this.$query.patch({
  112. tfaIsActive: true,
  113. tfaSecret: tfaInfo.secret
  114. })
  115. }
  116. async disableTFA() {
  117. return this.$query.patch({
  118. tfaIsActive: false,
  119. tfaSecret: ''
  120. })
  121. }
  122. async verifyTFA(code) {
  123. let result = tfa.verifyToken(this.tfaSecret, code)
  124. return (result && _.has(result, 'delta') && result.delta === 0)
  125. }
  126. getGlobalPermissions() {
  127. return _.uniq(_.flatten(_.map(this.groups, 'permissions')))
  128. }
  129. getGroups() {
  130. return _.uniq(_.map(this.groups, 'id'))
  131. }
  132. // ------------------------------------------------
  133. // Model Methods
  134. // ------------------------------------------------
  135. static async processProfile({ profile, providerKey }) {
  136. const provider = _.get(WIKI.auth.strategies, providerKey, {})
  137. provider.info = _.find(WIKI.data.authentication, ['key', providerKey])
  138. // Find existing user
  139. let user = await WIKI.models.users.query().findOne({
  140. providerId: _.toString(profile.id),
  141. providerKey
  142. })
  143. // Parse email
  144. let primaryEmail = ''
  145. if (_.isArray(profile.emails)) {
  146. const e = _.find(profile.emails, ['primary', true])
  147. primaryEmail = (e) ? e.value : _.first(profile.emails).value
  148. } else if (_.isString(profile.email) && profile.email.length > 5) {
  149. primaryEmail = profile.email
  150. } else if (_.isString(profile.mail) && profile.mail.length > 5) {
  151. primaryEmail = profile.mail
  152. } else if (profile.user && profile.user.email && profile.user.email.length > 5) {
  153. primaryEmail = profile.user.email
  154. } else {
  155. throw new Error('Missing or invalid email address from profile.')
  156. }
  157. primaryEmail = _.toLower(primaryEmail)
  158. // Find pending social user
  159. if (!user) {
  160. user = await WIKI.models.users.query().findOne({
  161. email: primaryEmail,
  162. providerId: null,
  163. providerKey
  164. })
  165. if (user) {
  166. user = await user.$query().patchAndFetch({
  167. providerId: _.toString(profile.id)
  168. })
  169. }
  170. }
  171. // Parse display name
  172. let displayName = ''
  173. if (_.isString(profile.displayName) && profile.displayName.length > 0) {
  174. displayName = profile.displayName
  175. } else if (_.isString(profile.name) && profile.name.length > 0) {
  176. displayName = profile.name
  177. } else {
  178. displayName = primaryEmail.split('@')[0]
  179. }
  180. // Parse picture URL
  181. let pictureUrl = _.truncate(_.get(profile, 'picture', _.get(user, 'pictureUrl', null)), {
  182. length: 255,
  183. omission: ''
  184. })
  185. // Update existing user
  186. if (user) {
  187. if (!user.isActive) {
  188. throw new WIKI.Error.AuthAccountBanned()
  189. }
  190. if (user.isSystem) {
  191. throw new Error('This is a system reserved account and cannot be used.')
  192. }
  193. user = await user.$query().patchAndFetch({
  194. email: primaryEmail,
  195. name: displayName,
  196. pictureUrl: pictureUrl
  197. })
  198. return user
  199. }
  200. // Self-registration
  201. if (provider.selfRegistration) {
  202. // Check if email domain is whitelisted
  203. if (_.get(provider, 'domainWhitelist', []).length > 0) {
  204. const emailDomain = _.last(primaryEmail.split('@'))
  205. if (!_.includes(provider.domainWhitelist, emailDomain)) {
  206. throw new WIKI.Error.AuthRegistrationDomainUnauthorized()
  207. }
  208. }
  209. // Create account
  210. user = await WIKI.models.users.query().insertAndFetch({
  211. providerKey: providerKey,
  212. providerId: _.toString(profile.id),
  213. email: primaryEmail,
  214. name: displayName,
  215. pictureUrl: pictureUrl,
  216. localeCode: WIKI.config.lang.code,
  217. defaultEditor: 'markdown',
  218. tfaIsActive: false,
  219. isSystem: false,
  220. isActive: true,
  221. isVerified: true
  222. })
  223. // Assign to group(s)
  224. if (provider.autoEnrollGroups.length > 0) {
  225. await user.$relatedQuery('groups').relate(provider.autoEnrollGroups)
  226. }
  227. return user
  228. }
  229. throw new Error('You are not authorized to login.')
  230. }
  231. static async login (opts, context) {
  232. if (_.has(WIKI.auth.strategies, opts.strategy)) {
  233. const strInfo = _.find(WIKI.data.authentication, ['key', opts.strategy])
  234. // Inject form user/pass
  235. if (strInfo.useForm) {
  236. _.set(context.req, 'body.email', opts.username)
  237. _.set(context.req, 'body.password', opts.password)
  238. }
  239. // Authenticate
  240. return new Promise((resolve, reject) => {
  241. WIKI.auth.passport.authenticate(opts.strategy, {
  242. session: !strInfo.useForm,
  243. scope: strInfo.scopes ? strInfo.scopes : null
  244. }, async (err, user, info) => {
  245. if (err) { return reject(err) }
  246. if (!user) { return reject(new WIKI.Error.AuthLoginFailed()) }
  247. // Must Change Password?
  248. if (user.mustChangePwd) {
  249. try {
  250. const pwdChangeToken = await WIKI.models.userKeys.generateToken({
  251. kind: 'changePwd',
  252. userId: user.id
  253. })
  254. return resolve({
  255. mustChangePwd: true,
  256. continuationToken: pwdChangeToken
  257. })
  258. } catch (errc) {
  259. WIKI.logger.warn(errc)
  260. return reject(new WIKI.Error.AuthGenericError())
  261. }
  262. }
  263. // Is 2FA required?
  264. if (user.tfaIsActive) {
  265. try {
  266. const tfaToken = await WIKI.models.userKeys.generateToken({
  267. kind: 'tfa',
  268. userId: user.id
  269. })
  270. return resolve({
  271. tfaRequired: true,
  272. continuationToken: tfaToken
  273. })
  274. } catch (errc) {
  275. WIKI.logger.warn(errc)
  276. return reject(new WIKI.Error.AuthGenericError())
  277. }
  278. }
  279. context.req.logIn(user, { session: !strInfo.useForm }, async errc => {
  280. if (errc) { return reject(errc) }
  281. const jwtToken = await WIKI.models.users.refreshToken(user)
  282. resolve({ jwt: jwtToken.token })
  283. })
  284. })(context.req, context.res, () => {})
  285. })
  286. } else {
  287. throw new WIKI.Error.AuthProviderInvalid()
  288. }
  289. }
  290. static async refreshToken(user) {
  291. if (_.isSafeInteger(user)) {
  292. user = await WIKI.models.users.query().findById(user).withGraphFetched('groups').modifyGraph('groups', builder => {
  293. builder.select('groups.id', 'permissions')
  294. })
  295. if (!user) {
  296. WIKI.logger.warn(`Failed to refresh token for user ${user}: Not found.`)
  297. throw new WIKI.Error.AuthGenericError()
  298. }
  299. } else if (_.isNil(user.groups)) {
  300. user.groups = await user.$relatedQuery('groups').select('groups.id', 'permissions')
  301. }
  302. return {
  303. token: jwt.sign({
  304. id: user.id,
  305. email: user.email,
  306. name: user.name,
  307. pictureUrl: user.pictureUrl,
  308. timezone: user.timezone,
  309. localeCode: user.localeCode,
  310. defaultEditor: user.defaultEditor,
  311. permissions: user.getGlobalPermissions(),
  312. groups: user.getGroups()
  313. }, {
  314. key: WIKI.config.certs.private,
  315. passphrase: WIKI.config.sessionSecret
  316. }, {
  317. algorithm: 'RS256',
  318. expiresIn: WIKI.config.auth.tokenExpiration,
  319. audience: WIKI.config.auth.audience,
  320. issuer: 'urn:wiki.js'
  321. }),
  322. user
  323. }
  324. }
  325. static async loginTFA (opts, context) {
  326. if (opts.securityCode.length === 6 && opts.loginToken.length === 64) {
  327. let result = await WIKI.redis.get(`tfa:${opts.loginToken}`)
  328. if (result) {
  329. let userId = _.toSafeInteger(result)
  330. if (userId && userId > 0) {
  331. let user = await WIKI.models.users.query().findById(userId)
  332. if (user && user.verifyTFA(opts.securityCode)) {
  333. return Promise.fromCallback(clb => {
  334. context.req.logIn(user, clb)
  335. }).return({
  336. succeeded: true,
  337. message: 'Login Successful'
  338. }).catch(err => {
  339. WIKI.logger.warn(err)
  340. throw new WIKI.Error.AuthGenericError()
  341. })
  342. } else {
  343. throw new WIKI.Error.AuthTFAFailed()
  344. }
  345. }
  346. }
  347. }
  348. throw new WIKI.Error.AuthTFAInvalid()
  349. }
  350. /**
  351. * Change Password from a Mandatory Password Change after Login
  352. */
  353. static async loginChangePassword ({ continuationToken, newPassword }, context) {
  354. if (!newPassword || newPassword.length < 6) {
  355. throw new WIKI.Error.InputInvalid('Password must be at least 6 characters!')
  356. }
  357. const usr = await WIKI.models.userKeys.validateToken({
  358. kind: 'changePwd',
  359. token: continuationToken
  360. })
  361. if (usr) {
  362. await WIKI.models.users.query().patch({
  363. password: newPassword,
  364. mustChangePwd: false
  365. }).findById(usr.id)
  366. return new Promise((resolve, reject) => {
  367. context.req.logIn(usr, { session: false }, async err => {
  368. if (err) { return reject(err) }
  369. const jwtToken = await WIKI.models.users.refreshToken(usr)
  370. resolve({ jwt: jwtToken.token })
  371. })
  372. })
  373. } else {
  374. throw new WIKI.Error.UserNotFound()
  375. }
  376. }
  377. /**
  378. * Create a new user
  379. *
  380. * @param {Object} param0 User Fields
  381. */
  382. static async createNewUser ({ providerKey, email, passwordRaw, name, groups, mustChangePassword, sendWelcomeEmail }) {
  383. // Input sanitization
  384. email = _.toLower(email)
  385. // Input validation
  386. let validation = null
  387. if (providerKey === 'local') {
  388. validation = validate({
  389. email,
  390. passwordRaw,
  391. name
  392. }, {
  393. email: {
  394. email: true,
  395. length: {
  396. maximum: 255
  397. }
  398. },
  399. passwordRaw: {
  400. presence: {
  401. allowEmpty: false
  402. },
  403. length: {
  404. minimum: 6
  405. }
  406. },
  407. name: {
  408. presence: {
  409. allowEmpty: false
  410. },
  411. length: {
  412. minimum: 2,
  413. maximum: 255
  414. }
  415. }
  416. }, { format: 'flat' })
  417. } else {
  418. validation = validate({
  419. email,
  420. name
  421. }, {
  422. email: {
  423. email: true,
  424. length: {
  425. maximum: 255
  426. }
  427. },
  428. name: {
  429. presence: {
  430. allowEmpty: false
  431. },
  432. length: {
  433. minimum: 2,
  434. maximum: 255
  435. }
  436. }
  437. }, { format: 'flat' })
  438. }
  439. if (validation && validation.length > 0) {
  440. throw new WIKI.Error.InputInvalid(validation[0])
  441. }
  442. // Check if email already exists
  443. const usr = await WIKI.models.users.query().findOne({ email, providerKey })
  444. if (!usr) {
  445. // Create the account
  446. let newUsrData = {
  447. providerKey,
  448. email,
  449. name,
  450. locale: 'en',
  451. defaultEditor: 'markdown',
  452. tfaIsActive: false,
  453. isSystem: false,
  454. isActive: true,
  455. isVerified: true,
  456. mustChangePwd: false
  457. }
  458. if (providerKey === `local`) {
  459. newUsrData.password = passwordRaw
  460. newUsrData.mustChangePwd = (mustChangePassword === true)
  461. }
  462. const newUsr = await WIKI.models.users.query().insert(newUsrData)
  463. // Assign to group(s)
  464. if (groups.length > 0) {
  465. await newUsr.$relatedQuery('groups').relate(groups)
  466. }
  467. if (sendWelcomeEmail) {
  468. // Send welcome email
  469. await WIKI.mail.send({
  470. template: 'accountWelcome',
  471. to: email,
  472. subject: `Welcome to the wiki ${WIKI.config.title}`,
  473. data: {
  474. preheadertext: `You've been invited to the wiki ${WIKI.config.title}`,
  475. title: `You've been invited to the wiki ${WIKI.config.title}`,
  476. content: `Click the button below to access the wiki.`,
  477. buttonLink: `${WIKI.config.host}/login`,
  478. buttonText: 'Login'
  479. },
  480. text: `You've been invited to the wiki ${WIKI.config.title}: ${WIKI.config.host}/login`
  481. })
  482. }
  483. } else {
  484. throw new WIKI.Error.AuthAccountAlreadyExists()
  485. }
  486. }
  487. /**
  488. * Update an existing user
  489. *
  490. * @param {Object} param0 User ID and fields to update
  491. */
  492. static async updateUser ({ id, email, name, newPassword, groups, location, jobTitle, timezone }) {
  493. const usr = await WIKI.models.users.query().findById(id)
  494. if (usr) {
  495. let usrData = {}
  496. if (!_.isEmpty(email) && email !== usr.email) {
  497. const dupUsr = await WIKI.models.users.query().select('id').where({
  498. email,
  499. providerKey: usr.providerKey
  500. }).first()
  501. if (dupUsr) {
  502. throw new WIKI.Error.AuthAccountAlreadyExists()
  503. }
  504. usrData.email = email
  505. }
  506. if (!_.isEmpty(name) && name !== usr.name) {
  507. usrData.name = _.trim(name)
  508. }
  509. if (!_.isEmpty(newPassword)) {
  510. if (newPassword.length < 6) {
  511. throw new WIKI.Error.InputInvalid('Password must be at least 6 characters!')
  512. }
  513. usrData.password = newPassword
  514. }
  515. if (_.isArray(groups)) {
  516. const usrGroupsRaw = await usr.$relatedQuery('groups')
  517. const usrGroups = _.map(usrGroupsRaw, 'id')
  518. // Relate added groups
  519. const addUsrGroups = _.difference(groups, usrGroups)
  520. for (const grp of addUsrGroups) {
  521. await usr.$relatedQuery('groups').relate(grp)
  522. }
  523. // Unrelate removed groups
  524. const remUsrGroups = _.difference(usrGroups, groups)
  525. for (const grp of remUsrGroups) {
  526. await usr.$relatedQuery('groups').unrelate().where('groupId', grp)
  527. }
  528. }
  529. if (!_.isEmpty(location) && location !== usr.location) {
  530. usrData.location = _.trim(location)
  531. }
  532. if (!_.isEmpty(jobTitle) && jobTitle !== usr.jobTitle) {
  533. usrData.jobTitle = _.trim(jobTitle)
  534. }
  535. if (!_.isEmpty(timezone) && timezone !== usr.timezone) {
  536. usrData.timezone = timezone
  537. }
  538. await WIKI.models.users.query().patch(usrData).findById(id)
  539. } else {
  540. throw new WIKI.Error.UserNotFound()
  541. }
  542. }
  543. /**
  544. * Register a new user (client-side registration)
  545. *
  546. * @param {Object} param0 User fields
  547. * @param {Object} context GraphQL Context
  548. */
  549. static async register ({ email, password, name, verify = false, bypassChecks = false }, context) {
  550. const localStrg = await WIKI.models.authentication.getStrategy('local')
  551. // Check if self-registration is enabled
  552. if (localStrg.selfRegistration || bypassChecks) {
  553. // Input sanitization
  554. email = _.toLower(email)
  555. // Input validation
  556. const validation = validate({
  557. email,
  558. password,
  559. name
  560. }, {
  561. email: {
  562. email: true,
  563. length: {
  564. maximum: 255
  565. }
  566. },
  567. password: {
  568. presence: {
  569. allowEmpty: false
  570. },
  571. length: {
  572. minimum: 6
  573. }
  574. },
  575. name: {
  576. presence: {
  577. allowEmpty: false
  578. },
  579. length: {
  580. minimum: 2,
  581. maximum: 255
  582. }
  583. }
  584. }, { format: 'flat' })
  585. if (validation && validation.length > 0) {
  586. throw new WIKI.Error.InputInvalid(validation[0])
  587. }
  588. // Check if email domain is whitelisted
  589. if (_.get(localStrg, 'domainWhitelist.v', []).length > 0 && !bypassChecks) {
  590. const emailDomain = _.last(email.split('@'))
  591. if (!_.includes(localStrg.domainWhitelist.v, emailDomain)) {
  592. throw new WIKI.Error.AuthRegistrationDomainUnauthorized()
  593. }
  594. }
  595. // Check if email already exists
  596. const usr = await WIKI.models.users.query().findOne({ email, providerKey: 'local' })
  597. if (!usr) {
  598. // Create the account
  599. const newUsr = await WIKI.models.users.query().insert({
  600. provider: 'local',
  601. email,
  602. name,
  603. password,
  604. locale: 'en',
  605. defaultEditor: 'markdown',
  606. tfaIsActive: false,
  607. isSystem: false,
  608. isActive: true,
  609. isVerified: false
  610. })
  611. // Assign to group(s)
  612. if (_.get(localStrg, 'autoEnrollGroups.v', []).length > 0) {
  613. await newUsr.$relatedQuery('groups').relate(localStrg.autoEnrollGroups.v)
  614. }
  615. if (verify) {
  616. // Create verification token
  617. const verificationToken = await WIKI.models.userKeys.generateToken({
  618. kind: 'verify',
  619. userId: newUsr.id
  620. })
  621. // Send verification email
  622. await WIKI.mail.send({
  623. template: 'accountVerify',
  624. to: email,
  625. subject: 'Verify your account',
  626. data: {
  627. preheadertext: 'Verify your account in order to gain access to the wiki.',
  628. title: 'Verify your account',
  629. content: 'Click the button below in order to verify your account and gain access to the wiki.',
  630. buttonLink: `${WIKI.config.host}/verify/${verificationToken}`,
  631. buttonText: 'Verify'
  632. },
  633. 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}`
  634. })
  635. }
  636. return true
  637. } else {
  638. throw new WIKI.Error.AuthAccountAlreadyExists()
  639. }
  640. } else {
  641. throw new WIKI.Error.AuthRegistrationDisabled()
  642. }
  643. }
  644. static async getGuestUser () {
  645. const user = await WIKI.models.users.query().findById(2).withGraphJoined('groups').modifyGraph('groups', builder => {
  646. builder.select('groups.id', 'permissions')
  647. })
  648. if (!user) {
  649. WIKI.logger.error('CRITICAL ERROR: Guest user is missing!')
  650. process.exit(1)
  651. }
  652. user.permissions = user.getGlobalPermissions()
  653. return user
  654. }
  655. static async getRootUser () {
  656. let user = await WIKI.models.users.query().findById(1)
  657. if (!user) {
  658. WIKI.logger.error('CRITICAL ERROR: Root Administrator user is missing!')
  659. process.exit(1)
  660. }
  661. user.permissions = ['manage:system']
  662. return user
  663. }
  664. }