User.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867
  1. const request = require('./request')
  2. const settings = require('settings-sharelatex')
  3. const { db, ObjectId } = require('../../../../app/src/infrastructure/mongodb')
  4. const UserModel = require('../../../../app/src/models/User').User
  5. const UserUpdater = require('../../../../app/src/Features/User/UserUpdater')
  6. const AuthenticationManager = require('../../../../app/src/Features/Authentication/AuthenticationManager')
  7. const { promisify } = require('util')
  8. const fs = require('fs')
  9. const Path = require('path')
  10. let count = 0
  11. class User {
  12. constructor(options) {
  13. if (options == null) {
  14. options = {}
  15. }
  16. this.emails = [
  17. {
  18. email: options.email || `acceptance-test-${count}@example.com`,
  19. createdAt: new Date()
  20. }
  21. ]
  22. this.email = this.emails[0].email
  23. this.password = `a-terrible-secret-${count}`
  24. count++
  25. this.jar = request.jar()
  26. this.request = request.defaults({
  27. jar: this.jar
  28. })
  29. }
  30. setExtraAttributes(user) {
  31. if ((user != null ? user._id : undefined) == null) {
  32. throw new Error('User does not exist')
  33. }
  34. this.id = user._id.toString()
  35. this._id = user._id.toString()
  36. this.first_name = user.first_name
  37. this.referal_id = user.referal_id
  38. }
  39. get(callback) {
  40. db.users.findOne({ _id: ObjectId(this._id) }, callback)
  41. }
  42. getAuditLogWithoutNoise(callback) {
  43. this.get((error, user) => {
  44. if (error) return callback(error)
  45. if (!user) return callback(new Error('User not found'))
  46. callback(
  47. null,
  48. (user.auditLog || []).filter(entry => {
  49. return entry.operation !== 'login'
  50. })
  51. )
  52. })
  53. }
  54. mongoUpdate(updateOp, callback) {
  55. db.users.updateOne({ _id: ObjectId(this._id) }, updateOp, callback)
  56. }
  57. register(callback) {
  58. this.registerWithQuery('', callback)
  59. }
  60. registerWithQuery(query, callback) {
  61. if (this._id != null) {
  62. return callback(new Error('User already registered'))
  63. }
  64. this.getCsrfToken(error => {
  65. if (error != null) {
  66. return callback(error)
  67. }
  68. this.request.post(
  69. {
  70. url: `/register${query}`,
  71. json: { email: this.email, password: this.password }
  72. },
  73. (error, response, body) => {
  74. if (error != null) {
  75. return callback(error)
  76. }
  77. db.users.findOne({ email: this.email }, (error, user) => {
  78. if (error != null) {
  79. return callback(error)
  80. }
  81. this.setExtraAttributes(user)
  82. callback(null, user)
  83. })
  84. }
  85. )
  86. })
  87. }
  88. login(callback) {
  89. this.loginWith(this.email, callback)
  90. }
  91. loginWith(email, callback) {
  92. this.ensureUserExists(error => {
  93. if (error != null) {
  94. return callback(error)
  95. }
  96. this.getCsrfToken(error => {
  97. if (error != null) {
  98. return callback(error)
  99. }
  100. this.request.post(
  101. {
  102. url: settings.enableLegacyLogin ? '/login/legacy' : '/login',
  103. json: { email, password: this.password }
  104. },
  105. (error, response, body) => {
  106. if (error != null) {
  107. return callback(error)
  108. }
  109. // get new csrf token, then return result of login
  110. this.getCsrfToken(err => {
  111. if (err) {
  112. return callback(err)
  113. }
  114. callback(null, response, body)
  115. })
  116. }
  117. )
  118. })
  119. })
  120. }
  121. ensureUserExists(callback) {
  122. const filter = { email: this.email }
  123. const options = { upsert: true, new: true, setDefaultsOnInsert: true }
  124. UserModel.findOneAndUpdate(filter, {}, options, (error, user) => {
  125. if (error != null) {
  126. return callback(error)
  127. }
  128. this.setExtraAttributes(user)
  129. AuthenticationManager.setUserPasswordInV2(user, this.password, error => {
  130. if (error != null) {
  131. return callback(error)
  132. }
  133. this.mongoUpdate({ $set: { emails: this.emails } }, error => {
  134. if (error != null) {
  135. return callback(error)
  136. }
  137. callback(null, this.password)
  138. })
  139. })
  140. })
  141. }
  142. setFeatures(features, callback) {
  143. const update = {}
  144. for (let key in features) {
  145. const value = features[key]
  146. update[`features.${key}`] = value
  147. }
  148. UserModel.updateOne({ _id: this.id }, update, callback)
  149. }
  150. setFeaturesOverride(featuresOverride, callback) {
  151. const update = { $push: { featuresOverrides: featuresOverride } }
  152. UserModel.updateOne({ _id: this.id }, update, callback)
  153. }
  154. setOverleafId(overleafId, callback) {
  155. UserModel.updateOne(
  156. { _id: this.id },
  157. { 'overleaf.id': overleafId },
  158. callback
  159. )
  160. }
  161. logout(callback) {
  162. this.getCsrfToken(error => {
  163. if (error != null) {
  164. return callback(error)
  165. }
  166. this.request.post(
  167. {
  168. url: '/logout',
  169. json: {
  170. email: this.email,
  171. password: this.password
  172. }
  173. },
  174. (error, response, body) => {
  175. if (error != null) {
  176. return callback(error)
  177. }
  178. db.users.findOne({ email: this.email }, (error, user) => {
  179. if (error != null) {
  180. return callback(error)
  181. }
  182. if (user == null) {
  183. return callback()
  184. }
  185. this.id = user._id.toString()
  186. this._id = user._id.toString()
  187. callback()
  188. })
  189. }
  190. )
  191. })
  192. }
  193. addEmail(email, callback) {
  194. this.emails.push({ email, createdAt: new Date() })
  195. UserUpdater.addEmailAddress(
  196. this.id,
  197. email,
  198. {},
  199. { initiatorId: this._id, ipAddress: '127:0:0:0' },
  200. callback
  201. )
  202. }
  203. confirmEmail(email, callback) {
  204. for (let idx = 0; idx < this.emails.length; idx++) {
  205. const emailData = this.emails[idx]
  206. if (emailData.email === email) {
  207. this.emails[idx].confirmedAt = new Date()
  208. }
  209. }
  210. UserUpdater.confirmEmail(this.id, email, callback)
  211. }
  212. ensureAdmin(callback) {
  213. this.mongoUpdate({ $set: { isAdmin: true } }, callback)
  214. }
  215. ensureStaffAccess(flag, callback) {
  216. const update = { $set: {} }
  217. update.$set[`staffAccess.${flag}`] = true
  218. this.mongoUpdate(update, callback)
  219. }
  220. upgradeFeatures(callback) {
  221. const features = {
  222. collaborators: -1, // Infinite
  223. versioning: true,
  224. dropbox: true,
  225. compileTimeout: 60,
  226. compileGroup: 'priority',
  227. templates: true,
  228. references: true,
  229. trackChanges: true,
  230. trackChangesVisible: true
  231. }
  232. this.mongoUpdate({ $set: { features } }, callback)
  233. }
  234. downgradeFeatures(callback) {
  235. const features = {
  236. collaborators: 1,
  237. versioning: false,
  238. dropbox: false,
  239. compileTimeout: 60,
  240. compileGroup: 'standard',
  241. templates: false,
  242. references: false,
  243. trackChanges: false,
  244. trackChangesVisible: false
  245. }
  246. this.mongoUpdate({ $set: { features } }, callback)
  247. }
  248. defaultFeatures(callback) {
  249. const features = settings.defaultFeatures
  250. this.mongoUpdate({ $set: { features } }, callback)
  251. }
  252. getFeatures(callback) {
  253. db.users.findOne(
  254. { _id: ObjectId(this.id) },
  255. { projection: { features: 1 } },
  256. (error, user) => callback(error, user && user.features)
  257. )
  258. }
  259. fullDeleteUser(email, callback) {
  260. db.users.findOne({ email }, (error, user) => {
  261. if (error != null) {
  262. return callback(error)
  263. }
  264. if (user == null) {
  265. return callback()
  266. }
  267. const userId = user._id
  268. db.projects.deleteMany({ owner_ref: ObjectId(userId) }, err => {
  269. if (err != null) {
  270. callback(err)
  271. }
  272. db.users.deleteOne({ _id: ObjectId(userId) }, callback)
  273. })
  274. })
  275. }
  276. deleteUser(callback) {
  277. this.getCsrfToken(error => {
  278. if (error) {
  279. return callback(error)
  280. }
  281. this.request.post(
  282. {
  283. url: '/user/delete',
  284. json: { password: this.password }
  285. },
  286. (err, res) => {
  287. if (err) {
  288. return callback(err)
  289. }
  290. if (res.statusCode < 200 || res.statusCode >= 300) {
  291. return callback(
  292. new Error('Error received from API: ' + res.statusCode)
  293. )
  294. }
  295. callback()
  296. }
  297. )
  298. })
  299. }
  300. getProject(projectId, callback) {
  301. db.projects.findOne({ _id: ObjectId(projectId.toString()) }, callback)
  302. }
  303. saveProject(project, callback) {
  304. db.projects.updateOne({ _id: project._id }, { $set: project }, callback)
  305. }
  306. createProject(name, options, callback) {
  307. if (typeof options === 'function') {
  308. callback = options
  309. options = {}
  310. }
  311. this.request.post(
  312. {
  313. url: '/project/new',
  314. json: Object.assign({ projectName: name }, options)
  315. },
  316. (error, response, body) => {
  317. if (error != null) {
  318. return callback(error)
  319. }
  320. if ((body != null ? body.project_id : undefined) == null) {
  321. error = new Error(
  322. JSON.stringify([
  323. 'SOMETHING WENT WRONG CREATING PROJECT',
  324. name,
  325. options,
  326. response.statusCode,
  327. response.headers.location,
  328. body
  329. ])
  330. )
  331. callback(error)
  332. } else {
  333. callback(null, body.project_id)
  334. }
  335. }
  336. )
  337. }
  338. deleteProject(projectId, callback) {
  339. this.request.delete(
  340. {
  341. url: `/project/${projectId}`
  342. },
  343. (error, response, body) => {
  344. if (error != null) {
  345. return callback(error)
  346. }
  347. callback(null)
  348. }
  349. )
  350. }
  351. undeleteProject(projectId, callback) {
  352. this.request.post(
  353. {
  354. url: `/admin/project/${projectId}/undelete`
  355. },
  356. (error, response) => {
  357. if (error) {
  358. return callback(error)
  359. }
  360. if (response.statusCode !== 204) {
  361. return callback(
  362. new Error(
  363. `Non-success response when undeleting project: ${response.statusCode}`
  364. )
  365. )
  366. }
  367. callback(null)
  368. }
  369. )
  370. }
  371. deleteProjects(callback) {
  372. db.projects.deleteMany({ owner_ref: ObjectId(this.id) }, callback)
  373. }
  374. openProject(projectId, callback) {
  375. this.request.get(
  376. {
  377. url: `/project/${projectId}`
  378. },
  379. (error, response, body) => {
  380. if (error != null) {
  381. return callback(error)
  382. }
  383. if (response.statusCode !== 200) {
  384. const err = new Error(
  385. `Non-success response when opening project: ${response.statusCode}`
  386. )
  387. return callback(err)
  388. }
  389. callback(null)
  390. }
  391. )
  392. }
  393. createDocInProject(projectId, parentFolderId, name, callback) {
  394. this.getCsrfToken(error => {
  395. if (error != null) {
  396. return callback(error)
  397. }
  398. this.request.post(
  399. {
  400. url: `/project/${projectId}/doc`,
  401. json: {
  402. name,
  403. parentFolderId
  404. }
  405. },
  406. (error, response, body) => {
  407. if (error != null) {
  408. return callback(error)
  409. }
  410. callback(null, body._id)
  411. }
  412. )
  413. })
  414. }
  415. uploadFileInProject(projectId, folderId, file, name, contentType, callback) {
  416. const imageFile = fs.createReadStream(
  417. Path.resolve(Path.join(__dirname, '..', '..', 'files', file))
  418. )
  419. this.request.post(
  420. {
  421. uri: `project/${projectId}/upload`,
  422. qs: {
  423. folder_id: String(folderId)
  424. },
  425. formData: {
  426. qqfile: {
  427. value: imageFile,
  428. options: {
  429. filename: name,
  430. contentType: contentType
  431. }
  432. }
  433. }
  434. },
  435. (error, res, body) => {
  436. if (error) {
  437. return callback(error)
  438. }
  439. if (res.statusCode < 200 || res.statusCode >= 300) {
  440. return callback(new Error(`failed to upload file ${res.statusCode}`))
  441. }
  442. callback(null, JSON.parse(body).entity_id)
  443. }
  444. )
  445. }
  446. uploadExampleFileInProject(projectId, folderId, name, callback) {
  447. this.uploadFileInProject(
  448. projectId,
  449. folderId,
  450. '1pixel.png',
  451. name,
  452. 'image/png',
  453. callback
  454. )
  455. }
  456. moveItemInProject(projectId, type, itemId, folderId, callback) {
  457. this.request.post(
  458. {
  459. uri: `project/${projectId}/${type}/${itemId}/move`,
  460. json: {
  461. folder_id: folderId
  462. }
  463. },
  464. (error, res) => {
  465. if (error) {
  466. return callback(error)
  467. }
  468. if (res.statusCode < 200 || res.statusCode >= 300) {
  469. return callback(new Error(`failed to move ${type} ${res.statusCode}`))
  470. }
  471. callback()
  472. }
  473. )
  474. }
  475. renameItemInProject(projectId, type, itemId, name, callback) {
  476. this.request.post(
  477. {
  478. uri: `project/${projectId}/${type}/${itemId}/rename`,
  479. json: {
  480. name: name
  481. }
  482. },
  483. (error, res) => {
  484. if (error) {
  485. return callback(error)
  486. }
  487. if (res.statusCode < 200 || res.statusCode >= 300) {
  488. return callback(
  489. new Error(`failed to rename ${type} ${res.statusCode}`)
  490. )
  491. }
  492. callback()
  493. }
  494. )
  495. }
  496. deleteItemInProject(projectId, type, itemId, callback) {
  497. this.request.delete(
  498. `project/${projectId}/${type}/${itemId}`,
  499. (error, res) => {
  500. if (error) {
  501. return callback(error)
  502. }
  503. if (res.statusCode < 200 || res.statusCode >= 300) {
  504. return callback(
  505. new Error(`failed to delete ${type} ${res.statusCode}`)
  506. )
  507. }
  508. callback()
  509. }
  510. )
  511. }
  512. joinProject(projectId, callback) {
  513. this.request.post(
  514. {
  515. url: `/project/${projectId}/join`,
  516. qs: { user_id: this._id },
  517. auth: {
  518. user: settings.apis.web.user,
  519. pass: settings.apis.web.pass,
  520. sendImmediately: true
  521. },
  522. json: true,
  523. jar: false
  524. },
  525. (error, res, body) => {
  526. if (error) {
  527. return callback(error)
  528. }
  529. if (res.statusCode < 200 || res.statusCode >= 300) {
  530. return callback(
  531. new Error(`failed to join project ${projectId} ${res.statusCode}`)
  532. )
  533. }
  534. callback(null, body)
  535. }
  536. )
  537. }
  538. addUserToProject(projectId, user, privileges, callback) {
  539. let updateOp
  540. if (privileges === 'readAndWrite') {
  541. updateOp = { $addToSet: { collaberator_refs: user._id } }
  542. } else if (privileges === 'readOnly') {
  543. updateOp = { $addToSet: { readOnly_refs: user._id } }
  544. }
  545. db.projects.updateOne({ _id: ObjectId(projectId) }, updateOp, callback)
  546. }
  547. makePublic(projectId, level, callback) {
  548. this.request.post(
  549. {
  550. url: `/project/${projectId}/settings/admin`,
  551. json: {
  552. publicAccessLevel: level
  553. }
  554. },
  555. (error, response, body) => {
  556. if (error != null) {
  557. return callback(error)
  558. }
  559. callback(null)
  560. }
  561. )
  562. }
  563. makePrivate(projectId, callback) {
  564. this.request.post(
  565. {
  566. url: `/project/${projectId}/settings/admin`,
  567. json: {
  568. publicAccessLevel: 'private'
  569. }
  570. },
  571. (error, response, body) => {
  572. if (error != null) {
  573. return callback(error)
  574. }
  575. callback(null)
  576. }
  577. )
  578. }
  579. makeTokenBased(projectId, callback) {
  580. this.request.post(
  581. {
  582. url: `/project/${projectId}/settings/admin`,
  583. json: {
  584. publicAccessLevel: 'tokenBased'
  585. }
  586. },
  587. (error, response, body) => {
  588. if (error != null) {
  589. return callback(error)
  590. }
  591. callback(null)
  592. }
  593. )
  594. }
  595. getCsrfToken(callback) {
  596. this.request.get(
  597. {
  598. url: '/dev/csrf'
  599. },
  600. (err, response, body) => {
  601. if (err != null) {
  602. return callback(err)
  603. }
  604. this.csrfToken = body
  605. this.request = this.request.defaults({
  606. headers: {
  607. 'x-csrf-token': this.csrfToken
  608. }
  609. })
  610. callback()
  611. }
  612. )
  613. }
  614. changePassword(callback) {
  615. this.getCsrfToken(error => {
  616. if (error != null) {
  617. return callback(error)
  618. }
  619. this.request.post(
  620. {
  621. url: '/user/password/update',
  622. json: {
  623. currentPassword: this.password,
  624. newPassword1: this.password,
  625. newPassword2: this.password
  626. }
  627. },
  628. callback
  629. )
  630. })
  631. }
  632. reconfirmAccountRequest(userEmail, callback) {
  633. this.getCsrfToken(error => {
  634. if (error != null) {
  635. return callback(error)
  636. }
  637. this.request.post(
  638. {
  639. url: '/user/reconfirm',
  640. json: {
  641. email: userEmail
  642. }
  643. },
  644. (error, response, body) => {
  645. callback(error, response)
  646. }
  647. )
  648. })
  649. }
  650. getUserSettingsPage(callback) {
  651. this.getCsrfToken(error => {
  652. if (error != null) {
  653. return callback(error)
  654. }
  655. this.request.get(
  656. {
  657. url: '/user/settings'
  658. },
  659. (error, response, body) => {
  660. if (error != null) {
  661. return callback(error)
  662. }
  663. callback(null, response.statusCode)
  664. }
  665. )
  666. })
  667. }
  668. updateSettings(newSettings, callback) {
  669. this.getCsrfToken(error => {
  670. if (error != null) {
  671. return callback(error)
  672. }
  673. this.request.post(
  674. {
  675. url: '/user/settings',
  676. json: newSettings
  677. },
  678. callback
  679. )
  680. })
  681. }
  682. getProjectListPage(callback) {
  683. this.getCsrfToken(error => {
  684. if (error != null) {
  685. return callback(error)
  686. }
  687. this.request.get(
  688. {
  689. url: '/project'
  690. },
  691. (error, response, body) => {
  692. if (error != null) {
  693. return callback(error)
  694. }
  695. callback(null, response.statusCode)
  696. }
  697. )
  698. })
  699. }
  700. isLoggedIn(callback) {
  701. this.request.get('/user/personal_info', (error, response, body) => {
  702. if (error != null) {
  703. return callback(error)
  704. }
  705. if (response.statusCode === 200) {
  706. callback(null, true)
  707. } else if (response.statusCode === 302) {
  708. callback(null, false)
  709. } else {
  710. callback(
  711. new Error(
  712. `unexpected status code from /user/personal_info: ${response.statusCode}`
  713. )
  714. )
  715. }
  716. })
  717. }
  718. transferProjectOwnership(projectId, userId, callback) {
  719. this.getCsrfToken(err => {
  720. if (err != null) {
  721. return callback(err)
  722. }
  723. this.request.post(
  724. {
  725. url: `/project/${projectId.toString()}/transfer-ownership`,
  726. json: {
  727. user_id: userId.toString()
  728. }
  729. },
  730. (err, response) => {
  731. if (err != null) {
  732. return callback(err)
  733. }
  734. if (response.statusCode !== 204) {
  735. return callback(
  736. new Error(`Unexpected status code: ${response.statusCode}`)
  737. )
  738. }
  739. callback()
  740. }
  741. )
  742. })
  743. }
  744. setV1Id(v1Id, callback) {
  745. UserModel.updateOne(
  746. {
  747. _id: this._id
  748. },
  749. {
  750. overleaf: {
  751. id: v1Id
  752. }
  753. },
  754. callback
  755. )
  756. }
  757. setCollaboratorInfo(projectId, userId, info, callback) {
  758. this.getCsrfToken(err => {
  759. if (err != null) {
  760. return callback(err)
  761. }
  762. this.request.put(
  763. {
  764. url: `/project/${projectId.toString()}/users/${userId.toString()}`,
  765. json: info
  766. },
  767. (err, response) => {
  768. if (err != null) {
  769. return callback(err)
  770. }
  771. if (response.statusCode !== 204) {
  772. return callback(
  773. new Error(`Unexpected status code: ${response.statusCode}`)
  774. )
  775. }
  776. callback()
  777. }
  778. )
  779. })
  780. }
  781. }
  782. User.promises = class extends User {
  783. doRequest(method, params) {
  784. return new Promise((resolve, reject) => {
  785. this.request[method.toLowerCase()](params, (err, response, body) => {
  786. if (err) {
  787. reject(err)
  788. } else {
  789. resolve({ response, body })
  790. }
  791. })
  792. })
  793. }
  794. }
  795. // promisify User class methods - works for methods with 0-1 output parameters,
  796. // otherwise we will need to implement the method manually instead
  797. const nonPromiseMethods = ['constructor', 'setExtraAttributes']
  798. Object.getOwnPropertyNames(User.prototype).forEach(methodName => {
  799. const method = User.prototype[methodName]
  800. if (typeof method === 'function' && !nonPromiseMethods.includes(methodName)) {
  801. User.promises.prototype[methodName] = promisify(method)
  802. }
  803. })
  804. module.exports = User