users.mjs 25 KB

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