• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

FieldDB / AuthenticationWebService / 20079488455

09 Dec 2025 09:44PM UTC coverage: 44.741% (-29.5%) from 74.284%
20079488455

Pull #133

github

web-flow
Merge 0879ec3ce into 8d2388c3d
Pull Request #133: Update nano

282 of 716 branches covered (39.39%)

Branch coverage included in aggregate %.

2 of 3 new or added lines in 1 file covered. (66.67%)

422 existing lines in 5 files now uncovered.

641 of 1347 relevant lines covered (47.59%)

3.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

36.24
/lib/user.js
1
const bcrypt = require('bcryptjs');
1✔
2
const debug = require('debug')('lib:user');
1✔
3
const config = require('config');
1✔
4
const nano = require('nano');
1✔
5
const md5 = require('md5');
1✔
6
const url = require('url');
1✔
7
const {
8
  Connection,
9
} = require('fielddb/api/corpus/Connection');
1✔
10
const { User } = require('fielddb/api/user/User');
1✔
11
const DEFAULT_USER_PREFERENCES = require('fielddb/api/user/preferences.json');
1✔
12

13
const authServerVersion = require('../package.json').version;
1✔
14
const emailFunctions = require('./email');
1✔
15
const corpus = require('./corpus');
1✔
16
const corpusmanagement = require('./corpusmanagement');
1✔
17

18
emailFunctions.emailWhenServerStarts();
1✔
19
/* variable for permissions */
20
const commenter = 'commenter';
1✔
21
const collaborator = 'reader';
1✔
22
const contributor = 'writer';
1✔
23
const {
24
  NODE_ENV,
25
} = process.env;
1✔
26
if (!Connection || !Connection.knownConnections || !Connection.knownConnections.production) {
1!
27
  throw new Error(`The app config for ${NODE_ENV} is missing app types to support. `);
×
28
}
29
/*
30
 * we are getting too many user signups from the Learn X users and the speech recognition trainer users,
31
 * they are starting to overpower the field linguist users. So if when we add the ability for them
32
 * to backup and share their languages lessions, then
33
 * we will create their dbs  with a non-anonymous username.
34
 */
35
const dontCreateDBsForLearnXUsersBecauseThereAreTooManyOfThem = true;
1✔
36

37
const parsed = url.parse(config.usersDbConnection.url);
1✔
38
const couchConnectUrl = `${parsed.protocol}//${config.couchKeys.username}:${config.couchKeys.password}@${parsed.host}`;
1✔
39

40
let sampleUsers = ['public'];
1✔
41
Object.keys(config.sampleUsers).forEach((userType) => {
1✔
42
  if (config.sampleUsers[userType].length > 0) {
3!
43
    sampleUsers = sampleUsers.concat(config.sampleUsers[userType]);
3✔
44
  }
45
});
46
debug(`${new Date()}  Sample users will not recieve save preferences changes.`, sampleUsers);
1✔
47

48
function undoCorpusCreation({
1✔
49
  user,
50
  connection,
51
  docs,
52
} = {}) {
53
  debug(`${new Date()} TODO need to clean up a broken corpus.${JSON.stringify(connection)}`, docs);
2✔
54
  return emailFunctions.emailCorusCreationFailure({ connection, user });
2✔
55
}
56

57
function sortByUsername(a, b) {
58
  if (a.username < b.username) {
1!
59
    return -1;
1✔
60
  }
UNCOV
61
  if (a.username > b.username) {
×
UNCOV
62
    return 1;
×
63
  }
64
  return 0;
×
65
}
66

67
function createCouchDBUser({
×
68
  req = {
×
69
    id: '',
70
    body: {},
71
    log: {
72
      // eslint-disable-next-line no-console
73
      warn: console.warn,
74
    },
75
  },
76
  user,
77
  password,
78
} = {}) {
79
  /*
80
   * Give the user access to other corpora so they can see what it is like
81
   * to collaborate
82
   */
UNCOV
83
  let whichUserGroup = 'normalusers';
×
UNCOV
84
  if (user.username.indexOf('test') > -1 || user.username.indexOf('anonymous') > -1) {
×
UNCOV
85
    whichUserGroup = 'betatesters';
×
86
  }
UNCOV
87
  debug('createCouchDBUser whichUserGroup', whichUserGroup);
×
UNCOV
88
  const couchdb = nano({
×
89
    headers: {
90
      'x-request-id': req.id || '',
×
91
    },
92
    url: couchConnectUrl,
93
  });
94

UNCOV
95
  const usersdb = couchdb.db.use('_users');
×
96

UNCOV
97
  const userid = `org.couchdb.user:${user.username}`;
×
UNCOV
98
  const userParamsForNewUser = {
×
99
    _id: userid,
100
    name: user.username,
101
    password,
102
    roles: [
103
      `${user.appbrand}_user`, // used to be connection.brandLowerCase
104
      'fielddbuser',
105
      `public-firstcorpus_${collaborator}`,
106
    // `${connection.dbname}_${admin}`,
107
      // `${connection.dbname}_${contributor}`,
108
      // `${connection.dbname}_${collaborator}`,
109
      // `${connection.dbname}_${commenter}`,
110
    ],
111
    type: 'user',
112
  };
113

UNCOV
114
  if (whichUserGroup === 'normalusers') {
×
115
    const sampleCorpus = 'lingllama-communitycorpus';
×
116
    userParamsForNewUser.roles.push(`${sampleCorpus}_${contributor}`);
×
117
    userParamsForNewUser.roles.push(`${sampleCorpus}_${collaborator}`);
×
118
    userParamsForNewUser.roles.push(`${sampleCorpus}_${commenter}`);
×
119
    debug('createCouchDBUser sampleCorpus', sampleCorpus);
×
120
  }
121

UNCOV
122
  userParamsForNewUser.roles = userParamsForNewUser.roles.sort();
×
UNCOV
123
  return usersdb.insert(userParamsForNewUser, userid)
×
UNCOV
124
    .then((couchUser) => ({
×
125
      // user,
126
      couchUser,
127
      whichUserGroup,
128
      // connection,
129
    }))
UNCOV
130
    .then(() => couchdb.db.replicate('new_corpus_activity_feed', `${user.username}-activity_feed`, {
×
131
      create_target: true,
132
    }))
133
    .catch((couchDBError) => {
134
      req.log.warn(`${new Date()} User activity feed replication failed.`, couchDBError);
×
135
    })
136
    .then((couchActivityFeedResponse) => {
UNCOV
137
      debug(`${new Date()} User activity feed successfully replicated.`, couchActivityFeedResponse);
×
138
      // Set up security on new user activity feed
UNCOV
139
      const activityDb = nano({
×
140
        headers: {
141
          'x-request-id': req.id || '',
×
142
        },
143
        url: couchConnectUrl,
144
      }).use(`${user.username}-activity_feed`);
UNCOV
145
      return activityDb.insert({
×
146
        admins: {
147
          names: [],
148
          roles: [
149
            'fielddbadmin',
150
          ],
151
        },
152
        members: {
153
          names: [
154
            user.username,
155
          ],
156
          roles: [],
157
        },
158
        _id: '_security',
159
      }, '_security');
160
    })
161
    .catch((couchDBError) => {
162
      req.log.warn(`${new Date()} User activity feed replication failed.`, couchDBError);
×
163
      return {
×
164
        whichUserGroup,
165
      };
166
    });
167
}
168

169
/*
170
 * Looks returns a list of users ordered by role in that corpus
171
 */
172
function fetchCorpusPermissions({
1✔
173
  req = {
1✔
174
    id: '',
175
    body: {},
176
  },
177
} = {}) {
178
  let dbConn;
179
  // If serverCode is present, request is coming from Spreadsheet app
180
  if (req.body.serverCode) {
7✔
181
    dbConn = Connection.defaultConnection(req.body.serverCode);
1✔
182
    dbConn.dbname = req.body.dbname;
1✔
183
  } else {
184
    dbConn = req.body.connection;
6✔
185
  }
186
  if (!req || !req.body.username || !req.body.username) {
7✔
187
    const err = new Error('Please provide a username, you must be a member of a corpus in order to find out who else is a member.');
1✔
188
    err.status = 412;
1✔
189
    return Promise.reject(err);
1✔
190
  }
191
  if (!dbConn) {
6!
192
    const err = new Error('Client didn\'t define the database connection.');
×
193
    err.status = 412;
×
194
    err.userFriendlyErrors = ['This app has made an invalid request. Please notify its developer. missing: serverCode or connection'];
×
195
    return Promise.reject(err);
×
196
  }
197
  const dbname = dbConn.dbname || dbConn.pouchname;
6!
198
  const requestingUser = req.body.username;
6✔
199
  let requestingUserIsAMemberOfCorpusTeam = false;
6✔
200
  if (dbConn && dbConn.domain && dbConn.domain.indexOf('iriscouch') > -1) {
6!
201
    dbConn.port = '6984';
×
202
  }
203
  debug(`${new Date()} ${requestingUser} requested the team on ${dbname}`);
6✔
204
  const nanoforpermissions = nano({
6✔
205
    headers: {
206
      'x-request-id': req.id || '',
6!
207
    },
208
    url: couchConnectUrl,
209
  });
210
  /*
211
   * Get user names and roles from the server
212
   *
213
   * https://127.0.0.1:6984/_users/_design/users/_view/userroles
214
   */
215
  const usersdb = nanoforpermissions.db.use('_users');
6✔
216
  let whichUserGroup = 'normalusers';
6✔
217
  if (requestingUser.indexOf('test') > -1 || requestingUser.indexOf('anonymous') > -1) {
6✔
218
    whichUserGroup = 'betatesters';
3✔
219
  }
220
  let userroles;
221
  return usersdb.view('users', whichUserGroup)
6✔
222
    .then((body) => {
UNCOV
223
      userroles = body.rows;
×
224
      /*
225
     * Get user masks from the server
226
     */
UNCOV
227
      const fieldDBUsersDB = nanoforpermissions.db.use(config.usersDbConnection.dbname);
×
228
      // Put the user in the database and callback
UNCOV
229
      return fieldDBUsersDB.view('users', 'usermasks');
×
230
    })
231
    .then((body) => {
UNCOV
232
      const usermasks = body.rows;
×
UNCOV
233
      const usersInThisGroup = {};
×
UNCOV
234
      const rolesAndUsers = {
×
235
        allusers: [],
236
        notonteam: [],
237
      };
238
      /*
239
       Convert the array into a hash to avoid n*m behavior (instead we have n+m+h)
240
       */
UNCOV
241
      userroles.forEach(({ key: username, value: roles }) => {
×
UNCOV
242
        usersInThisGroup[username] = { username, roles };
×
243
      });
244
      // Put the gravatars in for users who are in this category
UNCOV
245
      usermasks.forEach(({ value: { gravatar, gravatar_email: email, username } }) => {
×
246
        // if this usermask isnt in this category of users, skip them.
UNCOV
247
        if (!usersInThisGroup[username]) {
×
UNCOV
248
          return;
×
249
        }
250
        // debug(username, usersInThisGroup[username]);
UNCOV
251
        usersInThisGroup[username].gravatar = gravatar;
×
252
        // debug(new Date() + "  the value of this user ", usermasks[userIndex]);
UNCOV
253
        usersInThisGroup[username].email = email;
×
254
      });
255
      // Put the users into the list of roles and users
UNCOV
256
      Object.keys(usersInThisGroup).forEach((username) => {
×
UNCOV
257
        if (!usersInThisGroup[username]) {
×
258
          return;
×
259
        }
260
        // debug(new Date() + " Looking at " + username);
UNCOV
261
        let userIsOnTeam = false;
×
UNCOV
262
        const thisUsersMask = usersInThisGroup[username];
×
UNCOV
263
        if ((!thisUsersMask.gravatar || thisUsersMask.gravatar.indexOf('user_gravatar') > -1) && thisUsersMask.email) {
×
264
          debug(`${new Date()}  the gravtar of ${username} was missing/old `, thisUsersMask);
×
265
          thisUsersMask.gravatar = md5(thisUsersMask.email);
×
266
        }
267
        // Find out if this user is a member of the team
UNCOV
268
        if (!thisUsersMask.roles) {
×
269
          debug(`${new Date()} this is odd, ${username} doesnt have any roles defined, skipping this user, even for hte typeahead`);
×
270
          return;
×
271
        }
272
        // Add this user to the typeahead
UNCOV
273
        rolesAndUsers.allusers.push({
×
274
          username,
275
          gravatar: thisUsersMask.gravatar,
276
        });
277

UNCOV
278
        thisUsersMask.roles.forEach((role) => {
×
UNCOV
279
          if (role.indexOf(`${dbname}_`) !== 0) {
×
UNCOV
280
            return;
×
281
          }
UNCOV
282
          userIsOnTeam = true;
×
283
          // debug(new Date() + username + " is a member of this corpus: " + role);
UNCOV
284
          if (username === requestingUser) {
×
UNCOV
285
            requestingUserIsAMemberOfCorpusTeam = true;
×
286
          }
287
          /*
288
           * If the role is for this corpus, insert the users's mask into
289
           * the relevant roles, this permits the creation of new roles in the system
290
           */
UNCOV
291
          const roleType = `${role.replace(`${dbname}_`, '')}s`;
×
UNCOV
292
          rolesAndUsers[roleType] = rolesAndUsers[roleType] || [];
×
UNCOV
293
          rolesAndUsers[roleType].push({
×
294
            username,
295
            gravatar: thisUsersMask.gravatar,
296
          });
297
        });
UNCOV
298
        if (!userIsOnTeam) {
×
UNCOV
299
          rolesAndUsers.notonteam.push({
×
300
            username,
301
            gravatar: thisUsersMask.gravatar,
302
          });
303
        }
304
      });
305

306
      /* sort alphabetically the real roles (typeaheads dont matter) */
UNCOV
307
      if (rolesAndUsers.admins) {
×
UNCOV
308
        rolesAndUsers.admins.sort(sortByUsername);
×
309
      }
UNCOV
310
      if (rolesAndUsers.writers) {
×
UNCOV
311
        rolesAndUsers.writers.sort(sortByUsername);
×
312
      }
UNCOV
313
      if (rolesAndUsers.readers) {
×
UNCOV
314
        rolesAndUsers.readers.sort(sortByUsername);
×
315
      }
UNCOV
316
      if (rolesAndUsers.commenters) {
×
UNCOV
317
        rolesAndUsers.commenters.sort(sortByUsername);
×
318
      }
319
      /*
320
       * Send the results, if the user is part of the team
321
       */
UNCOV
322
      if (requestingUserIsAMemberOfCorpusTeam) {
×
UNCOV
323
        return {
×
324
          rolesAndUsers,
325
          info: {
326
            message: 'Look up successful.',
327
          },
328
        };
329
      }
330

UNCOV
331
      debug(`Requesting user \`${requestingUser}\` is not a member of the corpus team.${dbname}`);
×
UNCOV
332
      const err = new Error(`Requesting user \`${requestingUser}\` is not a member of the corpus team.`);
×
UNCOV
333
      err.status = 401;
×
UNCOV
334
      err.userFriendlyErrors = ['Unauthorized, you are not a member of this corpus team.'];
×
UNCOV
335
      throw err;
×
336
    // });
337
    })
338
    .catch((error) => {
339
      debug(`${new Date()} Error quering userroles: ${error}`);
6✔
340
      const err = {
6✔
341
        message: error.message,
342
        userFriendlyErrors: ['Server is not responding for request to query corpus permissions. Please report this 1289'],
343
        ...error,
344
      };
345
      throw err;
6✔
346
    });
347
}
348

349
function createNewCorpusesIfDontExist({
1✔
350
  user,
351
  corpora,
352
  req,
353
} = {}) {
354
  if (!corpora || corpora.length === 0) {
3✔
355
    return Promise.resolve([]);
1✔
356
  }
357

358
  const requestedDBCreation = {};
2✔
359
  debug(`${new Date()} Ensuring newCorpora are ready`, corpora);
2✔
360
  /*
361
   * Creates the user's new corpus databases
362
   */
363
  return Promise.all(corpora.map((potentialnewcorpusconnection) => {
2✔
364
    if (!potentialnewcorpusconnection || !potentialnewcorpusconnection.dbname
4!
365
       || requestedDBCreation[potentialnewcorpusconnection.dbname]) {
UNCOV
366
      debug('Not creating this corpus ', potentialnewcorpusconnection);
×
UNCOV
367
      return Promise.resolve(potentialnewcorpusconnection);
×
368
    }
369

370
    if (potentialnewcorpusconnection.dbname.indexOf(`${user.username}-`) !== 0) {
4✔
371
      debug('Not creating a corpus which appears to belong ot another user.', potentialnewcorpusconnection);
2✔
372
      return Promise.resolve(potentialnewcorpusconnection);
2✔
373
    }
374
    requestedDBCreation[potentialnewcorpusconnection.dbname] = true;
2✔
375

376
    return corpus.createNewCorpus({
2✔
377
      req,
378
      user,
379
      title: potentialnewcorpusconnection.title,
380
      connection: potentialnewcorpusconnection,
381
    })
382
      .then(({ corpusDetails, info } = {}) => {
×
UNCOV
383
        debug('Create new corpus results', corpusDetails.description, info);
×
384
        // if (err.status === 302) {
385
        //   for (var connectionIndex = corpora.length - 1; connectionIndex >= 0; connectionIndex--) {
386
        //     if (info.message === "Your corpus " + corpora[connectionIndex].dbname +
387
        // " already exists, no need to create it.") {
388
        //       debug("Removing this from the new connections  has no effect." + info.message);
389
        //       corpora.splice(connectionIndex, 1);
390
        //     }
391
        //   }
392
        // }
UNCOV
393
        return potentialnewcorpusconnection;
×
394
      })
395
      .catch((err) => {
396
        if (err.corpusexisted) {
2!
UNCOV
397
          return potentialnewcorpusconnection;
×
398
        }
399
        throw err;
2✔
400
      });
401
  }));
402
}
403

404
/**
405
function is called back
406
 * with (error, user, info) where error contains the server's detailed error
407
 * (not to be shared with the client), and info contains a client readible error
408
 * message.
409
 *
410
 * @param user
411
 */
412
function saveUpdateUserToDatabase({
1✔
413
  user: fieldDBUser,
414
  req = {
1✔
415
    id: '',
416
    log: {
417
      // eslint-disable-next-line no-console
418
      error: console.error,
419
      // eslint-disable-next-line no-console
420
      warn: console.warn,
421
    },
422
  },
423
} = {}) {
424
  if (!fieldDBUser || !fieldDBUser.username) {
3✔
425
    return Promise.reject(new Error('Please provide a username 8933'));
1✔
426
  }
427

428
  if (process.env.INSTALABLE !== 'true' && sampleUsers.indexOf(fieldDBUser.username) > -1) {
2✔
429
    return Promise.resolve({
1✔
430
      user: fieldDBUser,
431
      info: {
432
        message: 'User is a reserved user and cannot be updated in this manner.',
433
      },
434
    });
435
  }
436
  const user = fieldDBUser.toJSON ? fieldDBUser.toJSON() : fieldDBUser;
1!
437
  // dont need the salt
438
  delete user.salt;
1✔
439

440
  // Preparing the couch connection
441
  const usersdb = nano({
1✔
442
    headers: {
443
      'x-request-id': req.id || '',
1!
444
    },
445
    url: couchConnectUrl,
446
  }).db.use(config.usersDbConnection.dbname);
447
  // Put the user in the database and callback
448
  return usersdb.insert(user, user.username)
1✔
449
    .then((resultuser) => {
UNCOV
450
      if (resultuser.ok) {
×
UNCOV
451
        debug(`${new Date()} No error saving a user: ${JSON.stringify(resultuser)}`);
×
452
        // eslint-disable-next-line no-underscore-dangle, no-param-reassign
UNCOV
453
        fieldDBUser._rev = resultuser.rev;
×
UNCOV
454
        return {
×
455
          user: fieldDBUser,
456
          info: {
457
            message: 'User details saved.',
458
          },
459
        };
460
      }
461
      throw new Error('Unknown server result, this might be a bug.');
×
462
    })
463
    .catch((error) => {
464
      const err = {
1✔
465
        message: error.message,
466
        status: error.status || error.statusCode,
2✔
467
        ...error,
468
      };
469
      let message = 'Error saving a user in the database. ';
1✔
470
      if (err.status === 409) {
1!
UNCOV
471
        message = 'Conflict saving user in the database. Please try again.';
×
472
      }
473
      debug(`${new Date()} Error saving a user: ${JSON.stringify(error)}`);
1✔
474
      debug(`${new Date()} This is the user who was not saved: ${JSON.stringify(user)}`);
1✔
475
      err.userFriendlyErrors = [message];
1✔
476
      throw err;
1✔
477
    });
478
}
479

480
/**
481
 * connects to the usersdb, tries to retrieve the doc with the
482
 * provided id
483
 *
484
 * @param id
485
 */
486
function findByUsername({
1✔
487
  username,
488
  req = {
2✔
489
    id: '',
490
    log: {
491
      // eslint-disable-next-line no-console
492
      error: console.error,
493
      // eslint-disable-next-line no-console
494
      warn: console.warn,
495
    },
496
  },
497
} = {}) {
498
  if (!username) {
72✔
499
    return Promise.reject(new Error('username is required'));
2✔
500
  }
501

502
  const usersdb = nano({
70✔
503
    headers: {
504
      'x-request-id': req.id || '',
70!
505
    },
506
    url: couchConnectUrl,
507
  }).db.use(config.usersDbConnection.dbname);
508
  return usersdb.get(username)
70✔
509
    .then((result) => {
UNCOV
510
      debug(`${new Date()} User found: ${result.username}`);
×
UNCOV
511
      if (result.serverlogs && result.serverlogs.disabled) {
×
UNCOV
512
        const err = new Error(`User ${username} has been disabled, probably because of a violation of the terms of service. ${result.serverlogs.disabled}`);
×
UNCOV
513
        err.status = 401;
×
UNCOV
514
        err.userFriendlyErrors = [`This username has been disabled. Please contact us at support@lingsync.org if you would like to reactivate this username. Reasons: ${result.serverlogs.disabled}`];
×
UNCOV
515
        throw err;
×
516
      }
517

UNCOV
518
      const user = new User(result);
×
519
      // const user = {
520
      //   ...result,
521
      // };
522
      // user.corpora = user.corpora || user.corpuses || [];
UNCOV
523
      if (result.corpuses) {
×
524
        debug(` Upgrading ${result.username} data structure to v3.0`);
×
525
        // delete user.corpuses;
526
      }
527

UNCOV
528
      return { user };
×
529
    })
530
    .catch((error) => {
531
      if (error.error === 'not_found') {
70!
UNCOV
532
        debug(error, `${new Date()} No User found: ${username}`);
×
UNCOV
533
        const err = new Error(`User ${username} does not exist`);
×
UNCOV
534
        err.status = 404;
×
535
        // err.userFriendlyErrors = ['Username or password is invalid. Please try again.'];
UNCOV
536
        throw err;
×
537
      }
538

539
      if (error.error === 'unauthorized') {
70!
540
        req.log.error(error, `${new Date()} Wrong admin username and password`);
×
541
        throw new Error('Server is mis-configured. Please report this error 8914.');
×
542
      }
543

544
      throw error;
70✔
545
    });
546
}
547

548
/**
549
 * uses tries to look up users by email
550
 * if optionallyRestrictToIncorrectLoginAttempts
551
 *  then it will look up users who might be calling forgotPassword
552
 */
553
function findByEmail({
1✔
554
  email,
555
  optionallyRestrictToIncorrectLoginAttempts,
556
  req = {
1✔
557
    id: '',
558
    log: {
559
      // eslint-disable-next-line no-console
560
      error: console.error,
561
      // eslint-disable-next-line no-console
562
      warn: console.warn,
563
    },
564
  },
565
} = {}) {
566
  if (!email) {
4✔
567
    return Promise.reject(new Error('Please provide an email'));
1✔
568
  }
569
  let usersQuery = 'usersByEmail';
3✔
570
  if (optionallyRestrictToIncorrectLoginAttempts) {
3✔
571
    usersQuery = 'userWhoHaveTroubleLoggingIn';
2✔
572
  }
573
  usersQuery = `${usersQuery}?key="${email}"`;
3✔
574
  const usersdb = nano({
3✔
575
    headers: {
576
      'x-request-id': req.id || '',
3!
577
    },
578
    url: couchConnectUrl,
579
  }).db.use(config.usersDbConnection.dbname);
580
  // Query the database and callback with matching users
581
  return usersdb.view('users', usersQuery)
3✔
582
    .then((body) => {
UNCOV
583
      debug(`${new Date()} ${usersQuery} requested users who have this email ${email} from the server, and recieved results `);
×
UNCOV
584
      const users = body.rows.map((row) => row.value);
×
UNCOV
585
      debug(`${new Date()} users ${JSON.stringify(users)}`);
×
UNCOV
586
      if (users.length === 0) {
×
UNCOV
587
        const err = new Error(`No matching users for ${optionallyRestrictToIncorrectLoginAttempts}`);
×
UNCOV
588
        err.status = 401;
×
UNCOV
589
        err.userFriendlyErrors = [`Sorry, there are no users who have failed to login who have the email you provided ${email}. You cannot request a temporary password until you have at least tried to login once with your correct username. If you are not able to guess your username please contact us for assistance.`];
×
UNCOV
590
        throw err;
×
591
      }
UNCOV
592
      return ({
×
593
        users,
594
        info: {
595
          message: `Found ${users.length} users for ${optionallyRestrictToIncorrectLoginAttempts}`,
596
        },
597
      });
598
    })
599
    .catch((error) => {
600
      debug(`${new Date()} Error in findByEmail while quering ${usersQuery} ${JSON.stringify(error)}`);
3✔
601
      // const err = {
602
      //   message: error.message,
603
      //   status: error.status || error.statusCode,
604
      //   userFriendlyErrors: ['Server is not responding to request. Please report this 1609'],
605
      //   ...error,
606
      // };
607
      // error.userFriendlyErrors = error.userFriendlyErrors ||
608
      // ['Server is not responding to request. Please report this 1609'];
609
      throw error;
3✔
610
    });
611
}
612

613
function addCorpusToUser({
1✔
614
  username,
615
  newConnection,
616
  userPermissionResult,
617
  req,
618
} = {}) {
619
  return findByUsername({ username, req })
2✔
620
    .then(({ user, info }) => {
UNCOV
621
      debug('Find by username ', info);
×
622

UNCOV
623
      let shouldEmailWelcomeToCorpusToUser = false;
×
624
      // eslint-disable-next-line no-param-reassign
UNCOV
625
      user.serverlogs = user.serverlogs || {};
×
626
      // eslint-disable-next-line no-param-reassign
UNCOV
627
      user.serverlogs.welcomeToCorpusEmails = user.serverlogs.welcomeToCorpusEmails || {};
×
UNCOV
628
      if (!userPermissionResult.after) {
×
629
        const err = new Error('userPermissionResult.after was undefined');
×
630
        err.userFriendlyErrors = ['The server has errored please report this. 724'];
×
631
        throw err;
×
632
      }
633

UNCOV
634
      if (userPermissionResult.after.length > 0 && !user.serverlogs.welcomeToCorpusEmails[newConnection.dbname]) {
×
635
        shouldEmailWelcomeToCorpusToUser = true;
×
636
        // eslint-disable-next-line no-param-reassign
637
        user.serverlogs.welcomeToCorpusEmails[newConnection.dbname] = [Date.now()];
×
638
      }
639
      /*
640
       * If corpus is already there
641
       */
UNCOV
642
      debug(`${new Date()} Here are the user ${user.username}'s known corpora ${JSON.stringify(user.corpora)}`);
×
643
      let alreadyAdded;
UNCOV
644
      user.corpora.forEach((connection) => {
×
UNCOV
645
        if (userPermissionResult.after.length === 0) {
×
646
          // removes from all servers, TODO this might be something we should ask the user about.
UNCOV
647
          if (connection.dbname === newConnection.dbname) {
×
648
            // console.log('User', user.clone)
649
            user.corpora.remove(connection, 1);
×
650
          }
UNCOV
651
          return;
×
652
        }
UNCOV
653
        if (alreadyAdded) {
×
UNCOV
654
          return;
×
655
        }
UNCOV
656
        if (connection.dbname === newConnection.dbname) {
×
UNCOV
657
          alreadyAdded = true;
×
658
        }
659
      });
UNCOV
660
      debug(`${user.username}, after ${JSON.stringify(userPermissionResult)}`);
×
UNCOV
661
      if (userPermissionResult.after.length > 0) {
×
UNCOV
662
        if (alreadyAdded) {
×
UNCOV
663
          const message = `User ${user.username} now has ${
×
664
            userPermissionResult.after.join(' ')} access to ${
665
            newConnection.dbname}, the user was already a member of this corpus team.`;
UNCOV
666
          return Promise.resolve({
×
667
            userPermissionResult: {
668
              status: 200,
669
              message,
670
              ...userPermissionResult,
671
            },
672
            info: {
673
              message,
674
            },
675
          });
676
        }
677
        /*
678
         * Add the new db connection to the user, save them and send them an
679
         * email telling them they they have access
680
         */
681
        // user.corpora = user.corpora || [];
682
        user.corpora.add(newConnection);
×
683
      } else {
684
        // console.log('corpora before', user);
685
        // user.corpora = user.corpora.filter(({ dbname }) => (dbname !== newConnection.dbname));
686
        // console.log('corpora after', user.corpora);
UNCOV
687
        debug('after removed new corpus from user ', newConnection, user.corpora);
×
688
      }
UNCOV
689
      debug(`${new Date()} Here are the user ${user.username}'s after corpora ${JSON.stringify(user.corpora)}`);
×
690

691
      // return done({
692
      //   error: "todo",
693
      //   status: 412
694
      // }, [userPermissionResult, user.corpora], {
695
      //   message: "TODO. save modifying the list of corpora in the user "
696
      // });
UNCOV
697
      return saveUpdateUserToDatabase({ user, req })
×
698
        .then(() => {
699
          // console.log('user after save', userPermissionResult);
700
          // If the user was removed we can exit now
UNCOV
701
          if (userPermissionResult.after.length === 0) {
×
UNCOV
702
            const message = `User ${user.username} was removed from the ${
×
703
              newConnection.dbname
704
            } team.`;
UNCOV
705
            return {
×
706
              userPermissionResult: {
707
                status: 200,
708
                message,
709
                ...userPermissionResult,
710
              },
711
              info: {
712
                message,
713
              },
714
            };
715
          }
716
          const message = `User ${user.username} now has ${
×
717
            userPermissionResult.after.join(' ')} access to ${
718
            newConnection.dbname}`;
719
          // send the user an email to welcome to this corpus team
720
          if (shouldEmailWelcomeToCorpusToUser) {
×
721
            emailFunctions.emailWelcomeToCorpus({
×
722
              user,
723
              newConnection,
724
            });
725
          }
726
          return {
×
727
            userPermissionResult: {
728
              status: 200,
729
              message,
730
              ...userPermissionResult,
731
            },
732
            info: {
733
              message,
734
            },
735
          };
736
        })
737
        .catch((error) => {
738
          debug('error saving user ', error);
×
739
          const err = {
×
740
            message: error.message,
741
            status: error.status || error.statusCode || 505,
×
742
            userFriendlyErrors: [`User ${user.username} now has ${
743
              userPermissionResult.after.join(' ')} access to ${
744
              newConnection.dbname}, but we weren't able to add this corpus to their account. This is most likely a bug, please report it.`],
745
            ...error,
746
          };
747

748
          throw err;
×
749
        });
750
    })
751
    .catch((error) => {
752
      debug('error looking up user ', error);
2✔
753
      const err = {
2✔
754
        message: error.message,
755
        status: error.status || error.statusCode,
4✔
756
        userFriendlyErrors: ['Username doesnt exist on this server. This is a bug.'],
757
        ...error,
758
      };
759

760
      throw err;
2✔
761

762
    // if (!user) {
763
    //   // This case is a server error, it should not happen.
764
    //   userPermissionResult.status = error.status;
765
    //   userPermissionResult.message = 'Server was unable to process you request. Please report this: 1292';
766
    //   return done({
767
    //     status: 500,
768
    //     error: 'There was no error from couch, but also no user.',
769
    //   }, false, {
770
    //     message: userPermissionResult.message,
771
    //   });
772
    // }
773
    });
774
}
775
const normalizeRolesToDBname = function normalizeRolesToDBname({ dbname, role }) {
1✔
776
  // if (!role) {
777
  //   return;
778
  // }
UNCOV
779
  if (role && role.indexOf('_') > -1) {
×
780
    return `${dbname}_${role.substring(role.lastIndexOf('_') + 1)}`;
×
781
  }
UNCOV
782
  return `${dbname}_${role}`;
×
783
};
784
/*
785
 * Ensures the requesting user to make the permissions
786
 * modificaitons. Then adds the role to the user if they exist
787
 */
788
function addRoleToUser({
1✔
789
  req = {
1✔
790
    body: {},
791
    log: {
792
      // eslint-disable-next-line no-console
793
      error: console.error,
794
      // eslint-disable-next-line no-console
795
      warn: console.warn,
796
    },
797
  },
798
} = {}) {
799
  let dbConn = {};
2✔
800
  // If serverCode is present, request is coming from Spreadsheet app
801
  dbConn = req.body.connection;
2✔
802
  if (!dbConn || !dbConn.dbname || dbConn.dbname.indexOf('-') === -1) {
2✔
803
    debug(dbConn);
1✔
804
    const err = new Error('Client didnt define the dbname to modify.');
1✔
805
    err.status = 412;
1✔
806
    err.userFriendlyErrors = ['This app has made an invalid request. Please notify its developer. missing: corpusidentifier'];
1✔
807
    return Promise.reject(err);
1✔
808
  }
809
  if (!req || !req.body.username || !req.body.username) {
1!
810
    const err = new Error('Please provide a username');
×
811
    err.status = 412;
×
812
    return Promise.reject(err);
×
813
  }
814
  if (!req || !req.body.users || req.body.users.length === 0 || !req.body.users[0].username) {
1!
815
    const err = new Error('Client didnt define the username to modify.');
×
816
    err.status = 412;
×
817
    err.userFriendlyErrors = ['This app has made an invalid request. Please notify its developer. missing: user(s) to modify'];
×
818
    return Promise.reject(err);
×
819
  }
820
  if ((!req.body.users[0].add || req.body.users[0].add.length < 1)
1!
821
    && (!req.body.users[0].remove || req.body.users[0].remove.length < 1)) {
UNCOV
822
    const err = new Error('Client didnt define the roles to add nor remove');
×
UNCOV
823
    err.status = 412;
×
UNCOV
824
    err.userFriendlyErrors = ['This app has made an invalid request. Please notify its developer. missing: roles to add or remove'];
×
UNCOV
825
    return Promise.reject(err);
×
826
  }
827

828
  return corpus.isRequestingUserAnAdminOnCorpus({
1✔
829
    connection: dbConn,
830
    req,
831
    username: req.body.username,
832
  }).then((result) => {
UNCOV
833
    debug(`${new Date()} User ${req.body.username} is admin and can modify permissions on ${dbConn.dbname}`, result);
×
834
    // convert roles into corpus specific roles
UNCOV
835
    const promises = req.body.users.map((userPermissionOriginal) => {
×
UNCOV
836
      const userPermission = {
×
837
        ...userPermissionOriginal,
838
      };
UNCOV
839
      if (userPermission.add && typeof userPermission.add.map === 'function') {
×
UNCOV
840
        userPermission.add = userPermission.add
×
841
          // TODO unify with corpus.addRoleToUserInfo
UNCOV
842
          .map((role) => normalizeRolesToDBname({ dbname: dbConn.dbname, role }));
×
843
      } else {
UNCOV
844
        userPermission.add = [];
×
845
      }
846

UNCOV
847
      if (userPermission.remove && typeof userPermission.remove.map === 'function') {
×
UNCOV
848
        userPermission.remove = userPermission.remove
×
UNCOV
849
          .map((role) => normalizeRolesToDBname({ dbname: dbConn.dbname, role }));
×
850
      } else {
851
        userPermission.remove = [];
×
852
      }
UNCOV
853
      debug('userPermission', userPermission);
×
UNCOV
854
      return userPermission;
×
855
    })
856
      /*
857
       * If they are admin, add the role to the user, then add the corpus to user if succesfull
858
       */
859
      .map((userPermission) => {
UNCOV
860
        debug('userPermission', userPermission);
×
UNCOV
861
        return corpus.addRoleToUserInfo({ connection: dbConn, userPermission })
×
862
          .then(({ userPermissionResult }) => {
UNCOV
863
            debug('corpus.addRoleToUserInfo result', userPermissionResult);
×
UNCOV
864
            return addCorpusToUser({
×
865
              username: userPermission.username, userPermissionResult, newConnection: dbConn, req,
866
            });
867
          })
UNCOV
868
          .then(({ userPermissionResult: userPermissionFinalResult }) => userPermissionFinalResult)
×
869
          .catch((error) => {
UNCOV
870
            const err = {
×
871
              message: error.message,
872
              status: error.status || error.statusCode || 412,
×
873
              userFriendlyErrors: [`Unable to add ${userPermission.username} to this corpus.${error.statusCode === 404 ? ' User not found.' : ''}`],
×
874
              ...error,
875
            };
UNCOV
876
            throw err;
×
877
          });
878
      });
879

UNCOV
880
    return Promise.all(promises);
×
881
    // .then((results) => {
882
    //   let thereWasAnError;
883
    //   debug(`${new Date()} recieved promises back ${results.length}`);
884
    //   // results.forEach((userPermis) => {
885
    //   //   if (userPermis && userPermis.status !== 200 && !thereWasAnError) {
886
    //   //     thereWasAnError = new Error(`One or more of the add roles requsts failed. ${userPermis.message}`);
887
    //   //     debug('one errored', thereWasAnError);
888
    //   //   }
889
    //   // });
890

891
    //   return results;
892

893
    //   // if (!thereWasAnError) {
894
    //   //   console.log('reeturning the results', results)
895
    //   //   return results;
896
    //   // } else {
897
    //   //   throw thereWasAnError;
898
    //   // }
899
    // });
900
    // .catch(reject);
901
    // .fail(function(error) {
902
    //       debug(" returning fail.");
903
    //       error.status = cleanErrorStatus(error.status) || req.body.users[0].status || 500;
904
    //       req.body.users[0].message = req.body.users[0].message ||
905
    //  " There was a problem processing your request. Please notify us of this error 320343";
906
    //       return done(error, req.body.users);
907
    //     });
908
  })
909
    .catch((err) => {
910
      debug(err, 'error isRequestingUserAnAdminOnCorpus');
1✔
911
      throw err;
1✔
912
    });
913
  // });
914
}
915

916
function verifyPassword({
1✔
917
  password,
918
  user,
919
} = {}) {
920
  return new Promise((resolve, reject) => {
3✔
921
    setTimeout(() => {
3✔
922
      try {
3✔
923
        /*
924
         * If the user didnt furnish a password, set a fake one. It will return
925
         * unauthorized.
926
         */
927
        bcrypt.compare(password || ' ', user.hash)
3✔
928
          .then((matches) => {
929
            if (matches) {
2✔
930
              return resolve({ user });
1✔
931
            }
932

933
            const err = new Error('Username or password is invalid. Please try again.');
1✔
934
            err.status = 401;
1✔
935
            return reject(err);
1✔
936
          })
937
          .catch(reject);
938
      } catch (err) {
939
        reject(err);
1✔
940
      }
941
    });
942
  });
943
}
944

945
/**
946
 * accepts an old and new password, a user and a function to be
947
 * called with (error, user, info)
948
 * TODO rename to changePassword
949
 *
950
 * @param oldpassword
951
 * @param newpassword
952
 * @param username
953
 */
954
function setPassword({
1✔
955
  oldpassword,
956
  newpassword,
957
  username,
958
  req,
959
} = {}) {
960
  if (!username) {
4✔
961
    const err = new Error('Please provide a username');
1✔
962
    err.status = 412;
1✔
963
    return Promise.reject(err);
1✔
964
  }
965
  if (!oldpassword) {
3!
966
    const err = new Error('Please provide your old password');
×
967
    err.status = 412;
×
968
    return Promise.reject(err);
×
969
  }
970
  if (!newpassword) {
3✔
971
    const err = new Error('Please provide your new password');
1✔
972
    err.status = 412;
1✔
973
    return Promise.reject(err);
1✔
974
  }
975
  const safeUsernameForCouchDB = Connection.validateUsername(username);
2✔
976
  if (username !== safeUsernameForCouchDB.identifier) {
2!
977
    const err = new Error('Username or password is invalid. Please try again.');
×
978
    err.status = 412;
×
979
    return Promise.reject(err);
×
980
  }
981

982
  return findByUsername({ username, req })
2✔
983
    .then(({ user }) => {
UNCOV
984
      debug(`${new Date()} Found user in setPassword: ${JSON.stringify(user)}`);
×
985

UNCOV
986
      return verifyPassword({
×
987
        user,
988
        password: oldpassword,
989
      });
990
    })
991
    // Save new password to couch too
UNCOV
992
    .then(({ user }) => corpusmanagement.changeUsersPassword({
×
993
      user,
994
      newpassword,
995
    })
996
      .catch((err) => {
997
        debug(`${new Date()} There was an error in creating changing the couchdb password ${JSON.stringify(err)}\n`);
×
998
        throw err;
×
999
      })
1000
      .then((res) => {
1001
        // dont save this if the email to change the password failed
1002
        // const salt = bcrypt.genSaltSync(10);
1003
        // user.hash = bcrypt.hashSync(newpassword, salt);
UNCOV
1004
        debug(`${new Date()} There was success in creating changing the couchdb password: ${JSON.stringify(res)}\n`);
×
UNCOV
1005
        return saveUpdateUserToDatabase({ user, req });
×
1006
      }))
1007
    .then(({ user, info }) => {
1008
      // Change the success message to be more appropriate
UNCOV
1009
      if (info.message === 'User details saved.') {
×
1010
        // eslint-disable-next-line no-param-reassign
UNCOV
1011
        info.message = 'Your password has succesfully been updated.';
×
1012
      }
UNCOV
1013
      return { user, info };
×
1014
    })
1015
    .catch((err) => {
1016
      debug(`${new Date()} Error setPassword  ${username} : ${JSON.stringify(err.stack)}`);
2✔
1017

1018
      throw err;
2✔
1019
    });
1020
}
1021

1022
/**
1023
 * accepts an email, finds associated users who have had incorrect login
1024
 *
1025
 *
1026
 * @param email
1027
 */
1028
function forgotPassword({
1✔
1029
  email,
1030
  req,
1031
} = {}) {
1032
  if (!email) {
4✔
1033
    const err = new Error('Please provide an email.');
2✔
1034
    err.status = 412;
2✔
1035
    return Promise.reject(err);
2✔
1036
  }
1037
  return findByEmail({
2✔
1038
    email,
1039
    optionallyRestrictToIncorrectLoginAttempts: 'onlyUsersWithIncorrectLogins',
1040
    req,
1041
  })
1042
    .then(({ users }) => {
UNCOV
1043
      const sameTempPasswordForAllTheirUsers = emailFunctions.makeRandomPassword();
×
UNCOV
1044
      const promises = users.map((userToReset) => emailFunctions.emailTemporaryPasswordToTheUserIfTheyHavAnEmail({
×
1045
        user: userToReset,
1046
        temporaryPassword: sameTempPasswordForAllTheirUsers,
1047
        successMessage: `A temporary password has been sent to your email ${email}`,
1048
      }).then(({ user }) => {
1049
        debug(`${new Date()} Saving new hash to the user ${user.username} after setting it to a temp password.`);
×
1050
        return saveUpdateUserToDatabase({ user, req });
×
1051
      }));
UNCOV
1052
      let passwordChangeResults = '';
×
UNCOV
1053
      const forgotPasswordResults = {
×
1054
        status_codes: '',
1055
        error: {
1056
          status: 200,
1057
          error: '',
1058
        },
1059
        info: {
1060
          message: '',
1061
        },
1062
      };
1063

UNCOV
1064
      return Promise.all(promises).then((results) => {
×
1065
        debug(`${new Date()} recieved promises back ${results.length}`);
×
1066
        results.forEach((result) => {
×
1067
          if (result.state === 'fulfilled') {
×
1068
            const { value } = result;
×
1069
            if (value.source && value.source.exception) {
×
1070
              passwordChangeResults = `${passwordChangeResults} ${value.source.exception}`;
×
1071
            } else {
1072
              passwordChangeResults = `${passwordChangeResults} Success`;
×
1073
            }
1074
          } else {
1075
          // not fulfilled,happens rarely
1076
            const { reason } = result;
×
1077
            passwordChangeResults = `${passwordChangeResults} ${reason}`;
×
1078
          }
1079
        });
1080
        debug(`${new Date()} passwordChangeResults ${passwordChangeResults}`);
×
1081
        results.forEach((result) => {
×
1082
          debug('passwordChangeResults results', result);
×
1083
          if (result.error) {
×
1084
            forgotPasswordResults.status_codes = `${forgotPasswordResults.status_codes} ${result.error.status}`;
×
1085
            if (result.error.status > forgotPasswordResults.error.status) {
×
1086
              forgotPasswordResults.error.status = result.error.status;
×
1087
            }
1088
            forgotPasswordResults.error.error = `${forgotPasswordResults.error.error} ${result.error.error}`;
×
1089
          }
1090
          forgotPasswordResults.info.message = `${forgotPasswordResults.info.message} ${result.info.message}`;
×
1091
        });
1092
        if (passwordChangeResults.indexOf('Success') > -1) {
×
1093
          // At least one email was sent, this will be considered a success
1094
          // since the user just needs one of the emails to login to his/her username(s)
1095
          return {
×
1096
            forgotPasswordResults,
1097
            info: forgotPasswordResults.info,
1098
          };
1099
        }
1100
        // forgotPasswordResults.status_codes = forgotPasswordResults.status_codes;
1101
        // , forgotPasswordResults, forgotPasswordResults.info);
1102
        throw forgotPasswordResults.error;
×
1103
      });
1104
    })
1105
    .catch((error) => {
1106
      debug('forgotPassword', error.status);
2✔
1107
      const err = {
2✔
1108
        message: error.message,
1109
        status: error.status || error.statusCode,
4✔
1110
        // userFriendlyErrors: error.userFriendlyErrors,
1111
        ...error,
1112
      };
1113
      throw err;
2✔
1114
    });
1115
}
1116

1117
function handleInvalidPasswordAttempt({ user, req }) {
UNCOV
1118
  debug(`${new Date()} User found, but they have entered the wrong password ${user.username}`);
×
1119
  /*
1120
   * Log this unsucessful password attempt
1121
   */
1122
  // eslint-disable-next-line no-param-reassign
UNCOV
1123
  user.serverlogs = user.serverlogs || {};
×
1124
  // eslint-disable-next-line no-param-reassign
UNCOV
1125
  user.serverlogs.incorrectPasswordAttempts = user.serverlogs.incorrectPasswordAttempts || [];
×
UNCOV
1126
  user.serverlogs.incorrectPasswordAttempts.push(new Date());
×
1127
  // eslint-disable-next-line no-param-reassign
UNCOV
1128
  user.serverlogs.incorrectPasswordEmailSentCount = user.serverlogs.incorrectPasswordEmailSentCount || 0;
×
UNCOV
1129
  const incorrectPasswordAttemptsCount = user.serverlogs.incorrectPasswordAttempts.length;
×
1130
  // console.log('incorrectPasswordAttempts', user.serverlogs)
UNCOV
1131
  const timeToSendAnEmailEveryXattempts = incorrectPasswordAttemptsCount >= 5;
×
1132
  /* Dont reset the public user or lingllama's passwords */
UNCOV
1133
  if (user.username === 'public' || user.username === 'lingllama') {
×
1134
    return Promise.resolve({
×
1135
      user,
1136
    });
1137
  }
UNCOV
1138
  if (!timeToSendAnEmailEveryXattempts) {
×
1139
    let countDownUserToPasswordReset = '';
×
1140
    if (incorrectPasswordAttemptsCount > 1) {
×
1141
      // TOOD this isnt executing
1142
      countDownUserToPasswordReset = ` You have ${5 - incorrectPasswordAttemptsCount} more attempts before a temporary password will be emailed to your registration email (if you provided one).`;
×
1143
    }
1144
    return saveUpdateUserToDatabase({ user, req })
×
1145
      .then(() => {
1146
        debug(`${new Date()} Server logs updated in user. countDownUserToPasswordReset`, countDownUserToPasswordReset);
×
1147
        return {
×
1148
          user,
1149
          info: {
1150
            message: `Username or password is invalid. Please try again.${countDownUserToPasswordReset}`,
1151
          },
1152
        };
1153
      });
1154
  }
1155

UNCOV
1156
  debug(`${new Date()} User ${user.username} found, but they have entered the wrong password ${incorrectPasswordAttemptsCount} times. `);
×
1157
  /*
1158
   * This emails the user, if the user has an email, if the
1159
   * email is 'valid' TODO do better email validation. and if
1160
   * the config has a valid user. For the dev and local
1161
   * versions of the app, this wil never be fired because the
1162
   * config doesnt have a valid user. But the production
1163
   * config does, and it is working.
1164
   */
UNCOV
1165
  if (!user.email || user.email.length < 5) {
×
UNCOV
1166
    debug(`${new Date()}User didn't not provide a valid email, so their temporary password was not sent by email.`);
×
UNCOV
1167
    return saveUpdateUserToDatabase({ user, req })
×
1168
      .then(() => {
UNCOV
1169
        debug(`${new Date()} Email doesnt appear to be vaild. ${user.email}`);
×
UNCOV
1170
        return {
×
1171
          info: {
1172
            message: 'You have tried to log in too many times and you dont seem to have a valid email so we cant send you a temporary password.',
1173
          },
1174
        };
1175
      });
1176
  }
1177

UNCOV
1178
  const temporaryPassword = emailFunctions.makeRandomPassword();
×
UNCOV
1179
  return emailFunctions.emailTemporaryPasswordToTheUserIfTheyHavAnEmail({
×
1180
    user,
1181
    temporaryPassword,
1182
    successMessage: 'You have tried to log in too many times. We are sending a temporary password to your email.',
1183
  })
1184
    .then(({ user: userFromPasswordEmail, info }) => {
1185
      debug(`${new Date()} Reset User ${user.username} password to a temp password.`);
×
1186
      return saveUpdateUserToDatabase({ user: userFromPasswordEmail, req })
×
1187
        .then(() => ({
×
1188
          user: userFromPasswordEmail,
1189
          info,
1190
        }));
UNCOV
1191
    }).catch((emailError) => ({
×
1192
      info: {
1193
        message: `Username or password is invalid. Please try again. You have tried to log in too many times. ${emailError.userFriendlyErrors[0]}`,
1194
      },
1195
    }));
1196
}
1197

1198
/*
1199
 * Looks up the user by username, gets the user, confirms this is the right
1200
 * password. Takes user details from the request and saves them into the user,
1201
 *
1202
 * If its not the right password does some logging to find out how many times
1203
 * they have attempted, if its too many it emails them a temp password if they
1204
 * have given us a valid email. If this is a local or dev server config, it
1205
 * doesn't email, or change their password.
1206
 */
1207
function authenticateUser({
1✔
1208
  username,
1209
  password,
1210
  syncDetails,
1211
  syncUserDetails,
1212
  req = {
1✔
1213
    id: '',
1214
    log: {
1215
      // eslint-disable-next-line no-console
1216
      error: console.error,
1217
      // eslint-disable-next-line no-console
1218
      warn: console.warn,
1219
    },
1220
  },
1221
} = {}) {
1222
  if (!username) {
43✔
1223
    const err = new Error(`Username was not specified. ${username}`);
1✔
1224
    err.status = 412;
1✔
1225
    err.userFriendlyErrors = ['Please supply a username.'];
1✔
1226
    return Promise.reject(err);
1✔
1227
  }
1228

1229
  if (!password) {
42✔
1230
    const err = new Error(`Password was not specified. ${password}`);
1✔
1231
    err.status = 412;
1✔
1232
    err.userFriendlyErrors = ['Please supply a password.'];
1✔
1233
    return Promise.reject(err);
1✔
1234
  }
1235

1236
  const safeUsernameForCouchDB = Connection.validateUsername(username.trim());
41✔
1237
  if (username !== safeUsernameForCouchDB.identifier) {
41✔
1238
    const err = new Error('username is not safe for db names');
3✔
1239
    err.status = 406;
3✔
1240
    err.userFriendlyErrors = [`Username or password is invalid. Maybe your username is ${safeUsernameForCouchDB.identifier}?`];
3✔
1241
    return Promise.reject(err);
3✔
1242
  }
1243

1244
  return findByUsername({ username, req })
38✔
UNCOV
1245
    .then(({ user }) => verifyPassword({ password, user })
×
UNCOV
1246
      .catch((error) => handleInvalidPasswordAttempt({ user, req })
×
1247
        .then(({ info }) => {
1248
          // Don't tell them its because the password is wrong.
UNCOV
1249
          debug(`${new Date()} Returning: Username or password is invalid. Please try again.`);
×
UNCOV
1250
          const err = {
×
1251
            message: error.message,
1252
            userFriendlyErrors: [info.message],
1253
            status: error.status || 401,
×
1254
            ...error,
1255
          };
UNCOV
1256
          throw err;
×
1257
        })))
1258
    .then(({ user }) => {
UNCOV
1259
      debug(`${new Date()} User found, and password verified ${username} user.toJSON ${typeof user.toJSON}`);
×
1260
      /*
1261
       * Save the users' updated details, and return to caller TODO Add
1262
       * more attributes from the req.body below
1263
       */
UNCOV
1264
      if (syncDetails !== 'true' && syncDetails !== true) {
×
UNCOV
1265
        return { user };
×
1266
      }
UNCOV
1267
      debug(`${new Date()} Here is syncUserDetails: ${JSON.stringify(syncUserDetails)}`);
×
1268
      let userToSave;
UNCOV
1269
      try {
×
UNCOV
1270
        userToSave = new User(syncUserDetails);
×
UNCOV
1271
        userToSave.newCorpora = userToSave.newCorpora || userToSave.newCorpusConnections;
×
1272
      } catch (e) {
1273
        req.log.error(e, "Couldnt convert the users' sync details into a user.");
×
1274
      }
UNCOV
1275
      if (!userToSave || !userToSave.newCorpora) {
×
1276
        return { user };
×
1277
      }
UNCOV
1278
      debug(`${new Date()} It looks like the user has created some new local offline newCorpora. Attempting to make new corpus on the team server so the user can download them.`);
×
UNCOV
1279
      return createNewCorpusesIfDontExist({ user, corpora: userToSave.newCorpora, req })
×
1280
        .then((corpora) => {
1281
          // TODO this corpora is not written into the user?
UNCOV
1282
          debug('createNewCorpusesIfDontExist corpora', corpora);
×
1283
          // user = new User(user);
1284
          // TODO remove newCorpora?
UNCOV
1285
          user.merge('self', userToSave, 'overwrite');
×
1286
          // user = user.toJSON();
1287
          /* Users details which can come from a client side must be added here,
1288
          otherwise they are not saved on sync. */
1289
          // user.corpora = syncUserDetails.corpora;
1290
          // user.corpora = syncUserDetails.corpora;
1291
          // user.email = syncUserDetails.email;
1292
          // user.gravatar = syncUserDetails.gravatar;
1293
          // user.researchInterest = syncUserDetails.researchInterest;
1294
          // user.affiliation = syncUserDetails.affiliation;
1295
          // user.appVersionWhenCreated = syncUserDetails.appVersionWhenCreated;
1296
          // user.authUrl = syncUserDetails.authUrl;
1297
          // user.description = syncUserDetails.description;
1298
          // user.subtitle = syncUserDetails.subtitle;
1299
          // user.dataLists = syncUserDetails.dataLists;
1300
          // user.prefs = syncUserDetails.prefs;
1301
          // user.mostRecentIds = syncUserDetails.mostRecentIds;
1302
          // user.firstname = syncUserDetails.firstname;
1303
          // user.lastname = syncUserDetails.lastname;
1304
          // user.sessionHistory = syncUserDetails.sessionHistory;
1305
          // user.hotkeys = syncUserDetails.hotkeys;
UNCOV
1306
          return { user };
×
1307
        });
1308
      // .catch((err) => {
1309
      //   if (err.corpusexisted) {
1310
      //     return { user };
1311
      //   }
1312
      //   throw err;
1313
      // });
1314
    })
1315
    .then(({ user }) => {
1316
      // if we avoid assign into the arg, we loose the _rev and get a document update conflict
1317
      // const user = userToSave.clone();
1318
      // user._rev = userToSave._rev;
1319
      // eslint-disable-next-line no-param-reassign
UNCOV
1320
      user.dateModified = new Date();
×
1321
      // eslint-disable-next-line no-param-reassign
UNCOV
1322
      user.serverlogs = user.serverlogs || {};
×
1323
      // eslint-disable-next-line no-param-reassign
UNCOV
1324
      user.serverlogs.successfulLogins = user.serverlogs.successfulLogins || [];
×
UNCOV
1325
      user.serverlogs.successfulLogins.push(new Date());
×
UNCOV
1326
      if (user.serverlogs.incorrectPasswordAttempts && user.serverlogs.incorrectPasswordAttempts.length > 0) {
×
1327
        // eslint-disable-next-line no-param-reassign
1328
        user.serverlogs.oldIncorrectPasswordAttempts = user.serverlogs.oldIncorrectPasswordAttempts || [];
×
1329
        // eslint-disable-next-line no-param-reassign
1330
        user.serverlogs.oldIncorrectPasswordAttempts = user.serverlogs.oldIncorrectPasswordAttempts
×
1331
          .concat(user.serverlogs.incorrectPasswordAttempts);
1332
        // eslint-disable-next-line no-param-reassign
1333
        user.serverlogs.incorrectPasswordAttempts = [];
×
1334
      }
UNCOV
1335
      return saveUpdateUserToDatabase({ user, req });
×
1336
    })
1337
    .catch((error) => {
1338
      debug('error', error);
38✔
1339
      // Don't tell them its because the user doesnt exist
1340
      const err = {
38✔
1341
        message: error.message,
1342
        status: error.status,
1343
        ...error,
1344
      };
1345
      if (err.message === `User ${username} does not exist`) {
38!
UNCOV
1346
        err.status = 401;
×
UNCOV
1347
        err.userFriendlyErrors = ['Username or password is invalid. Please try again.'];
×
1348
      }
1349
      throw err;
38✔
1350
    });
1351
}
1352
/**
1353
 * Takes parameters from the request and creates a new user json, salts and
1354
 * hashes the password, has the corpusmanagement library create a new couchdb
1355
 * user, permissions and couches for the new user. The returns the save of the
1356
 * user to the users database.
1357
 */
1358
function registerNewUser({
1✔
1359
  req = {
1✔
1360
    body: {},
1361
    id: '',
1362
    log: {
1363
      // eslint-disable-next-line no-console
1364
      error: console.error,
1365
      // eslint-disable-next-line no-console
1366
      warn: console.warn,
1367
    },
1368
  },
1369
} = {}) {
1370
  if (req.body.username === 'yourusernamegoeshere') {
32✔
1371
    const err = new Error('Username is the default username');
1✔
1372
    err.status = 412;
1✔
1373
    err.userFriendlyErrors = ['Please type a username instead of yourusernamegoeshere.'];
1✔
1374
    return Promise.reject(err);
1✔
1375
  }
1376
  if (!req || !req.body.username || !req.body.username) {
31✔
1377
    const err = new Error('Please provide a username');
2✔
1378
    err.status = 412;
2✔
1379
    return Promise.reject(err);
2✔
1380
  }
1381
  if (req.body.username.length < 3) {
29✔
1382
    const err = new Error(`Please choose a longer username \`${req.body.username}\` is too short.`);
2✔
1383
    err.status = 412;
2✔
1384
    return Promise.reject(err);
2✔
1385
  }
1386
  const safeUsernameForCouchDB = Connection.validateUsername(req.body.username);
27✔
1387
  if (req.body.username !== safeUsernameForCouchDB.identifier) {
27✔
1388
    const err = new Error('username is not safe for db names');
2✔
1389
    err.status = 406;
2✔
1390
    err.userFriendlyErrors = [`Please use '${safeUsernameForCouchDB.identifier}' instead (the username you have chosen isn't very safe for urls, which means your corpora would be potentially inaccessible in old browsers)`];
2✔
1391
    return Promise.reject(err);
2✔
1392
  }
1393
  // Make sure the username doesn't exist.
1394
  return findByUsername({
25✔
1395
    username: req.body.username,
1396
    req,
1397
    // req: {
1398
    //   id: `pre-${req.id}`,
1399
    //   log: req.log
1400
    // },
1401
  })
1402
    .then(() => {
UNCOV
1403
      const err = new Error(`Username ${req.body.username} already exists, try a different username.`);
×
UNCOV
1404
      err.status = err.status || err.statusCode || 409;
×
UNCOV
1405
      throw err;
×
1406
    })
1407
    .catch((err) => {
1408
      if (err.message !== `User ${req.body.username} does not exist`) {
25!
1409
        throw err;
25✔
1410
      }
1411

UNCOV
1412
      debug(`${new Date()} Registering new user: ${req.body.username}`);
×
UNCOV
1413
      const user = new User(req.body);
×
UNCOV
1414
      const salt = bcrypt.genSaltSync(10);
×
UNCOV
1415
      user.hash = bcrypt.hashSync(req.body.password, salt);
×
UNCOV
1416
      user.dateCreated = user.dateCreated || Date.now();
×
UNCOV
1417
      user.authServerVersionWhenCreated = authServerVersion;
×
UNCOV
1418
      user.prefs = JSON.parse(JSON.stringify(DEFAULT_USER_PREFERENCES));
×
1419
      // FieldDB.User is not setting the gravatar from the username
UNCOV
1420
      if (!user.gravatar) {
×
UNCOV
1421
        user.gravatar = md5(user.username);
×
1422
      }
UNCOV
1423
      let { appbrand } = req.body;
×
UNCOV
1424
      if (!appbrand) {
×
1425
        if (req.body.username.indexOf('test') > -1) {
×
1426
          appbrand = 'beta';
×
1427
        } else if (req.body.username.indexOf('anonymouskartulispeechrecognition') === 0) {
×
1428
          appbrand = 'kartulispeechrecognition';
×
1429
        } else if (req.body.username.search(/anonymous[0-9]/) === 0) {
×
1430
          appbrand = 'georgiantogether';
×
1431
        } else if (req.body.username.search(/anonymouswordcloud/) === 0) {
×
1432
          appbrand = 'wordcloud';
×
1433
        } else {
1434
          appbrand = 'lingsync';
×
1435
        }
1436
      }
UNCOV
1437
      user.appbrand = appbrand;
×
UNCOV
1438
      debug('createCouchDBUser appbrand', appbrand);
×
1439

UNCOV
1440
      if (dontCreateDBsForLearnXUsersBecauseThereAreTooManyOfThem
×
1441
      && req.body.username.indexOf('anonymous') === 0) {
UNCOV
1442
        debug(`${new Date()}  delaying creation of the dbs for ${req.body.username} until they can actually use them.`);
×
1443
        // user.newCorpora = user.corpora;
UNCOV
1444
        return emailFunctions.emailWelcomeToTheUser({ user })
×
1445
          .then(() => {
UNCOV
1446
            debug(`${new Date()} Sent command to save user to couch`);
×
1447
            /*
1448
           * The user was built correctly, saves the new user into the users database
1449
           */
UNCOV
1450
            return saveUpdateUserToDatabase({ user, req });
×
1451
          });
1452
      }
1453

UNCOV
1454
      const connection = req.body.connection
×
1455
        || req.body.couchConnection ? new Connection(req.body.connection
×
1456
        || req.body.couchConnection) : Connection.defaultConnection(user.appbrand);
UNCOV
1457
      connection.appbrand = connection.appbrand || user.appbrand;
×
1458
      // TODO this has to be come asynchonous if this design is a central server who can register users on other servers
UNCOV
1459
      if (!connection.dbname || connection.dbname === `${user.username}-firstcorpus`) {
×
UNCOV
1460
        connection.dbname = `${user.username}-firstcorpus`;
×
UNCOV
1461
        if (connection.appbrand === 'phophlo') {
×
1462
          connection.dbname = `${user.username}-phophlo`;
×
1463
        }
UNCOV
1464
        if (connection.appbrand === 'kartulispeechrecognition') {
×
1465
          connection.dbname = `${user.username}-kartuli`;
×
1466
        }
UNCOV
1467
        if (connection.appbrand === 'georgiantogether') {
×
UNCOV
1468
          connection.dbname = `${user.username}-kartuli`;
×
1469
        }
1470
      }
1471

UNCOV
1472
      return createCouchDBUser({
×
1473
        req,
1474
        user,
1475
        password: req.body.password,
1476
        // connection: corpusDetails.connection,
1477
      })
1478
        .then(({ couchUser, whichUserGroup }) => {
UNCOV
1479
          debug('user created', couchUser, whichUserGroup);
×
1480

UNCOV
1481
          return corpus.createNewCorpus({
×
1482
            connection,
1483
            req,
1484
            user,
1485
            whichUserGroup,
1486
          });
1487
        })
1488
        // .then((result) => {
1489
        //   debug(`${new Date()} There was success in creating the corpus: ${JSON.stringify(result.info)}\n`);
1490
        //   /* Save corpus, datalist and session docs so that apps can load the dashboard for the user */
1491
        //   const db = nano({
1492
        //     headers: {
1493
        //       'x-request-id': req.id,
1494
        //     },
1495
        //     url: `${couchConnectUrl}/${corpusDetails.dbname}`,
1496
        //   });
1497
        //   return db.bulk({
1498
        //     docs: docsNeededForAProperFieldDBDatabase,
1499
        //   });
1500
        // })
1501
        .then((couchresponse) => {
UNCOV
1502
          debug(`${new Date()} Created corpus for ${connection.dbname}\n`, couchresponse);
×
UNCOV
1503
          user.corpora = [connection];
×
UNCOV
1504
          return emailFunctions.emailWelcomeToTheUser({ user })
×
1505
            .then(() => {
UNCOV
1506
              debug(`${new Date()} Sent command to save user to couch`);
×
1507
              /*
1508
             * The user was built correctly, saves the new user into the users database
1509
             */
UNCOV
1510
              return saveUpdateUserToDatabase({ user, req });
×
1511
            });
1512
        })
1513
        .catch((couchErr) => {
1514
          debug(couchErr, `${new Date()} There was an couchError in creating the docs for the users first corpus`);
×
1515
          undoCorpusCreation({ user, connection });
×
1516
          const error = {
×
1517
            message: couchErr.message,
1518
            status: couchErr.status || couchErr.statusCode,
×
1519
            userFriendlyErrors: [`Server is not responding for request to create user \`${user.username}\`. Please report this.`],
1520
            ...couchErr,
1521
          };
1522
          throw error;
×
1523
          // debug(`${new Date()} There was an couchError in creating the corpus
1524
          // database: ${JSON.stringify(couchErr)}\n`);
1525
          // undoCorpusCreation(user, corpusDetails.connection, docsNeededForAProperFieldDBDatabase);
1526
          // couchErr.status = couchErr.status || couchErr.statusCode || 500;
1527
          // couchErr.userFriendlyErrors = [`Server is not responding for request
1528
          // to create user \`${user.username}\`. Please report this.`];
1529
          // reject(couchErr);
1530
        });
1531
    });
1532
}
1533

1534
module.exports = {
1✔
1535
  addCorpusToUser,
1536
  addRoleToUser,
1537
  authenticateUser,
1538
  createNewCorpusesIfDontExist,
1539
  fetchCorpusPermissions,
1540
  findByEmail,
1541
  findByUsername,
1542
  forgotPassword,
1543
  registerNewUser,
1544
  sampleUsers,
1545
  saveUpdateUserToDatabase,
1546
  setPassword,
1547
  sortByUsername,
1548
  undoCorpusCreation,
1549
  verifyPassword,
1550
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc