users.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. /* global WIKI */
  2. const bcrypt = require('bcryptjs-then')
  3. const _ = require('lodash')
  4. const tfa = require('node-2fa')
  5. const securityHelper = require('../helpers/security')
  6. const jwt = require('jsonwebtoken')
  7. const Model = require('objection').Model
  8. const validate = require('validate.js')
  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. role: {type: 'string', enum: ['admin', 'guest', 'user']},
  26. tfaIsActive: {type: 'boolean', default: false},
  27. tfaSecret: {type: 'string'},
  28. jobTitle: {type: 'string'},
  29. location: {type: 'string'},
  30. pictureUrl: {type: 'string'},
  31. isSystem: {type: 'boolean'},
  32. isActive: {type: 'boolean'},
  33. isVerified: {type: 'boolean'},
  34. createdAt: {type: 'string'},
  35. updatedAt: {type: 'string'}
  36. }
  37. }
  38. }
  39. static get relationMappings() {
  40. return {
  41. groups: {
  42. relation: Model.ManyToManyRelation,
  43. modelClass: require('./groups'),
  44. join: {
  45. from: 'users.id',
  46. through: {
  47. from: 'userGroups.userId',
  48. to: 'userGroups.groupId'
  49. },
  50. to: 'groups.id'
  51. }
  52. },
  53. provider: {
  54. relation: Model.BelongsToOneRelation,
  55. modelClass: require('./authentication'),
  56. join: {
  57. from: 'users.providerKey',
  58. to: 'authentication.key'
  59. }
  60. },
  61. defaultEditor: {
  62. relation: Model.BelongsToOneRelation,
  63. modelClass: require('./editors'),
  64. join: {
  65. from: 'users.editorKey',
  66. to: 'editors.key'
  67. }
  68. },
  69. locale: {
  70. relation: Model.BelongsToOneRelation,
  71. modelClass: require('./locales'),
  72. join: {
  73. from: 'users.localeCode',
  74. to: 'locales.code'
  75. }
  76. }
  77. }
  78. }
  79. async $beforeUpdate(opt, context) {
  80. await super.$beforeUpdate(opt, context)
  81. this.updatedAt = new Date().toISOString()
  82. if (!(opt.patch && this.password === undefined)) {
  83. await this.generateHash()
  84. }
  85. }
  86. async $beforeInsert(context) {
  87. await super.$beforeInsert(context)
  88. this.createdAt = new Date().toISOString()
  89. this.updatedAt = new Date().toISOString()
  90. await this.generateHash()
  91. }
  92. // ------------------------------------------------
  93. // Instance Methods
  94. // ------------------------------------------------
  95. async generateHash() {
  96. if (this.password) {
  97. if (bcryptRegexp.test(this.password)) { return }
  98. this.password = await bcrypt.hash(this.password, 12)
  99. }
  100. }
  101. async verifyPassword(pwd) {
  102. if (await bcrypt.compare(pwd, this.password) === true) {
  103. return true
  104. } else {
  105. throw new WIKI.Error.AuthLoginFailed()
  106. }
  107. }
  108. async enableTFA() {
  109. let tfaInfo = tfa.generateSecret({
  110. name: WIKI.config.site.title
  111. })
  112. return this.$query.patch({
  113. tfaIsActive: true,
  114. tfaSecret: tfaInfo.secret
  115. })
  116. }
  117. async disableTFA() {
  118. return this.$query.patch({
  119. tfaIsActive: false,
  120. tfaSecret: ''
  121. })
  122. }
  123. async verifyTFA(code) {
  124. let result = tfa.verifyToken(this.tfaSecret, code)
  125. return (result && _.has(result, 'delta') && result.delta === 0)
  126. }
  127. getGlobalPermissions() {
  128. return _.uniq(_.flatten(_.map(this.groups, 'permissions')))
  129. }
  130. getGroups() {
  131. return _.uniq(_.map(this.groups, 'id'))
  132. }
  133. // ------------------------------------------------
  134. // Model Methods
  135. // ------------------------------------------------
  136. static async processProfile({ profile, providerKey }) {
  137. const provider = _.get(WIKI.auth.strategies, providerKey, {})
  138. provider.info = _.find(WIKI.data.authentication, ['key', providerKey])
  139. // Find existing user
  140. let user = await WIKI.models.users.query().findOne({
  141. providerId: _.toString(profile.id),
  142. providerKey
  143. })
  144. // Parse email
  145. let primaryEmail = ''
  146. if (_.isArray(profile.emails)) {
  147. const e = _.find(profile.emails, ['primary', true])
  148. primaryEmail = (e) ? e.value : _.first(profile.emails).value
  149. } else if (_.isString(profile.email) && profile.email.length > 5) {
  150. primaryEmail = profile.email
  151. } else if (_.isString(profile.mail) && profile.mail.length > 5) {
  152. primaryEmail = profile.mail
  153. } else if (profile.user && profile.user.email && profile.user.email.length > 5) {
  154. primaryEmail = profile.user.email
  155. } else {
  156. throw new Error('Missing or invalid email address from profile.')
  157. }
  158. primaryEmail = _.toLower(primaryEmail)
  159. // Find pending social user
  160. if (!user) {
  161. user = await WIKI.models.users.query().findOne({
  162. email: primaryEmail,
  163. providerId: null,
  164. providerKey
  165. })
  166. if (user) {
  167. user = await user.$query().patchAndFetch({
  168. providerId: _.toString(profile.id)
  169. })
  170. }
  171. }
  172. // Parse display name
  173. let displayName = ''
  174. if (_.isString(profile.displayName) && profile.displayName.length > 0) {
  175. displayName = profile.displayName
  176. } else if (_.isString(profile.name) && profile.name.length > 0) {
  177. displayName = profile.name
  178. } else {
  179. displayName = primaryEmail.split('@')[0]
  180. }
  181. // Parse picture URL
  182. let pictureUrl = _.get(profile, 'picture', _.get(user, 'pictureUrl', null))
  183. // Update existing user
  184. if (user) {
  185. if (!user.isActive) {
  186. throw new WIKI.Error.AuthAccountBanned()
  187. }
  188. if (user.isSystem) {
  189. throw new Error('This is a system reserved account and cannot be used.')
  190. }
  191. user = await user.$query().patchAndFetch({
  192. email: primaryEmail,
  193. name: displayName,
  194. pictureUrl: pictureUrl
  195. })
  196. return user
  197. }
  198. // Self-registration
  199. if (provider.selfRegistration) {
  200. // Check if email domain is whitelisted
  201. if (_.get(provider, 'domainWhitelist', []).length > 0) {
  202. const emailDomain = _.last(primaryEmail.split('@'))
  203. if (!_.includes(provider.domainWhitelist, emailDomain)) {
  204. throw new WIKI.Error.AuthRegistrationDomainUnauthorized()
  205. }
  206. }
  207. // Create account
  208. user = await WIKI.models.users.query().insertAndFetch({
  209. providerKey: providerKey,
  210. providerId: _.toString(profile.id),
  211. email: primaryEmail,
  212. name: displayName,
  213. pictureUrl: pictureUrl,
  214. localeCode: WIKI.config.lang.code,
  215. defaultEditor: 'markdown',
  216. tfaIsActive: false,
  217. isSystem: false,
  218. isActive: true,
  219. isVerified: true
  220. })
  221. // Assign to group(s)
  222. if (provider.autoEnrollGroups.length > 0) {
  223. await user.$relatedQuery('groups').relate(provider.autoEnrollGroups)
  224. }
  225. return user
  226. }
  227. throw new Error('You are not authorized to login.')
  228. }
  229. static async login (opts, context) {
  230. if (_.has(WIKI.auth.strategies, opts.strategy)) {
  231. const strInfo = _.find(WIKI.data.authentication, ['key', opts.strategy])
  232. // Inject form user/pass
  233. if (strInfo.useForm) {
  234. _.set(context.req, 'body.email', opts.username)
  235. _.set(context.req, 'body.password', opts.password)
  236. }
  237. // Authenticate
  238. return new Promise((resolve, reject) => {
  239. WIKI.auth.passport.authenticate(opts.strategy, {
  240. session: !strInfo.useForm,
  241. scope: strInfo.scopes ? strInfo.scopes : null
  242. }, async (err, user, info) => {
  243. if (err) { return reject(err) }
  244. if (!user) { return reject(new WIKI.Error.AuthLoginFailed()) }
  245. // Is 2FA required?
  246. if (user.tfaIsActive) {
  247. try {
  248. let loginToken = await securityHelper.generateToken(32)
  249. await WIKI.redis.set(`tfa:${loginToken}`, user.id, 'EX', 600)
  250. return resolve({
  251. tfaRequired: true,
  252. tfaLoginToken: loginToken
  253. })
  254. } catch (err) {
  255. WIKI.logger.warn(err)
  256. return reject(new WIKI.Error.AuthGenericError())
  257. }
  258. } else {
  259. // No 2FA, log in user
  260. return context.req.logIn(user, { session: !strInfo.useForm }, async err => {
  261. if (err) { return reject(err) }
  262. const jwtToken = await WIKI.models.users.refreshToken(user)
  263. resolve({
  264. jwt: jwtToken.token,
  265. tfaRequired: false
  266. })
  267. })
  268. }
  269. })(context.req, context.res, () => {})
  270. })
  271. } else {
  272. throw new WIKI.Error.AuthProviderInvalid()
  273. }
  274. }
  275. static async refreshToken(user) {
  276. if (_.isSafeInteger(user)) {
  277. user = await WIKI.models.users.query().findById(user).eager('groups').modifyEager('groups', builder => {
  278. builder.select('groups.id', 'permissions')
  279. })
  280. if (!user) {
  281. WIKI.logger.warn(`Failed to refresh token for user ${user}: Not found.`)
  282. throw new WIKI.Error.AuthGenericError()
  283. }
  284. } else if (_.isNil(user.groups)) {
  285. await user.$relatedQuery('groups').select('groups.id', 'permissions')
  286. }
  287. return {
  288. token: jwt.sign({
  289. id: user.id,
  290. email: user.email,
  291. name: user.name,
  292. pictureUrl: user.pictureUrl,
  293. timezone: user.timezone,
  294. localeCode: user.localeCode,
  295. defaultEditor: user.defaultEditor,
  296. permissions: user.getGlobalPermissions(),
  297. groups: user.getGroups()
  298. }, {
  299. key: WIKI.config.certs.private,
  300. passphrase: WIKI.config.sessionSecret
  301. }, {
  302. algorithm: 'RS256',
  303. expiresIn: WIKI.config.auth.tokenExpiration,
  304. audience: WIKI.config.auth.audience,
  305. issuer: 'urn:wiki.js'
  306. }),
  307. user
  308. }
  309. }
  310. static async loginTFA(opts, context) {
  311. if (opts.securityCode.length === 6 && opts.loginToken.length === 64) {
  312. let result = await WIKI.redis.get(`tfa:${opts.loginToken}`)
  313. if (result) {
  314. let userId = _.toSafeInteger(result)
  315. if (userId && userId > 0) {
  316. let user = await WIKI.models.users.query().findById(userId)
  317. if (user && user.verifyTFA(opts.securityCode)) {
  318. return Promise.fromCallback(clb => {
  319. context.req.logIn(user, clb)
  320. }).return({
  321. succeeded: true,
  322. message: 'Login Successful'
  323. }).catch(err => {
  324. WIKI.logger.warn(err)
  325. throw new WIKI.Error.AuthGenericError()
  326. })
  327. } else {
  328. throw new WIKI.Error.AuthTFAFailed()
  329. }
  330. }
  331. }
  332. }
  333. throw new WIKI.Error.AuthTFAInvalid()
  334. }
  335. /**
  336. * Create a new user
  337. *
  338. * @param {Object} param0 User Fields
  339. */
  340. static async createNewUser ({ providerKey, email, passwordRaw, name, groups, mustChangePassword, sendWelcomeEmail }) {
  341. // Input sanitization
  342. email = _.toLower(email)
  343. // Input validation
  344. let validation = null
  345. if (providerKey === 'local') {
  346. validation = validate({
  347. email,
  348. passwordRaw,
  349. name
  350. }, {
  351. email: {
  352. email: true,
  353. length: {
  354. maximum: 255
  355. }
  356. },
  357. passwordRaw: {
  358. presence: {
  359. allowEmpty: false
  360. },
  361. length: {
  362. minimum: 6
  363. }
  364. },
  365. name: {
  366. presence: {
  367. allowEmpty: false
  368. },
  369. length: {
  370. minimum: 2,
  371. maximum: 255
  372. }
  373. }
  374. }, { format: 'flat' })
  375. } else {
  376. validation = validate({
  377. email,
  378. name
  379. }, {
  380. email: {
  381. email: true,
  382. length: {
  383. maximum: 255
  384. }
  385. },
  386. name: {
  387. presence: {
  388. allowEmpty: false
  389. },
  390. length: {
  391. minimum: 2,
  392. maximum: 255
  393. }
  394. }
  395. }, { format: 'flat' })
  396. }
  397. if (validation && validation.length > 0) {
  398. throw new WIKI.Error.InputInvalid(validation[0])
  399. }
  400. // Check if email already exists
  401. const usr = await WIKI.models.users.query().findOne({ email, providerKey })
  402. if (!usr) {
  403. // Create the account
  404. let newUsrData = {
  405. providerKey,
  406. email,
  407. name,
  408. locale: 'en',
  409. defaultEditor: 'markdown',
  410. tfaIsActive: false,
  411. isSystem: false,
  412. isActive: true,
  413. isVerified: true,
  414. mustChangePwd: false
  415. }
  416. if (providerKey === `local`) {
  417. newUsrData.password = passwordRaw
  418. newUsrData.mustChangePwd = (mustChangePassword === true)
  419. }
  420. const newUsr = await WIKI.models.users.query().insert(newUsrData)
  421. // Assign to group(s)
  422. if (groups.length > 0) {
  423. await newUsr.$relatedQuery('groups').relate(groups)
  424. }
  425. if (sendWelcomeEmail) {
  426. // Send welcome email
  427. await WIKI.mail.send({
  428. template: 'accountWelcome',
  429. to: email,
  430. subject: `Welcome to the wiki ${WIKI.config.title}`,
  431. data: {
  432. preheadertext: `You've been invited to the wiki ${WIKI.config.title}`,
  433. title: `You've been invited to the wiki ${WIKI.config.title}`,
  434. content: `Click the button below to access the wiki.`,
  435. buttonLink: `${WIKI.config.host}/login`,
  436. buttonText: 'Login'
  437. },
  438. text: `You've been invited to the wiki ${WIKI.config.title}: ${WIKI.config.host}/login`
  439. })
  440. }
  441. } else {
  442. throw new WIKI.Error.AuthAccountAlreadyExists()
  443. }
  444. }
  445. /**
  446. * Update an existing user
  447. *
  448. * @param {Object} param0 User ID and fields to update
  449. */
  450. static async updateUser ({ id, email, name, newPassword, groups, location, jobTitle, timezone }) {
  451. const usr = await WIKI.models.users.query().findById(id)
  452. if (usr) {
  453. let usrData = {}
  454. if (!_.isEmpty(email) && email !== usr.email) {
  455. const dupUsr = await WIKI.models.users.query().select('id').where({
  456. email,
  457. providerKey: usr.providerKey
  458. })
  459. if (dupUsr) {
  460. throw new WIKI.Error.AuthAccountAlreadyExists()
  461. }
  462. usrData.email = email
  463. }
  464. if (!_.isEmpty(name) && name !== usr.name) {
  465. usrData.name = _.trim(name)
  466. }
  467. if (!_.isEmpty(newPassword)) {
  468. if (newPassword.length < 6) {
  469. throw new WIKI.Error.InputInvalid('Password must be at least 6 characters!')
  470. }
  471. usrData.password = newPassword
  472. }
  473. if (!_.isEmpty(groups)) {
  474. const usrGroupsRaw = await usr.$relatedQuery('groups')
  475. const usrGroups = _.map(usrGroupsRaw, 'id')
  476. // Relate added groups
  477. const addUsrGroups = _.difference(groups, usrGroups)
  478. for (const grp of addUsrGroups) {
  479. await usr.$relatedQuery('groups').relate(grp)
  480. }
  481. // Unrelate removed groups
  482. const remUsrGroups = _.difference(usrGroups, groups)
  483. for (const grp of remUsrGroups) {
  484. await usr.$relatedQuery('groups').unrelate().where('groupId', grp)
  485. }
  486. }
  487. if (!_.isEmpty(location) && location !== usr.location) {
  488. usrData.location = _.trim(location)
  489. }
  490. if (!_.isEmpty(jobTitle) && jobTitle !== usr.jobTitle) {
  491. usrData.jobTitle = _.trim(jobTitle)
  492. }
  493. if (!_.isEmpty(timezone) && timezone !== usr.timezone) {
  494. usrData.timezone = timezone
  495. }
  496. await WIKI.models.users.query().patch(usrData).findById(id)
  497. } else {
  498. throw new WIKI.Error.UserNotFound()
  499. }
  500. }
  501. /**
  502. * Register a new user (client-side registration)
  503. *
  504. * @param {Object} param0 User fields
  505. * @param {Object} context GraphQL Context
  506. */
  507. static async register ({ email, password, name, verify = false, bypassChecks = false }, context) {
  508. const localStrg = await WIKI.models.authentication.getStrategy('local')
  509. // Check if self-registration is enabled
  510. if (localStrg.selfRegistration || bypassChecks) {
  511. // Input sanitization
  512. email = _.toLower(email)
  513. // Input validation
  514. const validation = validate({
  515. email,
  516. password,
  517. name
  518. }, {
  519. email: {
  520. email: true,
  521. length: {
  522. maximum: 255
  523. }
  524. },
  525. password: {
  526. presence: {
  527. allowEmpty: false
  528. },
  529. length: {
  530. minimum: 6
  531. }
  532. },
  533. name: {
  534. presence: {
  535. allowEmpty: false
  536. },
  537. length: {
  538. minimum: 2,
  539. maximum: 255
  540. }
  541. }
  542. }, { format: 'flat' })
  543. if (validation && validation.length > 0) {
  544. throw new WIKI.Error.InputInvalid(validation[0])
  545. }
  546. // Check if email domain is whitelisted
  547. if (_.get(localStrg, 'domainWhitelist.v', []).length > 0 && !bypassChecks) {
  548. const emailDomain = _.last(email.split('@'))
  549. if (!_.includes(localStrg.domainWhitelist.v, emailDomain)) {
  550. throw new WIKI.Error.AuthRegistrationDomainUnauthorized()
  551. }
  552. }
  553. // Check if email already exists
  554. const usr = await WIKI.models.users.query().findOne({ email, providerKey: 'local' })
  555. if (!usr) {
  556. // Create the account
  557. const newUsr = await WIKI.models.users.query().insert({
  558. provider: 'local',
  559. email,
  560. name,
  561. password,
  562. locale: 'en',
  563. defaultEditor: 'markdown',
  564. tfaIsActive: false,
  565. isSystem: false,
  566. isActive: true,
  567. isVerified: false
  568. })
  569. // Assign to group(s)
  570. if (_.get(localStrg, 'autoEnrollGroups.v', []).length > 0) {
  571. await newUsr.$relatedQuery('groups').relate(localStrg.autoEnrollGroups.v)
  572. }
  573. if (verify) {
  574. // Create verification token
  575. const verificationToken = await WIKI.models.userKeys.generateToken({
  576. kind: 'verify',
  577. userId: newUsr.id
  578. })
  579. // Send verification email
  580. await WIKI.mail.send({
  581. template: 'accountVerify',
  582. to: email,
  583. subject: 'Verify your account',
  584. data: {
  585. preheadertext: 'Verify your account in order to gain access to the wiki.',
  586. title: 'Verify your account',
  587. content: 'Click the button below in order to verify your account and gain access to the wiki.',
  588. buttonLink: `${WIKI.config.host}/verify/${verificationToken}`,
  589. buttonText: 'Verify'
  590. },
  591. 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}`
  592. })
  593. }
  594. return true
  595. } else {
  596. throw new WIKI.Error.AuthAccountAlreadyExists()
  597. }
  598. } else {
  599. throw new WIKI.Error.AuthRegistrationDisabled()
  600. }
  601. }
  602. static async getGuestUser () {
  603. const user = await WIKI.models.users.query().findById(2).eager('groups').modifyEager('groups', builder => {
  604. builder.select('groups.id', 'permissions')
  605. })
  606. if (!user) {
  607. WIKI.logger.error('CRITICAL ERROR: Guest user is missing!')
  608. process.exit(1)
  609. }
  610. user.permissions = user.getGlobalPermissions()
  611. return user
  612. }
  613. }