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

FieldDB / AuthenticationWebService / 20049962129

09 Dec 2025 02:37AM UTC coverage: 44.692% (-29.6%) from 74.284%
20049962129

Pull #134

github

web-flow
Merge 936545d57 into a9858f6b0
Pull Request #134: 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.

640 of 1347 relevant lines covered (47.51%)

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
    agentOptions: {
90
      headers: {
91
        'x-request-id': req.id || '',
×
92
      },
93
    },
94
    url: couchConnectUrl,
95
  });
96

UNCOV
97
  const usersdb = couchdb.db.use('_users');
×
98

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

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

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

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

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

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

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

355
function createNewCorpusesIfDontExist({
1✔
356
  user,
357
  corpora,
358
  req,
359
} = {}) {
360
  if (!corpora || corpora.length === 0) {
3✔
361
    return Promise.resolve([]);
1✔
362
  }
363

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

376
    if (potentialnewcorpusconnection.dbname.indexOf(`${user.username}-`) !== 0) {
4✔
377
      debug('Not creating a corpus which appears to belong ot another user.', potentialnewcorpusconnection);
2✔
378
      return Promise.resolve(potentialnewcorpusconnection);
2✔
379
    }
380
    requestedDBCreation[potentialnewcorpusconnection.dbname] = true;
2✔
381

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

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

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

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

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

510
  const usersdb = nano({
70✔
511
    agentOptions: {
512
      headers: {
513
        'x-request-id': req.id || '',
70!
514
      },
515
    },
516
    url: couchConnectUrl,
517
  }).db.use(config.usersDbConnection.dbname);
518
  return usersdb.get(username)
70✔
519
    .then((result) => {
UNCOV
520
      debug(`${new Date()} User found: ${result.username}`);
×
UNCOV
521
      if (result.serverlogs && result.serverlogs.disabled) {
×
UNCOV
522
        const err = new Error(`User ${username} has been disabled, probably because of a violation of the terms of service. ${result.serverlogs.disabled}`);
×
UNCOV
523
        err.status = 401;
×
UNCOV
524
        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
525
        throw err;
×
526
      }
527

UNCOV
528
      const user = new User(result);
×
529
      // const user = {
530
      //   ...result,
531
      // };
532
      // user.corpora = user.corpora || user.corpuses || [];
UNCOV
533
      if (result.corpuses) {
×
534
        debug(` Upgrading ${result.username} data structure to v3.0`);
×
535
        // delete user.corpuses;
536
      }
537

UNCOV
538
      return { user };
×
539
    })
540
    .catch((error) => {
541
      if (error.error === 'not_found') {
70!
UNCOV
542
        debug(error, `${new Date()} No User found: ${username}`);
×
UNCOV
543
        const err = new Error(`User ${username} does not exist`);
×
UNCOV
544
        err.status = 404;
×
545
        // err.userFriendlyErrors = ['Username or password is invalid. Please try again.'];
UNCOV
546
        throw err;
×
547
      }
548

549
      if (error.error === 'unauthorized') {
70!
550
        req.log.error(error, `${new Date()} Wrong admin username and password`);
×
551
        throw new Error('Server is mis-configured. Please report this error 8914.');
×
552
      }
553

554
      throw error;
70✔
555
    });
556
}
557

558
/**
559
 * uses tries to look up users by email
560
 * if optionallyRestrictToIncorrectLoginAttempts
561
 *  then it will look up users who might be calling forgotPassword
562
 */
563
function findByEmail({
1✔
564
  email,
565
  optionallyRestrictToIncorrectLoginAttempts,
566
  req = {
1✔
567
    id: '',
568
    log: {
569
      // eslint-disable-next-line no-console
570
      error: console.error,
571
      // eslint-disable-next-line no-console
572
      warn: console.warn,
573
    },
574
  },
575
} = {}) {
576
  if (!email) {
4✔
577
    return Promise.reject(new Error('Please provide an email'));
1✔
578
  }
579
  let usersQuery = 'usersByEmail';
3✔
580
  if (optionallyRestrictToIncorrectLoginAttempts) {
3✔
581
    usersQuery = 'userWhoHaveTroubleLoggingIn';
2✔
582
  }
583
  usersQuery = `${usersQuery}?key="${email}"`;
3✔
584
  const usersdb = nano({
3✔
585
    agentOptions: {
586
      headers: {
587
        'x-request-id': req.id || '',
3!
588
      },
589
    },
590
    url: couchConnectUrl,
591
  }).db.use(config.usersDbConnection.dbname);
592
  // Query the database and callback with matching users
593
  return usersdb.view('users', usersQuery)
3✔
594
    .then((body) => {
UNCOV
595
      debug(`${new Date()} ${usersQuery} requested users who have this email ${email} from the server, and recieved results `);
×
UNCOV
596
      const users = body.rows.map((row) => row.value);
×
UNCOV
597
      debug(`${new Date()} users ${JSON.stringify(users)}`);
×
UNCOV
598
      if (users.length === 0) {
×
UNCOV
599
        const err = new Error(`No matching users for ${optionallyRestrictToIncorrectLoginAttempts}`);
×
UNCOV
600
        err.status = 401;
×
UNCOV
601
        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
602
        throw err;
×
603
      }
UNCOV
604
      return ({
×
605
        users,
606
        info: {
607
          message: `Found ${users.length} users for ${optionallyRestrictToIncorrectLoginAttempts}`,
608
        },
609
      });
610
    })
611
    .catch((error) => {
612
      debug(`${new Date()} Error in findByEmail while quering ${usersQuery} ${JSON.stringify(error)}`);
3✔
613
      // const err = {
614
      //   message: error.message,
615
      //   status: error.status || error.statusCode,
616
      //   userFriendlyErrors: ['Server is not responding to request. Please report this 1609'],
617
      //   ...error,
618
      // };
619
      // error.userFriendlyErrors = error.userFriendlyErrors ||
620
      // ['Server is not responding to request. Please report this 1609'];
621
      throw error;
3✔
622
    });
623
}
624

625
function addCorpusToUser({
1✔
626
  username,
627
  newConnection,
628
  userPermissionResult,
629
  req,
630
} = {}) {
631
  return findByUsername({ username, req })
2✔
632
    .then(({ user, info }) => {
UNCOV
633
      debug('Find by username ', info);
×
634

UNCOV
635
      let shouldEmailWelcomeToCorpusToUser = false;
×
636
      // eslint-disable-next-line no-param-reassign
UNCOV
637
      user.serverlogs = user.serverlogs || {};
×
638
      // eslint-disable-next-line no-param-reassign
UNCOV
639
      user.serverlogs.welcomeToCorpusEmails = user.serverlogs.welcomeToCorpusEmails || {};
×
UNCOV
640
      if (!userPermissionResult.after) {
×
641
        const err = new Error('userPermissionResult.after was undefined');
×
642
        err.userFriendlyErrors = ['The server has errored please report this. 724'];
×
643
        throw err;
×
644
      }
645

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

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

760
          throw err;
×
761
        });
762
    })
763
    .catch((error) => {
764
      debug('error looking up user ', error);
2✔
765
      const err = {
2✔
766
        message: error.message,
767
        status: error.status || error.statusCode,
4✔
768
        userFriendlyErrors: ['Username doesnt exist on this server. This is a bug.'],
769
        ...error,
770
      };
771

772
      throw err;
2✔
773

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

840
  return corpus.isRequestingUserAnAdminOnCorpus({
1✔
841
    connection: dbConn,
842
    req,
843
    username: req.body.username,
844
  }).then((result) => {
UNCOV
845
    debug(`${new Date()} User ${req.body.username} is admin and can modify permissions on ${dbConn.dbname}`, result);
×
846
    // convert roles into corpus specific roles
UNCOV
847
    const promises = req.body.users.map((userPermissionOriginal) => {
×
UNCOV
848
      const userPermission = {
×
849
        ...userPermissionOriginal,
850
      };
UNCOV
851
      if (userPermission.add && typeof userPermission.add.map === 'function') {
×
UNCOV
852
        userPermission.add = userPermission.add
×
853
          // TODO unify with corpus.addRoleToUserInfo
UNCOV
854
          .map((role) => normalizeRolesToDBname({ dbname: dbConn.dbname, role }));
×
855
      } else {
UNCOV
856
        userPermission.add = [];
×
857
      }
858

UNCOV
859
      if (userPermission.remove && typeof userPermission.remove.map === 'function') {
×
UNCOV
860
        userPermission.remove = userPermission.remove
×
UNCOV
861
          .map((role) => normalizeRolesToDBname({ dbname: dbConn.dbname, role }));
×
862
      } else {
863
        userPermission.remove = [];
×
864
      }
UNCOV
865
      debug('userPermission', userPermission);
×
UNCOV
866
      return userPermission;
×
867
    })
868
      /*
869
       * If they are admin, add the role to the user, then add the corpus to user if succesfull
870
       */
871
      .map((userPermission) => {
UNCOV
872
        debug('userPermission', userPermission);
×
UNCOV
873
        return corpus.addRoleToUserInfo({ connection: dbConn, userPermission })
×
874
          .then(({ userPermissionResult }) => {
UNCOV
875
            debug('corpus.addRoleToUserInfo result', userPermissionResult);
×
UNCOV
876
            return addCorpusToUser({
×
877
              username: userPermission.username, userPermissionResult, newConnection: dbConn, req,
878
            });
879
          })
UNCOV
880
          .then(({ userPermissionResult: userPermissionFinalResult }) => userPermissionFinalResult)
×
881
          .catch((error) => {
UNCOV
882
            const err = {
×
883
              message: error.message,
884
              status: error.status || error.statusCode || 412,
×
885
              userFriendlyErrors: [`Unable to add ${userPermission.username} to this corpus.${error.statusCode === 404 ? ' User not found.' : ''}`],
×
886
              ...error,
887
            };
UNCOV
888
            throw err;
×
889
          });
890
      });
891

UNCOV
892
    return Promise.all(promises);
×
893
    // .then((results) => {
894
    //   let thereWasAnError;
895
    //   debug(`${new Date()} recieved promises back ${results.length}`);
896
    //   // results.forEach((userPermis) => {
897
    //   //   if (userPermis && userPermis.status !== 200 && !thereWasAnError) {
898
    //   //     thereWasAnError = new Error(`One or more of the add roles requsts failed. ${userPermis.message}`);
899
    //   //     debug('one errored', thereWasAnError);
900
    //   //   }
901
    //   // });
902

903
    //   return results;
904

905
    //   // if (!thereWasAnError) {
906
    //   //   console.log('reeturning the results', results)
907
    //   //   return results;
908
    //   // } else {
909
    //   //   throw thereWasAnError;
910
    //   // }
911
    // });
912
    // .catch(reject);
913
    // .fail(function(error) {
914
    //       debug(" returning fail.");
915
    //       error.status = cleanErrorStatus(error.status) || req.body.users[0].status || 500;
916
    //       req.body.users[0].message = req.body.users[0].message ||
917
    //  " There was a problem processing your request. Please notify us of this error 320343";
918
    //       return done(error, req.body.users);
919
    //     });
920
  })
921
    .catch((err) => {
922
      debug(err, 'error isRequestingUserAnAdminOnCorpus');
1✔
923
      throw err;
1✔
924
    });
925
  // });
926
}
927

928
function verifyPassword({
1✔
929
  password,
930
  user,
931
} = {}) {
932
  return new Promise((resolve, reject) => {
3✔
933
    setTimeout(() => {
3✔
934
      try {
3✔
935
        /*
936
         * If the user didnt furnish a password, set a fake one. It will return
937
         * unauthorized.
938
         */
939
        bcrypt.compare(password || ' ', user.hash)
3✔
940
          .then((matches) => {
941
            if (matches) {
2✔
942
              return resolve({ user });
1✔
943
            }
944

945
            const err = new Error('Username or password is invalid. Please try again.');
1✔
946
            err.status = 401;
1✔
947
            return reject(err);
1✔
948
          })
949
          .catch(reject);
950
      } catch (err) {
951
        reject(err);
1✔
952
      }
953
    });
954
  });
955
}
956

957
/**
958
 * accepts an old and new password, a user and a function to be
959
 * called with (error, user, info)
960
 * TODO rename to changePassword
961
 *
962
 * @param oldpassword
963
 * @param newpassword
964
 * @param username
965
 */
966
function setPassword({
1✔
967
  oldpassword,
968
  newpassword,
969
  username,
970
  req,
971
} = {}) {
972
  if (!username) {
4✔
973
    const err = new Error('Please provide a username');
1✔
974
    err.status = 412;
1✔
975
    return Promise.reject(err);
1✔
976
  }
977
  if (!oldpassword) {
3!
978
    const err = new Error('Please provide your old password');
×
979
    err.status = 412;
×
980
    return Promise.reject(err);
×
981
  }
982
  if (!newpassword) {
3✔
983
    const err = new Error('Please provide your new password');
1✔
984
    err.status = 412;
1✔
985
    return Promise.reject(err);
1✔
986
  }
987
  const safeUsernameForCouchDB = Connection.validateUsername(username);
2✔
988
  if (username !== safeUsernameForCouchDB.identifier) {
2!
989
    const err = new Error('Username or password is invalid. Please try again.');
×
990
    err.status = 412;
×
991
    return Promise.reject(err);
×
992
  }
993

994
  return findByUsername({ username, req })
2✔
995
    .then(({ user }) => {
UNCOV
996
      debug(`${new Date()} Found user in setPassword: ${JSON.stringify(user)}`);
×
997

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

1030
      throw err;
2✔
1031
    });
1032
}
1033

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

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

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

UNCOV
1168
  debug(`${new Date()} User ${user.username} found, but they have entered the wrong password ${incorrectPasswordAttemptsCount} times. `);
×
1169
  /*
1170
   * This emails the user, if the user has an email, if the
1171
   * email is 'valid' TODO do better email validation. and if
1172
   * the config has a valid user. For the dev and local
1173
   * versions of the app, this wil never be fired because the
1174
   * config doesnt have a valid user. But the production
1175
   * config does, and it is working.
1176
   */
UNCOV
1177
  if (!user.email || user.email.length < 5) {
×
UNCOV
1178
    debug(`${new Date()}User didn't not provide a valid email, so their temporary password was not sent by email.`);
×
UNCOV
1179
    return saveUpdateUserToDatabase({ user, req })
×
1180
      .then(() => {
UNCOV
1181
        debug(`${new Date()} Email doesnt appear to be vaild. ${user.email}`);
×
UNCOV
1182
        return {
×
1183
          info: {
1184
            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.',
1185
          },
1186
        };
1187
      });
1188
  }
1189

UNCOV
1190
  const temporaryPassword = emailFunctions.makeRandomPassword();
×
UNCOV
1191
  return emailFunctions.emailTemporaryPasswordToTheUserIfTheyHavAnEmail({
×
1192
    user,
1193
    temporaryPassword,
1194
    successMessage: 'You have tried to log in too many times. We are sending a temporary password to your email.',
1195
  })
1196
    .then(({ user: userFromPasswordEmail, info }) => {
1197
      debug(`${new Date()} Reset User ${user.username} password to a temp password.`);
×
1198
      return saveUpdateUserToDatabase({ user: userFromPasswordEmail, req })
×
1199
        .then(() => ({
×
1200
          user: userFromPasswordEmail,
1201
          info,
1202
        }));
UNCOV
1203
    }).catch((emailError) => ({
×
1204
      info: {
1205
        message: `Username or password is invalid. Please try again. You have tried to log in too many times. ${emailError.userFriendlyErrors[0]}`,
1206
      },
1207
    }));
1208
}
1209

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

1241
  if (!password) {
42✔
1242
    const err = new Error(`Password was not specified. ${password}`);
1✔
1243
    err.status = 412;
1✔
1244
    err.userFriendlyErrors = ['Please supply a password.'];
1✔
1245
    return Promise.reject(err);
1✔
1246
  }
1247

1248
  const safeUsernameForCouchDB = Connection.validateUsername(username.trim());
41✔
1249
  if (username !== safeUsernameForCouchDB.identifier) {
41✔
1250
    const err = new Error('username is not safe for db names');
3✔
1251
    err.status = 406;
3✔
1252
    err.userFriendlyErrors = [`Username or password is invalid. Maybe your username is ${safeUsernameForCouchDB.identifier}?`];
3✔
1253
    return Promise.reject(err);
3✔
1254
  }
1255

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

UNCOV
1424
      debug(`${new Date()} Registering new user: ${req.body.username}`);
×
UNCOV
1425
      const user = new User(req.body);
×
UNCOV
1426
      const salt = bcrypt.genSaltSync(10);
×
UNCOV
1427
      user.hash = bcrypt.hashSync(req.body.password, salt);
×
UNCOV
1428
      user.dateCreated = user.dateCreated || Date.now();
×
UNCOV
1429
      user.authServerVersionWhenCreated = authServerVersion;
×
UNCOV
1430
      user.prefs = JSON.parse(JSON.stringify(DEFAULT_USER_PREFERENCES));
×
1431
      // FieldDB.User is not setting the gravatar from the username
UNCOV
1432
      if (!user.gravatar) {
×
UNCOV
1433
        user.gravatar = md5(user.username);
×
1434
      }
UNCOV
1435
      let { appbrand } = req.body;
×
UNCOV
1436
      if (!appbrand) {
×
1437
        if (req.body.username.indexOf('test') > -1) {
×
1438
          appbrand = 'beta';
×
1439
        } else if (req.body.username.indexOf('anonymouskartulispeechrecognition') === 0) {
×
1440
          appbrand = 'kartulispeechrecognition';
×
1441
        } else if (req.body.username.search(/anonymous[0-9]/) === 0) {
×
1442
          appbrand = 'georgiantogether';
×
1443
        } else if (req.body.username.search(/anonymouswordcloud/) === 0) {
×
1444
          appbrand = 'wordcloud';
×
1445
        } else {
1446
          appbrand = 'lingsync';
×
1447
        }
1448
      }
UNCOV
1449
      user.appbrand = appbrand;
×
UNCOV
1450
      debug('createCouchDBUser appbrand', appbrand);
×
1451

UNCOV
1452
      if (dontCreateDBsForLearnXUsersBecauseThereAreTooManyOfThem
×
1453
      && req.body.username.indexOf('anonymous') === 0) {
UNCOV
1454
        debug(`${new Date()}  delaying creation of the dbs for ${req.body.username} until they can actually use them.`);
×
1455
        // user.newCorpora = user.corpora;
UNCOV
1456
        return emailFunctions.emailWelcomeToTheUser({ user })
×
1457
          .then(() => {
UNCOV
1458
            debug(`${new Date()} Sent command to save user to couch`);
×
1459
            /*
1460
           * The user was built correctly, saves the new user into the users database
1461
           */
UNCOV
1462
            return saveUpdateUserToDatabase({ user, req });
×
1463
          });
1464
      }
1465

UNCOV
1466
      const connection = req.body.connection
×
1467
        || req.body.couchConnection ? new Connection(req.body.connection
×
1468
        || req.body.couchConnection) : Connection.defaultConnection(user.appbrand);
UNCOV
1469
      connection.appbrand = connection.appbrand || user.appbrand;
×
1470
      // TODO this has to be come asynchonous if this design is a central server who can register users on other servers
UNCOV
1471
      if (!connection.dbname || connection.dbname === `${user.username}-firstcorpus`) {
×
UNCOV
1472
        connection.dbname = `${user.username}-firstcorpus`;
×
UNCOV
1473
        if (connection.appbrand === 'phophlo') {
×
1474
          connection.dbname = `${user.username}-phophlo`;
×
1475
        }
UNCOV
1476
        if (connection.appbrand === 'kartulispeechrecognition') {
×
1477
          connection.dbname = `${user.username}-kartuli`;
×
1478
        }
UNCOV
1479
        if (connection.appbrand === 'georgiantogether') {
×
UNCOV
1480
          connection.dbname = `${user.username}-kartuli`;
×
1481
        }
1482
      }
1483

UNCOV
1484
      return createCouchDBUser({
×
1485
        req,
1486
        user,
1487
        password: req.body.password,
1488
        // connection: corpusDetails.connection,
1489
      })
1490
        .then(({ couchUser, whichUserGroup }) => {
UNCOV
1491
          debug('user created', couchUser, whichUserGroup);
×
1492

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

1548
module.exports = {
1✔
1549
  addCorpusToUser,
1550
  addRoleToUser,
1551
  authenticateUser,
1552
  createNewCorpusesIfDontExist,
1553
  fetchCorpusPermissions,
1554
  findByEmail,
1555
  findByUsername,
1556
  forgotPassword,
1557
  registerNewUser,
1558
  sampleUsers,
1559
  saveUpdateUserToDatabase,
1560
  setPassword,
1561
  sortByUsername,
1562
  undoCorpusCreation,
1563
  verifyPassword,
1564
};
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