| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903 |
- const request = require('./request')
- const settings = require('@overleaf/settings')
- const { db, ObjectId } = require('../../../../app/src/infrastructure/mongodb')
- const UserModel = require('../../../../app/src/models/User').User
- const UserUpdater = require('../../../../app/src/Features/User/UserUpdater')
- const AuthenticationManager = require('../../../../app/src/Features/Authentication/AuthenticationManager')
- const { promisify } = require('util')
- const fs = require('fs')
- const Path = require('path')
- let count = settings.test.counterInit
- class User {
- constructor(options) {
- if (options == null) {
- options = {}
- }
- this.emails = [
- {
- email: options.email || `acceptance-test-${count}@example.com`,
- createdAt: new Date(),
- },
- ]
- this.email = this.emails[0].email
- this.password = `a-terrible-secret-${count}`
- count++
- this.jar = request.jar()
- this.request = request.defaults({
- jar: this.jar,
- })
- }
- setExtraAttributes(user) {
- if ((user != null ? user._id : undefined) == null) {
- throw new Error('User does not exist')
- }
- this.id = user._id.toString()
- this._id = user._id.toString()
- this.first_name = user.first_name
- this.referal_id = user.referal_id
- }
- get(callback) {
- db.users.findOne({ _id: ObjectId(this._id) }, callback)
- }
- getAuditLog(callback) {
- this.get((error, user) => {
- if (error) return callback(error)
- if (!user) return callback(new Error('User not found'))
- db.userAuditLogEntries
- .find({ userId: ObjectId(this._id) })
- .toArray((error, auditLog) => {
- if (error) return callback(error)
- callback(null, auditLog || [])
- })
- })
- }
- getAuditLogWithoutNoise(callback) {
- this.getAuditLog((error, auditLog) => {
- if (error) return callback(error)
- callback(
- null,
- auditLog.filter(entry => {
- return entry.operation !== 'login'
- })
- )
- })
- }
- mongoUpdate(updateOp, callback) {
- db.users.updateOne({ _id: ObjectId(this._id) }, updateOp, callback)
- }
- register(callback) {
- this.registerWithQuery('', callback)
- }
- registerWithQuery(query, callback) {
- if (this._id != null) {
- return callback(new Error('User already registered'))
- }
- this.getCsrfToken(error => {
- if (error != null) {
- return callback(error)
- }
- this.request.post(
- {
- url: `/register${query}`,
- json: { email: this.email, password: this.password },
- },
- (error, response, body) => {
- if (error != null) {
- return callback(error)
- }
- db.users.findOne({ email: this.email }, (error, user) => {
- if (error != null) {
- return callback(error)
- }
- this.setExtraAttributes(user)
- callback(null, user)
- })
- }
- )
- })
- }
- login(callback) {
- this.loginWith(this.email, callback)
- }
- loginWith(email, callback) {
- this.ensureUserExists(error => {
- if (error != null) {
- return callback(error)
- }
- this.loginWithEmailPassword(email, this.password, callback)
- })
- }
- loginNoUpdate(callback) {
- this.loginWithEmailPassword(this.email, this.password, callback)
- }
- loginWithEmailPassword(email, password, callback) {
- this.getCsrfToken(error => {
- if (error != null) {
- return callback(error)
- }
- this.request.post(
- {
- url: settings.enableLegacyLogin ? '/login/legacy' : '/login',
- json: {
- email,
- password,
- 'g-recaptcha-response': 'valid',
- },
- },
- (error, response, body) => {
- if (error != null) {
- return callback(error)
- }
- // get new csrf token, then return result of login
- this.getCsrfToken(err => {
- if (err) {
- return callback(err)
- }
- callback(null, response, body)
- })
- }
- )
- })
- }
- ensureUserExists(callback) {
- const filter = { email: this.email }
- const options = { upsert: true, new: true, setDefaultsOnInsert: true }
- AuthenticationManager.hashPassword(
- this.password,
- (error, hashedPassword) => {
- if (error != null) {
- return callback(error)
- }
- UserModel.findOneAndUpdate(
- filter,
- { $set: { hashedPassword, emails: this.emails } },
- options,
- (error, user) => {
- if (error != null) {
- return callback(error)
- }
- this.setExtraAttributes(user)
- callback(null, this.password)
- }
- )
- }
- )
- }
- setFeatures(features, callback) {
- const update = {}
- for (const key in features) {
- const value = features[key]
- update[`features.${key}`] = value
- }
- UserModel.updateOne({ _id: this.id }, update, callback)
- }
- setFeaturesOverride(featuresOverride, callback) {
- const update = { $push: { featuresOverrides: featuresOverride } }
- UserModel.updateOne({ _id: this.id }, update, callback)
- }
- setOverleafId(overleafId, callback) {
- UserModel.updateOne(
- { _id: this.id },
- { 'overleaf.id': overleafId },
- callback
- )
- }
- logout(callback) {
- this.getCsrfToken(error => {
- if (error != null) {
- return callback(error)
- }
- this.request.post(
- {
- url: '/logout',
- json: {
- email: this.email,
- password: this.password,
- },
- },
- (error, response, body) => {
- if (error != null) {
- return callback(error)
- }
- db.users.findOne({ email: this.email }, (error, user) => {
- if (error != null) {
- return callback(error)
- }
- if (user == null) {
- return callback()
- }
- this.id = user._id.toString()
- this._id = user._id.toString()
- callback()
- })
- }
- )
- })
- }
- addEmail(email, callback) {
- this.emails.push({ email, createdAt: new Date() })
- UserUpdater.addEmailAddress(
- this.id,
- email,
- {},
- { initiatorId: this._id, ipAddress: '127:0:0:0' },
- callback
- )
- }
- confirmEmail(email, callback) {
- for (let idx = 0; idx < this.emails.length; idx++) {
- const emailData = this.emails[idx]
- if (emailData.email === email) {
- this.emails[idx].confirmedAt = new Date()
- }
- }
- UserUpdater.confirmEmail(this.id, email, callback)
- }
- ensureAdmin(callback) {
- this.mongoUpdate({ $set: { isAdmin: true } }, callback)
- }
- ensureStaffAccess(flag, callback) {
- const update = { $set: {} }
- update.$set[`staffAccess.${flag}`] = true
- this.mongoUpdate(update, callback)
- }
- upgradeFeatures(callback) {
- const features = {
- collaborators: -1, // Infinite
- versioning: true,
- dropbox: true,
- compileTimeout: 60,
- compileGroup: 'priority',
- templates: true,
- references: true,
- trackChanges: true,
- trackChangesVisible: true,
- }
- this.mongoUpdate({ $set: { features } }, callback)
- }
- downgradeFeatures(callback) {
- const features = {
- collaborators: 1,
- versioning: false,
- dropbox: false,
- compileTimeout: 60,
- compileGroup: 'standard',
- templates: false,
- references: false,
- trackChanges: false,
- trackChangesVisible: false,
- }
- this.mongoUpdate({ $set: { features } }, callback)
- }
- defaultFeatures(callback) {
- const features = settings.defaultFeatures
- this.mongoUpdate({ $set: { features } }, callback)
- }
- getFeatures(callback) {
- db.users.findOne(
- { _id: ObjectId(this.id) },
- { projection: { features: 1 } },
- (error, user) => callback(error, user && user.features)
- )
- }
- fullDeleteUser(email, callback) {
- db.users.findOne({ email }, (error, user) => {
- if (error != null) {
- return callback(error)
- }
- if (user == null) {
- return callback()
- }
- const userId = user._id
- db.projects.deleteMany({ owner_ref: ObjectId(userId) }, err => {
- if (err != null) {
- callback(err)
- }
- db.users.deleteOne({ _id: ObjectId(userId) }, callback)
- })
- })
- }
- deleteUser(callback) {
- this.getCsrfToken(error => {
- if (error) {
- return callback(error)
- }
- this.request.post(
- {
- url: '/user/delete',
- json: { password: this.password },
- },
- (err, res) => {
- if (err) {
- return callback(err)
- }
- if (res.statusCode < 200 || res.statusCode >= 300) {
- return callback(
- new Error('Error received from API: ' + res.statusCode)
- )
- }
- callback()
- }
- )
- })
- }
- getProject(projectId, callback) {
- db.projects.findOne({ _id: ObjectId(projectId.toString()) }, callback)
- }
- saveProject(project, callback) {
- db.projects.updateOne({ _id: project._id }, { $set: project }, callback)
- }
- createProject(name, options, callback) {
- if (typeof options === 'function') {
- callback = options
- options = {}
- }
- this.request.post(
- {
- url: '/project/new',
- json: Object.assign({ projectName: name }, options),
- },
- (error, response, body) => {
- if (error != null) {
- return callback(error)
- }
- if ((body != null ? body.project_id : undefined) == null) {
- error = new Error(
- JSON.stringify([
- 'SOMETHING WENT WRONG CREATING PROJECT',
- name,
- options,
- response.statusCode,
- response.headers.location,
- body,
- ])
- )
- callback(error)
- } else {
- callback(null, body.project_id)
- }
- }
- )
- }
- deleteProject(projectId, callback) {
- this.request.delete(
- {
- url: `/project/${projectId}`,
- },
- (error, response, body) => {
- if (error != null) {
- return callback(error)
- }
- callback(null)
- }
- )
- }
- undeleteProject(projectId, callback) {
- this.request.post(
- {
- url: `/admin/project/${projectId}/undelete`,
- },
- (error, response) => {
- if (error) {
- return callback(error)
- }
- if (response.statusCode !== 204) {
- return callback(
- new Error(
- `Non-success response when undeleting project: ${response.statusCode}`
- )
- )
- }
- callback(null)
- }
- )
- }
- deleteProjects(callback) {
- db.projects.deleteMany({ owner_ref: ObjectId(this.id) }, callback)
- }
- openProject(projectId, callback) {
- this.request.get(
- {
- url: `/project/${projectId}`,
- },
- (error, response, body) => {
- if (error != null) {
- return callback(error)
- }
- if (response.statusCode !== 200) {
- const err = new Error(
- `Non-success response when opening project: ${response.statusCode}`
- )
- return callback(err)
- }
- callback(null)
- }
- )
- }
- createDocInProject(projectId, parentFolderId, name, callback) {
- this.getCsrfToken(error => {
- if (error != null) {
- return callback(error)
- }
- this.request.post(
- {
- url: `/project/${projectId}/doc`,
- json: {
- name,
- parentFolderId,
- },
- },
- (error, response, body) => {
- if (error != null) {
- return callback(error)
- }
- callback(null, body._id)
- }
- )
- })
- }
- uploadFileInProject(projectId, folderId, file, name, contentType, callback) {
- const imageFile = fs.createReadStream(
- Path.resolve(Path.join(__dirname, '..', '..', 'files', file))
- )
- this.request.post(
- {
- uri: `project/${projectId}/upload`,
- qs: {
- folder_id: String(folderId),
- },
- formData: {
- qqfile: {
- value: imageFile,
- options: {
- filename: name,
- contentType,
- },
- },
- },
- },
- (error, res, body) => {
- if (error) {
- return callback(error)
- }
- if (res.statusCode < 200 || res.statusCode >= 300) {
- return callback(new Error(`failed to upload file ${res.statusCode}`))
- }
- callback(null, JSON.parse(body).entity_id)
- }
- )
- }
- uploadExampleFileInProject(projectId, folderId, name, callback) {
- this.uploadFileInProject(
- projectId,
- folderId,
- '1pixel.png',
- name,
- 'image/png',
- callback
- )
- }
- moveItemInProject(projectId, type, itemId, folderId, callback) {
- this.request.post(
- {
- uri: `project/${projectId}/${type}/${itemId}/move`,
- json: {
- folder_id: folderId,
- },
- },
- (error, res) => {
- if (error) {
- return callback(error)
- }
- if (res.statusCode < 200 || res.statusCode >= 300) {
- return callback(new Error(`failed to move ${type} ${res.statusCode}`))
- }
- callback()
- }
- )
- }
- renameItemInProject(projectId, type, itemId, name, callback) {
- this.request.post(
- {
- uri: `project/${projectId}/${type}/${itemId}/rename`,
- json: {
- name,
- },
- },
- (error, res) => {
- if (error) {
- return callback(error)
- }
- if (res.statusCode < 200 || res.statusCode >= 300) {
- return callback(
- new Error(`failed to rename ${type} ${res.statusCode}`)
- )
- }
- callback()
- }
- )
- }
- deleteItemInProject(projectId, type, itemId, callback) {
- this.request.delete(
- `project/${projectId}/${type}/${itemId}`,
- (error, res) => {
- if (error) {
- return callback(error)
- }
- if (res.statusCode < 200 || res.statusCode >= 300) {
- return callback(
- new Error(`failed to delete ${type} ${res.statusCode}`)
- )
- }
- callback()
- }
- )
- }
- joinProject(projectId, callback) {
- this.request.post(
- {
- url: `/project/${projectId}/join`,
- qs: { user_id: this._id },
- auth: {
- user: settings.apis.web.user,
- pass: settings.apis.web.pass,
- sendImmediately: true,
- },
- json: true,
- jar: false,
- },
- (error, res, body) => {
- if (error) {
- return callback(error)
- }
- if (res.statusCode < 200 || res.statusCode >= 300) {
- return callback(
- new Error(`failed to join project ${projectId} ${res.statusCode}`)
- )
- }
- callback(null, body)
- }
- )
- }
- addUserToProject(projectId, user, privileges, callback) {
- let updateOp
- if (privileges === 'readAndWrite') {
- updateOp = { $addToSet: { collaberator_refs: user._id } }
- } else if (privileges === 'readOnly') {
- updateOp = { $addToSet: { readOnly_refs: user._id } }
- }
- db.projects.updateOne({ _id: ObjectId(projectId) }, updateOp, callback)
- }
- makePublic(projectId, level, callback) {
- this.request.post(
- {
- url: `/project/${projectId}/settings/admin`,
- json: {
- publicAccessLevel: level,
- },
- },
- (error, response, body) => {
- if (error != null) {
- return callback(error)
- }
- callback(null)
- }
- )
- }
- makePrivate(projectId, callback) {
- this.request.post(
- {
- url: `/project/${projectId}/settings/admin`,
- json: {
- publicAccessLevel: 'private',
- },
- },
- (error, response, body) => {
- if (error != null) {
- return callback(error)
- }
- callback(null)
- }
- )
- }
- makeTokenBased(projectId, callback) {
- this.request.post(
- {
- url: `/project/${projectId}/settings/admin`,
- json: {
- publicAccessLevel: 'tokenBased',
- },
- },
- (error, response, body) => {
- if (error != null) {
- return callback(error)
- }
- callback(null)
- }
- )
- }
- getCsrfToken(callback) {
- this.request.get(
- {
- url: '/dev/csrf',
- },
- (err, response, body) => {
- if (err != null) {
- return callback(err)
- }
- this.csrfToken = body
- this.request = this.request.defaults({
- headers: {
- 'x-csrf-token': this.csrfToken,
- },
- })
- callback()
- }
- )
- }
- changePassword(newPassword, callback) {
- this.getCsrfToken(error => {
- if (error != null) {
- return callback(error)
- }
- this.request.post(
- {
- url: '/user/password/update',
- json: {
- currentPassword: this.password,
- newPassword1: newPassword,
- newPassword2: newPassword,
- },
- },
- err => {
- if (err) {
- return callback(err)
- }
- this.password = newPassword
- callback()
- }
- )
- })
- }
- reconfirmAccountRequest(userEmail, callback) {
- this.getCsrfToken(error => {
- if (error != null) {
- return callback(error)
- }
- this.request.post(
- {
- url: '/user/reconfirm',
- json: {
- email: userEmail,
- },
- },
- (error, response, body) => {
- callback(error, response)
- }
- )
- })
- }
- getUserSettingsPage(callback) {
- this.getCsrfToken(error => {
- if (error != null) {
- return callback(error)
- }
- this.request.get(
- {
- url: '/user/settings',
- },
- (error, response, body) => {
- if (error != null) {
- return callback(error)
- }
- callback(null, response.statusCode)
- }
- )
- })
- }
- updateSettings(newSettings, callback) {
- this.getCsrfToken(error => {
- if (error != null) {
- return callback(error)
- }
- this.request.post(
- {
- url: '/user/settings',
- json: newSettings,
- },
- callback
- )
- })
- }
- getProjectListPage(callback) {
- this.getCsrfToken(error => {
- if (error != null) {
- return callback(error)
- }
- this.request.get(
- {
- url: '/project',
- },
- (error, response, body) => {
- if (error != null) {
- return callback(error)
- }
- callback(null, response.statusCode)
- }
- )
- })
- }
- isLoggedIn(callback) {
- this.request.get('/user/personal_info', (error, response, body) => {
- if (error != null) {
- return callback(error)
- }
- if (response.statusCode === 200) {
- callback(null, true)
- } else if (response.statusCode === 302) {
- callback(null, false)
- } else {
- callback(
- new Error(
- `unexpected status code from /user/personal_info: ${response.statusCode}`
- )
- )
- }
- })
- }
- transferProjectOwnership(projectId, userId, callback) {
- this.getCsrfToken(err => {
- if (err != null) {
- return callback(err)
- }
- this.request.post(
- {
- url: `/project/${projectId.toString()}/transfer-ownership`,
- json: {
- user_id: userId.toString(),
- },
- },
- (err, response) => {
- if (err != null) {
- return callback(err)
- }
- if (response.statusCode !== 204) {
- return callback(
- new Error(`Unexpected status code: ${response.statusCode}`)
- )
- }
- callback()
- }
- )
- })
- }
- setV1Id(v1Id, callback) {
- UserModel.updateOne(
- {
- _id: this._id,
- },
- {
- overleaf: {
- id: v1Id,
- },
- },
- callback
- )
- }
- setCollaboratorInfo(projectId, userId, info, callback) {
- this.getCsrfToken(err => {
- if (err != null) {
- return callback(err)
- }
- this.request.put(
- {
- url: `/project/${projectId.toString()}/users/${userId.toString()}`,
- json: info,
- },
- (err, response) => {
- if (err != null) {
- return callback(err)
- }
- if (response.statusCode !== 204) {
- return callback(
- new Error(`Unexpected status code: ${response.statusCode}`)
- )
- }
- callback()
- }
- )
- })
- }
- }
- User.promises = class extends User {
- doRequest(method, params) {
- return new Promise((resolve, reject) => {
- this.request[method.toLowerCase()](params, (err, response, body) => {
- if (err) {
- reject(err)
- } else {
- resolve({ response, body })
- }
- })
- })
- }
- }
- // promisify User class methods - works for methods with 0-1 output parameters,
- // otherwise we will need to implement the method manually instead
- const nonPromiseMethods = ['constructor', 'setExtraAttributes']
- Object.getOwnPropertyNames(User.prototype).forEach(methodName => {
- const method = User.prototype[methodName]
- if (typeof method === 'function' && !nonPromiseMethods.includes(methodName)) {
- User.promises.prototype[methodName] = promisify(method)
- }
- })
- module.exports = User
|