User.mjs 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355
  1. import OError from '@overleaf/o-error'
  2. import request from './request.js'
  3. import settings from '@overleaf/settings'
  4. import { db, ObjectId } from '../../../../app/src/infrastructure/mongodb.js'
  5. import { User as UserModel } from '../../../../app/src/models/User.js'
  6. import UserUpdater from '../../../../app/src/Features/User/UserUpdater.js'
  7. import AuthenticationManager from '../../../../app/src/Features/Authentication/AuthenticationManager.js'
  8. import { promisifyClass } from '@overleaf/promise-utils'
  9. import fs from 'node:fs'
  10. import Path from 'node:path'
  11. import { fileURLToPath } from 'node:url'
  12. import { Cookie } from 'tough-cookie'
  13. const __dirname = fileURLToPath(new URL('.', import.meta.url))
  14. const COOKIE_DOMAIN = settings.cookieDomain
  15. // The cookie domain has a leading '.' but the cookie jar stores it without.
  16. const DEFAULT_COOKIE_URL = `https://${COOKIE_DOMAIN.replace(/^\./, '')}/`
  17. let count = settings.test.counterInit
  18. class User {
  19. constructor(options) {
  20. if (options == null) {
  21. options = {}
  22. }
  23. this.emails = [
  24. {
  25. email: options.email || `acceptance-test-${count}@example.com`,
  26. createdAt: new Date(),
  27. confirmedAt: options.confirmedAt,
  28. },
  29. ]
  30. this.email = this.emails[0].email
  31. this.password = `a-terrible-secret-${count}`
  32. count++
  33. this.jar = request.jar()
  34. this.request = request.defaults({
  35. jar: this.jar,
  36. })
  37. this.signUpDate = options.signUpDate ?? new Date()
  38. this.labsProgram = options.labsProgram || false
  39. }
  40. getSession(options, callback) {
  41. if (typeof options === 'function') {
  42. callback = options
  43. options = {}
  44. }
  45. this.request.get(
  46. {
  47. url: '/dev/session',
  48. qs: options.set,
  49. },
  50. (err, response, body) => {
  51. if (err != null) {
  52. return callback(err)
  53. }
  54. if (response.statusCode !== 200) {
  55. return callback(
  56. new Error(
  57. `get session failed: status=${
  58. response.statusCode
  59. } body=${JSON.stringify(body)}`
  60. )
  61. )
  62. }
  63. const session = JSON.parse(response.body)
  64. callback(null, session)
  65. }
  66. )
  67. }
  68. setInSession(params, callback) {
  69. this.getCsrfToken(error => {
  70. if (error != null) {
  71. return callback(error)
  72. }
  73. this.request.post(
  74. {
  75. url: '/dev/set_in_session',
  76. json: params,
  77. },
  78. (err, response, body) => {
  79. if (err != null) {
  80. return callback(err)
  81. }
  82. if (response.statusCode !== 200) {
  83. return callback(
  84. new Error(
  85. `post set in session failed: status=${
  86. response.statusCode
  87. } body=${JSON.stringify(body)}`
  88. )
  89. )
  90. }
  91. callback(null)
  92. }
  93. )
  94. })
  95. }
  96. getSplitTestAssignment(splitTestName, query, callback) {
  97. if (!callback) {
  98. callback = query
  99. }
  100. const params = new URLSearchParams({
  101. splitTestName,
  102. ...query,
  103. }).toString()
  104. this.request.get(
  105. {
  106. url: `/dev/split_test/get_assignment?${params}`,
  107. },
  108. (err, response, body) => {
  109. if (err != null) {
  110. return callback(err)
  111. }
  112. if (response.statusCode !== 200) {
  113. return callback(
  114. new Error(
  115. `get split test assignment failed: status=${
  116. response.statusCode
  117. } body=${JSON.stringify(body)}`
  118. )
  119. )
  120. }
  121. const assignment = JSON.parse(response.body)
  122. callback(null, assignment)
  123. }
  124. )
  125. }
  126. doSessionMaintenance(callback) {
  127. this.request.post(
  128. {
  129. url: `/dev/split_test/session_maintenance`,
  130. },
  131. (err, response, body) => {
  132. if (err != null) {
  133. return callback(err)
  134. }
  135. if (response.statusCode !== 200) {
  136. return callback(
  137. new Error(
  138. `post session maintenance failed: status=${
  139. response.statusCode
  140. } body=${JSON.stringify(body)}`
  141. )
  142. )
  143. }
  144. callback(null)
  145. }
  146. )
  147. }
  148. optIntoBeta(callback) {
  149. this.request.post(
  150. {
  151. url: '/beta/opt-in',
  152. },
  153. (err, response, body) => {
  154. if (err != null) {
  155. return callback(err)
  156. }
  157. if (response.statusCode !== 302) {
  158. return callback(
  159. new Error(
  160. `post beta opt-in failed: status=${
  161. response.statusCode
  162. } body=${JSON.stringify(body)}`
  163. )
  164. )
  165. }
  166. callback(null)
  167. }
  168. )
  169. }
  170. optOutOfBeta(callback) {
  171. this.request.post(
  172. {
  173. url: '/beta/opt-out',
  174. },
  175. (err, response, body) => {
  176. if (err != null) {
  177. return callback(err)
  178. }
  179. if (response.statusCode !== 302) {
  180. return callback(
  181. new Error(
  182. `post beta opt-out failed: status=${
  183. response.statusCode
  184. } body=${JSON.stringify(body)}`
  185. )
  186. )
  187. }
  188. callback(null)
  189. }
  190. )
  191. }
  192. /* Return the session cookie, url decoded. Use the option {raw:true} to get the original undecoded value */
  193. sessionCookie(options) {
  194. const cookie = Cookie.parse(this.jar.getCookieString(DEFAULT_COOKIE_URL))
  195. if (cookie?.value && !options?.raw) {
  196. cookie.value = decodeURIComponent(cookie.value)
  197. }
  198. return cookie
  199. }
  200. /* Set the session cookie from a string and store it in the cookie jar, so that it will be used
  201. for subsequent requests. */
  202. setSessionCookie(cookie) {
  203. const sessionCookie = request.cookie(
  204. `${settings.cookieName}=${cookie}; Domain=${COOKIE_DOMAIN}; Max-age=3600; Path=/; SameSite=Lax`
  205. )
  206. this.jar.setCookie(sessionCookie, DEFAULT_COOKIE_URL)
  207. }
  208. getEmailConfirmationCode(callback) {
  209. this.getSession((err, session) => {
  210. if (err != null) {
  211. return callback(err)
  212. }
  213. const code = session.pendingUserRegistration?.confirmCode
  214. if (!code) {
  215. return callback(new Error('No confirmation code found in session'))
  216. }
  217. callback(null, code)
  218. })
  219. }
  220. resetCookies() {
  221. this.jar = request.jar()
  222. this.request = request.defaults({
  223. jar: this.jar,
  224. })
  225. }
  226. setExtraAttributes(user) {
  227. if (!user?._id) {
  228. throw new Error('User does not exist')
  229. }
  230. this.id = user._id.toString()
  231. this._id = user._id.toString()
  232. this.first_name = user.first_name
  233. this.referal_id = user.referal_id
  234. this.enrollment = user.enrollment
  235. }
  236. get(callback) {
  237. db.users.findOne({ _id: new ObjectId(this._id) }, callback)
  238. }
  239. getAuditLog(callback) {
  240. this.get((error, user) => {
  241. if (error) {
  242. return callback(error)
  243. }
  244. if (!user) {
  245. return callback(new Error('User not found'))
  246. }
  247. db.userAuditLogEntries
  248. .find({ userId: new ObjectId(this._id) })
  249. .toArray((error, auditLog) => {
  250. if (error) {
  251. return callback(error)
  252. }
  253. callback(null, auditLog || [])
  254. })
  255. })
  256. }
  257. getAuditLogWithoutNoise(callback) {
  258. this.getAuditLog((error, auditLog) => {
  259. if (error) {
  260. return callback(error)
  261. }
  262. callback(
  263. null,
  264. auditLog.filter(entry => {
  265. return entry.operation !== 'login'
  266. })
  267. )
  268. })
  269. }
  270. mongoUpdate(updateOp, callback) {
  271. db.users.updateOne({ _id: new ObjectId(this._id) }, updateOp, callback)
  272. }
  273. register(callback) {
  274. this.registerWithQuery('', callback)
  275. }
  276. registerWithQuery(query, callback) {
  277. if (this._id != null) {
  278. return callback(new Error('User already registered'))
  279. }
  280. this.getCsrfToken(error => {
  281. if (error != null) {
  282. return callback(error)
  283. }
  284. this.request.post(
  285. {
  286. url: `/register${query}`,
  287. json: { email: this.email, password: this.password },
  288. },
  289. (error, response, body) => {
  290. if (error != null) {
  291. return callback(error)
  292. }
  293. if (response.statusCode !== 200) {
  294. return callback(
  295. new Error(
  296. `register failed: status=${
  297. response.statusCode
  298. } body=${JSON.stringify(body)}`
  299. )
  300. )
  301. }
  302. this.getEmailConfirmationCode((error, code) => {
  303. if (error != null) {
  304. return callback(error)
  305. }
  306. this.request.post(
  307. {
  308. url: '/registration/confirm-email',
  309. json: { code },
  310. },
  311. (error, response, body) => {
  312. if (error != null) {
  313. return callback(error)
  314. }
  315. if (response.statusCode !== 200) {
  316. return callback(
  317. new Error(
  318. `email confirmation failed: status=${
  319. response.statusCode
  320. } body=${JSON.stringify(body)}`
  321. )
  322. )
  323. }
  324. db.users.findOne({ email: this.email }, (error, user) => {
  325. if (error != null) {
  326. return callback(error)
  327. }
  328. this.setExtraAttributes(user)
  329. callback(null, user, response)
  330. })
  331. }
  332. )
  333. })
  334. }
  335. )
  336. })
  337. }
  338. login(callback) {
  339. this.loginWith(this.email, callback)
  340. }
  341. loginWith(email, callback) {
  342. this.ensureUserExists(error => {
  343. if (error != null) {
  344. return callback(error)
  345. }
  346. this.loginWithEmailPassword(email, this.password, callback)
  347. })
  348. }
  349. loginNoUpdate(callback) {
  350. this.loginWithEmailPassword(this.email, this.password, callback)
  351. }
  352. loginWithEmailPassword(email, password, callback) {
  353. this.getCsrfToken(error => {
  354. if (error != null) {
  355. return callback(error)
  356. }
  357. this.request.post(
  358. {
  359. url: settings.enableLegacyLogin ? '/login/legacy' : '/login',
  360. json: {
  361. email,
  362. password,
  363. 'g-recaptcha-response': 'valid',
  364. },
  365. },
  366. (error, response, body) => {
  367. if (error != null) {
  368. return callback(error)
  369. }
  370. if (response.statusCode !== 200) {
  371. return callback(
  372. new OError(
  373. `login failed: status=${
  374. response.statusCode
  375. } body=${JSON.stringify(body)}`,
  376. { response, body }
  377. )
  378. )
  379. }
  380. // get new csrf token, then return result of login
  381. this.getCsrfToken(err => {
  382. if (err) {
  383. return callback(OError.tag(err, 'after login'))
  384. }
  385. callback(null, response, body)
  386. })
  387. }
  388. )
  389. })
  390. }
  391. ensureUserExists(callback) {
  392. if (this._id) {
  393. return callback()
  394. } // already exists
  395. const filter = { email: this.email }
  396. const options = { upsert: true, new: true, setDefaultsOnInsert: true }
  397. AuthenticationManager.hashPassword(
  398. this.password,
  399. (error, hashedPassword) => {
  400. if (error != null) {
  401. return callback(error)
  402. }
  403. UserModel.findOneAndUpdate(
  404. filter,
  405. {
  406. $set: {
  407. hashedPassword,
  408. emails: this.emails,
  409. signUpDate: this.signUpDate,
  410. labsProgram: this.labsProgram,
  411. },
  412. },
  413. options
  414. )
  415. .then(user => {
  416. this.setExtraAttributes(user)
  417. callback(null, this.password)
  418. })
  419. .catch(callback)
  420. }
  421. )
  422. }
  423. // Update and persist feature upgrade. Downgrades will be flaky!
  424. upgradeFeatures(features, callback) {
  425. this.setFeatures(features, err => {
  426. if (err) {
  427. return callback(err)
  428. }
  429. // Persist the feature update, otherwise the next feature refresh will reset them.
  430. this.setFeaturesOverride(
  431. {
  432. createdAt: new Date(),
  433. note: 'Some note',
  434. features,
  435. },
  436. callback
  437. )
  438. })
  439. }
  440. // Low-level. Temporary feature change. A feature refresh might reset them, e.g. "some time" after logging in.
  441. setFeatures(features, callback) {
  442. const update = {}
  443. for (const key in features) {
  444. const value = features[key]
  445. update[`features.${key}`] = value
  446. }
  447. UserModel.updateOne({ _id: this.id }, update)
  448. .then((...args) => callback(null, ...args))
  449. .catch(callback)
  450. }
  451. // Low-level. Permanent feature change. Feature overrides are not applied right away. A feature refresh might populate them "some time" after login/changing subscriptions.
  452. setFeaturesOverride(featuresOverride, callback) {
  453. if (!featuresOverride?.features) {
  454. throw new Error('bad featuresOverride schema')
  455. }
  456. const update = { $push: { featuresOverrides: featuresOverride } }
  457. UserModel.updateOne({ _id: this.id }, update)
  458. .then((...args) => callback(null, ...args))
  459. .catch(callback)
  460. }
  461. setOverleafId(overleafId, callback) {
  462. UserModel.updateOne({ _id: this.id }, { 'overleaf.id': overleafId })
  463. .then((...args) => callback(null, ...args))
  464. .catch(callback)
  465. }
  466. setEmails(emails, callback) {
  467. UserModel.updateOne({ _id: this.id }, { emails })
  468. .then((...args) => callback(null, ...args))
  469. .catch(callback)
  470. }
  471. setSuspended(suspended, callback) {
  472. UserModel.updateOne({ _id: this.id }, { suspended })
  473. .then((...args) => callback(null, ...args))
  474. .catch(callback)
  475. }
  476. logout(callback) {
  477. this.getCsrfToken(error => {
  478. if (error != null) {
  479. return callback(error)
  480. }
  481. this.request.post(
  482. {
  483. url: '/logout',
  484. },
  485. (error, response, body) => {
  486. if (error != null) {
  487. return callback(error)
  488. }
  489. if (response.statusCode >= 400) {
  490. return callback(
  491. new Error(
  492. `logout failed: status=${
  493. response.statusCode
  494. } body=${JSON.stringify(body)}`
  495. )
  496. )
  497. }
  498. db.users.findOne({ email: this.email }, (error, user) => {
  499. if (error != null) {
  500. return callback(error)
  501. }
  502. if (user == null) {
  503. return callback()
  504. }
  505. this.id = user._id.toString()
  506. this._id = user._id.toString()
  507. callback()
  508. })
  509. }
  510. )
  511. })
  512. }
  513. addEmail(email, callback) {
  514. this.emails.push({ email, createdAt: new Date() })
  515. UserUpdater.addEmailAddress(
  516. this.id,
  517. email,
  518. {},
  519. { initiatorId: this._id, ipAddress: '127:0:0:0' },
  520. callback
  521. )
  522. }
  523. confirmEmail(email, callback) {
  524. for (let idx = 0; idx < this.emails.length; idx++) {
  525. const emailData = this.emails[idx]
  526. if (emailData.email === email) {
  527. this.emails[idx].confirmedAt = new Date()
  528. }
  529. }
  530. UserUpdater.confirmEmail(this.id, email, callback)
  531. }
  532. ensureAdmin(callback) {
  533. this.mongoUpdate({ $set: { isAdmin: true } }, callback)
  534. }
  535. ensureAdminRole(role, callback) {
  536. this.mongoUpdate({ $addToSet: { adminRoles: 'engineering' } }, callback)
  537. }
  538. ensureStaffAccess(flag, callback) {
  539. const update = { $set: {} }
  540. update.$set[`staffAccess.${flag}`] = true
  541. this.mongoUpdate(update, callback)
  542. }
  543. upgradeSomeFeatures(callback) {
  544. const features = {
  545. collaborators: -1, // Infinite
  546. versioning: true,
  547. dropbox: true,
  548. compileTimeout: 60,
  549. compileGroup: 'priority',
  550. references: true,
  551. trackChanges: true,
  552. trackChangesVisible: true,
  553. }
  554. this.mongoUpdate({ $set: { features } }, callback)
  555. }
  556. downgradeFeatures(callback) {
  557. const features = {
  558. collaborators: 1,
  559. versioning: false,
  560. dropbox: false,
  561. compileTimeout: 60,
  562. compileGroup: 'standard',
  563. references: false,
  564. trackChanges: false,
  565. trackChangesVisible: false,
  566. }
  567. this.mongoUpdate({ $set: { features } }, callback)
  568. }
  569. defaultFeatures(callback) {
  570. const features = settings.defaultFeatures
  571. this.mongoUpdate({ $set: { features } }, callback)
  572. }
  573. getFeatures(callback) {
  574. db.users.findOne(
  575. { _id: new ObjectId(this.id) },
  576. { projection: { features: 1 } },
  577. (error, user) => callback(error, user && user.features)
  578. )
  579. }
  580. fullDeleteUser(email, callback) {
  581. db.users.findOne({ email }, (error, user) => {
  582. if (error != null) {
  583. return callback(error)
  584. }
  585. if (user == null) {
  586. return callback()
  587. }
  588. const userId = user._id
  589. db.projects.deleteMany({ owner_ref: new ObjectId(userId) }, err => {
  590. if (err != null) {
  591. callback(err)
  592. }
  593. db.users.deleteOne({ _id: new ObjectId(userId) }, callback)
  594. })
  595. })
  596. }
  597. deleteUser(callback) {
  598. this.getCsrfToken(error => {
  599. if (error) {
  600. return callback(error)
  601. }
  602. this.request.post(
  603. {
  604. url: '/user/delete',
  605. json: { password: this.password },
  606. },
  607. (err, response, body) => {
  608. if (err) {
  609. return callback(err)
  610. }
  611. if (response.statusCode !== 200) {
  612. return callback(
  613. new Error(
  614. `user deletion failed: status=${
  615. response.statusCode
  616. } body=${JSON.stringify(body)}`
  617. )
  618. )
  619. }
  620. callback()
  621. }
  622. )
  623. })
  624. }
  625. getProject(projectId, callback) {
  626. db.projects.findOne({ _id: new ObjectId(projectId.toString()) }, callback)
  627. }
  628. saveProject(project, callback) {
  629. db.projects.updateOne({ _id: project._id }, { $set: project }, callback)
  630. }
  631. createProject(name, options, callback) {
  632. if (typeof options === 'function') {
  633. callback = options
  634. options = {}
  635. }
  636. this.request.post(
  637. {
  638. url: '/project/new',
  639. json: Object.assign({ projectName: name }, options),
  640. },
  641. (error, response, body) => {
  642. if (error != null) {
  643. return callback(error)
  644. }
  645. if (response.statusCode !== 200) {
  646. return callback(
  647. new Error(
  648. `project creation failed: status=${
  649. response.statusCode
  650. } body=${JSON.stringify(body)}`
  651. )
  652. )
  653. }
  654. if ((body != null ? body.project_id : undefined) == null) {
  655. error = new Error(
  656. JSON.stringify([
  657. 'SOMETHING WENT WRONG CREATING PROJECT',
  658. name,
  659. options,
  660. response.statusCode,
  661. response.headers.location,
  662. body,
  663. ])
  664. )
  665. callback(error)
  666. } else {
  667. callback(null, body.project_id)
  668. }
  669. }
  670. )
  671. }
  672. deleteProject(projectId, callback) {
  673. this.request.delete(
  674. {
  675. url: `/project/${projectId}`,
  676. },
  677. (error, response, body) => {
  678. if (error != null) {
  679. return callback(error)
  680. }
  681. if (response.statusCode !== 200) {
  682. return callback(
  683. new Error(
  684. `project deletion failed: status=${
  685. response.statusCode
  686. } body=${JSON.stringify(body)}`
  687. )
  688. )
  689. }
  690. callback(null)
  691. }
  692. )
  693. }
  694. undeleteProject(projectId, callback) {
  695. this.request.post(
  696. {
  697. url: `/admin/project/${projectId}/undelete`,
  698. },
  699. (error, response, body) => {
  700. if (error) {
  701. return callback(error)
  702. }
  703. if (response.statusCode !== 204) {
  704. return callback(
  705. new Error(
  706. `project un-deletion failed: status=${
  707. response.statusCode
  708. } body=${JSON.stringify(body)}`
  709. )
  710. )
  711. }
  712. callback(null)
  713. }
  714. )
  715. }
  716. deleteProjects(callback) {
  717. db.projects.deleteMany({ owner_ref: new ObjectId(this.id) }, callback)
  718. }
  719. openProject(projectId, callback) {
  720. this.request.get(
  721. {
  722. url: `/project/${projectId}`,
  723. },
  724. (error, response, body) => {
  725. if (error != null) {
  726. return callback(error)
  727. }
  728. if (response.statusCode !== 200) {
  729. return callback(
  730. new Error(
  731. `opening project failed: status=${
  732. response.statusCode
  733. } body=${JSON.stringify(body)}`
  734. )
  735. )
  736. }
  737. callback(null)
  738. }
  739. )
  740. }
  741. createDocInProject(projectId, parentFolderId, name, callback) {
  742. this.getCsrfToken(error => {
  743. if (error != null) {
  744. return callback(error)
  745. }
  746. this.request.post(
  747. {
  748. url: `/project/${projectId}/doc`,
  749. json: {
  750. name,
  751. parentFolderId,
  752. },
  753. },
  754. (error, response, body) => {
  755. if (error != null) {
  756. return callback(error)
  757. }
  758. if (response.statusCode !== 200) {
  759. return callback(
  760. new Error(
  761. `doc creation in project failed: status=${
  762. response.statusCode
  763. } body=${JSON.stringify(body)}`
  764. )
  765. )
  766. }
  767. callback(null, body._id)
  768. }
  769. )
  770. })
  771. }
  772. uploadFileInProject(projectId, folderId, file, name, contentType, callback) {
  773. this.uploadFileInProjectFull(
  774. projectId,
  775. folderId,
  776. file,
  777. name,
  778. contentType,
  779. (err, body) => callback(err, body?.entity_id)
  780. )
  781. }
  782. uploadFileInProjectFull(
  783. projectId,
  784. folderId,
  785. file,
  786. name,
  787. contentType,
  788. callback
  789. ) {
  790. const fileStream = fs.createReadStream(
  791. Path.resolve(Path.join(__dirname, '..', '..', 'files', file))
  792. )
  793. this.request.post(
  794. {
  795. uri: `project/${projectId}/upload`,
  796. qs: {
  797. folder_id: String(folderId),
  798. },
  799. formData: {
  800. name,
  801. qqfile: {
  802. value: fileStream,
  803. options: {
  804. filename: name,
  805. contentType,
  806. },
  807. },
  808. },
  809. },
  810. (error, response, body) => {
  811. if (error) {
  812. return callback(error)
  813. }
  814. if (response.statusCode !== 200) {
  815. return callback(
  816. new Error(
  817. `uploading file failed: status=${
  818. response.statusCode
  819. } body=${JSON.stringify(body)}`
  820. )
  821. )
  822. }
  823. callback(null, JSON.parse(body))
  824. }
  825. )
  826. }
  827. uploadExampleFileInProject(projectId, folderId, name, callback) {
  828. this.uploadFileInProject(
  829. projectId,
  830. folderId,
  831. '1pixel.png',
  832. name,
  833. 'image/png',
  834. callback
  835. )
  836. }
  837. moveItemInProject(projectId, type, itemId, folderId, callback) {
  838. this.request.post(
  839. {
  840. uri: `project/${projectId}/${type}/${itemId}/move`,
  841. json: {
  842. folder_id: folderId,
  843. },
  844. },
  845. (error, response, body) => {
  846. if (error) {
  847. return callback(error)
  848. }
  849. if (response.statusCode !== 200) {
  850. return callback(
  851. new Error(
  852. `move item in project failed: type=${type} status=${
  853. response.statusCode
  854. } body=${JSON.stringify(body)}`
  855. )
  856. )
  857. }
  858. callback()
  859. }
  860. )
  861. }
  862. renameItemInProject(projectId, type, itemId, name, callback) {
  863. this.request.post(
  864. {
  865. uri: `project/${projectId}/${type}/${itemId}/rename`,
  866. json: {
  867. name,
  868. },
  869. },
  870. (error, response, body) => {
  871. if (error) {
  872. return callback(error)
  873. }
  874. if (response.statusCode !== 204) {
  875. return callback(
  876. new Error(
  877. `rename item in project failed: type=${type} status=${
  878. response.statusCode
  879. } body=${JSON.stringify(body)}`
  880. )
  881. )
  882. }
  883. callback()
  884. }
  885. )
  886. }
  887. deleteItemInProject(projectId, type, itemId, callback) {
  888. this.request.delete(
  889. `project/${projectId}/${type}/${itemId}`,
  890. (error, response, body) => {
  891. if (error) {
  892. return callback(error)
  893. }
  894. if (response.statusCode !== 204) {
  895. return callback(
  896. new Error(
  897. `delete item in project failed: type=${type} status=${
  898. response.statusCode
  899. } body=${JSON.stringify(body)}`
  900. )
  901. )
  902. }
  903. callback()
  904. }
  905. )
  906. }
  907. joinProject(projectId, callback) {
  908. this.request.post(
  909. {
  910. url: `/project/${projectId}/join`,
  911. auth: {
  912. user: settings.apis.web.user,
  913. pass: settings.apis.web.pass,
  914. sendImmediately: true,
  915. },
  916. json: { userId: this._id },
  917. jar: false,
  918. },
  919. (error, response, body) => {
  920. if (error) {
  921. return callback(error)
  922. }
  923. if (response.statusCode !== 200) {
  924. return callback(
  925. new Error(
  926. `join project failed: status=${
  927. response.statusCode
  928. } body=${JSON.stringify(body)}`
  929. )
  930. )
  931. }
  932. callback(null, body)
  933. }
  934. )
  935. }
  936. addUserToProject(projectId, user, privileges, callback) {
  937. let updateOp
  938. if (privileges === 'readAndWrite') {
  939. updateOp = { $addToSet: { collaberator_refs: user._id } }
  940. } else if (privileges === 'readOnly') {
  941. updateOp = { $addToSet: { readOnly_refs: user._id } }
  942. } else if (privileges === 'pendingEditor') {
  943. updateOp = {
  944. $addToSet: { readOnly_refs: user._id, pendingEditor_refs: user._id },
  945. }
  946. } else if (privileges === 'pendingReviewer') {
  947. updateOp = {
  948. $addToSet: { readOnly_refs: user._id, pendingReviewer_refs: user._id },
  949. }
  950. } else if (privileges === 'review') {
  951. updateOp = {
  952. $addToSet: { reviewer_refs: user._id },
  953. }
  954. }
  955. db.projects.updateOne({ _id: new ObjectId(projectId) }, updateOp, callback)
  956. }
  957. makePublic(projectId, level, callback) {
  958. // A fudge, to get around the fact that `readOnly` and `readAndWrite` are now disallowed
  959. // via the API, but we still need to test the behaviour of projects with these values set.
  960. db.projects.updateOne(
  961. { _id: new ObjectId(projectId) },
  962. // NOTE: Yes, there is a typo in the db schema.
  963. { $set: { publicAccesLevel: level } },
  964. callback
  965. )
  966. }
  967. makePrivate(projectId, callback) {
  968. this.request.post(
  969. {
  970. url: `/project/${projectId}/settings/admin`,
  971. json: {
  972. publicAccessLevel: 'private',
  973. },
  974. },
  975. (error, response, body) => {
  976. if (error != null) {
  977. return callback(error)
  978. }
  979. if (response.statusCode !== 204) {
  980. return callback(
  981. new Error(
  982. `disable link sharing failed: status=${
  983. response.statusCode
  984. } body=${JSON.stringify(body)}`
  985. )
  986. )
  987. }
  988. callback(null)
  989. }
  990. )
  991. }
  992. makeTokenBased(projectId, callback) {
  993. this.request.post(
  994. {
  995. url: `/project/${projectId}/settings/admin`,
  996. json: {
  997. publicAccessLevel: 'tokenBased',
  998. },
  999. },
  1000. (error, response, body) => {
  1001. if (error != null) {
  1002. return callback(error)
  1003. }
  1004. if (response.statusCode !== 204) {
  1005. return callback(
  1006. new Error(
  1007. `enable link sharing failed: status=${
  1008. response.statusCode
  1009. } body=${JSON.stringify(body)}`
  1010. )
  1011. )
  1012. }
  1013. callback(null)
  1014. }
  1015. )
  1016. }
  1017. getCsrfToken(callback) {
  1018. this.request.get(
  1019. {
  1020. url: '/dev/csrf',
  1021. },
  1022. (err, response, body) => {
  1023. if (err != null) {
  1024. return callback(err)
  1025. }
  1026. if (response.statusCode !== 200) {
  1027. return callback(
  1028. new Error(
  1029. `get csrf token failed: status=${
  1030. response.statusCode
  1031. } body=${JSON.stringify(body)}`
  1032. )
  1033. )
  1034. }
  1035. this.csrfToken = body
  1036. this.request = this.request.defaults({
  1037. headers: {
  1038. 'x-csrf-token': this.csrfToken,
  1039. },
  1040. })
  1041. callback()
  1042. }
  1043. )
  1044. }
  1045. changePassword(newPassword, callback) {
  1046. this.getCsrfToken(error => {
  1047. if (error != null) {
  1048. return callback(error)
  1049. }
  1050. this.request.post(
  1051. {
  1052. url: '/user/password/update',
  1053. json: {
  1054. currentPassword: this.password,
  1055. newPassword1: newPassword,
  1056. newPassword2: newPassword,
  1057. },
  1058. },
  1059. (err, response, body) => {
  1060. if (err) {
  1061. return callback(err)
  1062. }
  1063. if (response.statusCode !== 200) {
  1064. return callback(
  1065. new Error(
  1066. `change password failed: status=${
  1067. response.statusCode
  1068. } body=${JSON.stringify(body)}`
  1069. )
  1070. )
  1071. }
  1072. this.password = newPassword
  1073. callback()
  1074. }
  1075. )
  1076. })
  1077. }
  1078. reconfirmAccountRequest(userEmail, callback) {
  1079. this.getCsrfToken(error => {
  1080. if (error != null) {
  1081. return callback(error)
  1082. }
  1083. this.request.post(
  1084. {
  1085. url: '/user/reconfirm',
  1086. json: {
  1087. email: userEmail,
  1088. },
  1089. },
  1090. (error, response) => {
  1091. callback(error, response)
  1092. }
  1093. )
  1094. })
  1095. }
  1096. getUserSettingsPage(callback) {
  1097. this.getCsrfToken(error => {
  1098. if (error != null) {
  1099. return callback(error)
  1100. }
  1101. this.request.get(
  1102. {
  1103. url: '/user/settings',
  1104. },
  1105. (error, response, body) => {
  1106. if (error != null) {
  1107. return callback(error)
  1108. }
  1109. if (response.statusCode !== 200) {
  1110. return callback(
  1111. new Error(
  1112. `open settings page failed: status=${
  1113. response.statusCode
  1114. } body=${JSON.stringify(body)}`
  1115. )
  1116. )
  1117. }
  1118. callback(null, response.statusCode)
  1119. }
  1120. )
  1121. })
  1122. }
  1123. updateSettings(newSettings, callback) {
  1124. this.getCsrfToken(error => {
  1125. if (error != null) {
  1126. return callback(error)
  1127. }
  1128. this.request.post(
  1129. {
  1130. url: '/user/settings',
  1131. json: newSettings,
  1132. },
  1133. (err, response, body) => {
  1134. if (err) {
  1135. return callback(err)
  1136. }
  1137. if (response.statusCode !== 200) {
  1138. return callback(
  1139. new Error(
  1140. `update settings failed: status=${
  1141. response.statusCode
  1142. } body=${JSON.stringify(body)}`
  1143. )
  1144. )
  1145. }
  1146. callback()
  1147. }
  1148. )
  1149. })
  1150. }
  1151. getProjectListPage(callback) {
  1152. this.request.get('/project', (error, response) => {
  1153. if (error != null) {
  1154. return callback(error)
  1155. }
  1156. callback(null, response.statusCode)
  1157. })
  1158. }
  1159. isLoggedIn(callback) {
  1160. this.request.get('/user/personal_info', (error, response, body) => {
  1161. if (error != null) {
  1162. return callback(error)
  1163. }
  1164. if (response.statusCode === 200) {
  1165. callback(null, true)
  1166. } else if (response.statusCode === 302) {
  1167. callback(null, false)
  1168. } else {
  1169. callback(
  1170. new Error(
  1171. `unexpected status code from /user/personal_info: status=${response.statusCode} body=${body}`
  1172. )
  1173. )
  1174. }
  1175. })
  1176. }
  1177. transferProjectOwnership(projectId, userId, callback) {
  1178. this.getCsrfToken(err => {
  1179. if (err != null) {
  1180. return callback(err)
  1181. }
  1182. this.request.post(
  1183. {
  1184. url: `/project/${projectId.toString()}/transfer-ownership`,
  1185. json: {
  1186. user_id: userId.toString(),
  1187. },
  1188. },
  1189. (err, response, body) => {
  1190. if (err != null) {
  1191. return callback(err)
  1192. }
  1193. if (response.statusCode !== 204) {
  1194. return callback(
  1195. new Error(
  1196. `transfer project ownership failed: status=${
  1197. response.statusCode
  1198. } body=${JSON.stringify(body)}`
  1199. )
  1200. )
  1201. }
  1202. callback()
  1203. }
  1204. )
  1205. })
  1206. }
  1207. setV1Id(v1Id, callback) {
  1208. UserModel.updateOne(
  1209. {
  1210. _id: this._id,
  1211. },
  1212. {
  1213. overleaf: {
  1214. id: v1Id,
  1215. },
  1216. }
  1217. )
  1218. .then((...args) => callback(null, ...args))
  1219. .catch(callback)
  1220. }
  1221. setCollaboratorInfo(projectId, userId, info, callback) {
  1222. this.getCsrfToken(err => {
  1223. if (err != null) {
  1224. return callback(err)
  1225. }
  1226. this.request.put(
  1227. {
  1228. url: `/project/${projectId.toString()}/users/${userId.toString()}`,
  1229. json: info,
  1230. },
  1231. (err, response, body) => {
  1232. if (err != null) {
  1233. return callback(err)
  1234. }
  1235. if (response.statusCode !== 204) {
  1236. return callback(
  1237. new Error(
  1238. `update collaborator access failed: status=${
  1239. response.statusCode
  1240. } body=${JSON.stringify(body)}`
  1241. )
  1242. )
  1243. }
  1244. callback()
  1245. }
  1246. )
  1247. })
  1248. }
  1249. }
  1250. User.promises = promisifyClass(User, {
  1251. without: ['setExtraAttributes', 'sessionCookie', 'setSessionCookie'],
  1252. })
  1253. User.promises.prototype.doRequest = async function (method, params) {
  1254. return new Promise((resolve, reject) => {
  1255. this.request[method.toLowerCase()](params, (err, response, body) => {
  1256. if (err) {
  1257. reject(err)
  1258. } else {
  1259. resolve({ response, body })
  1260. }
  1261. })
  1262. })
  1263. }
  1264. export default User