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

FieldDB / AuthenticationWebService / 19936671137

04 Dec 2025 04:45PM UTC coverage: 24.852% (-51.5%) from 76.331%
19936671137

Pull #100

github

web-flow
Merge a4ec967ff into c246fc2cb
Pull Request #100: upgrade couchdb to 3.1.2

119 of 963 branches covered (12.36%)

Branch coverage included in aggregate %.

595 of 1910 relevant lines covered (31.15%)

0.33 hits per line

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

16.37
/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({
×
49
  user,
50
  connection,
51
  docs,
52
} = {}) {
53
  debug(`${new Date()} TODO need to clean up a broken corpus.${JSON.stringify(connection)}`, docs);
×
54
  return emailFunctions.emailCorusCreationFailure({ connection, user });
×
55
}
56

57
function sortByUsername(a, b) {
58
  if (a.username < b.username) {
×
59
    return -1;
×
60
  }
61
  if (a.username > b.username) {
×
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
   */
83
  let whichUserGroup = 'normalusers';
1✔
84
  if (user.username.indexOf('test') > -1 || user.username.indexOf('anonymous') > -1) {
1!
85
    whichUserGroup = 'betatesters';
1✔
86
  }
87
  debug('createCouchDBUser whichUserGroup', whichUserGroup);
1✔
88
  const couchdb = nano({
1✔
89
    requestDefaults: {
90
      headers: {
91
        'x-request-id': req.id || '',
1!
92
      },
93
    },
94
    url: couchConnectUrl,
95
  });
96

97
  const usersdb = couchdb.db.use('_users');
1✔
98

99
  const userid = `org.couchdb.user:${user.username}`;
1✔
100
  const userParamsForNewUser = {
1✔
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

116
  if (whichUserGroup === 'normalusers') {
1!
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

124
  userParamsForNewUser.roles = userParamsForNewUser.roles.sort();
1✔
125
  return usersdb.insert(userParamsForNewUser, userid)
1✔
126
    .then((couchUser) => ({
1✔
127
      // user,
128
      couchUser,
129
      whichUserGroup,
130
      // connection,
131
    }))
132
    .then(() => couchdb.db.replicate('new_corpus_activity_feed', `${user.username}-activity_feed`, {
1✔
133
      create_target: true,
134
    }))
135
    .catch((couchDBError) => {
136
      req.log.warn(`${new Date()} User activity feed replication failed.`, couchDBError);
×
137
    })
138
    .then((couchActivityFeedResponse) => {
139
      debug(`${new Date()} User activity feed successfully replicated.`, couchActivityFeedResponse);
1✔
140
      // Set up security on new user activity feed
141
      const activityDb = nano({
1✔
142
        requestDefaults: {
143
          headers: {
144
            'x-request-id': req.id || '',
1!
145
          },
146
        },
147
        url: `${couchConnectUrl}/${user.username}-activity_feed`,
148
      });
149
      return activityDb.insert({
1✔
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({
×
177
  req = {
×
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) {
×
185
    dbConn = Connection.defaultConnection(req.body.serverCode);
×
186
    dbConn.dbname = req.body.dbname;
×
187
  } else {
188
    dbConn = req.body.connection;
×
189
  }
190
  if (!req || !req.body.username || !req.body.username) {
×
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.');
×
192
    err.status = 412;
×
193
    return Promise.reject(err);
×
194
  }
195
  if (!dbConn) {
×
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;
×
202
  const requestingUser = req.body.username;
×
203
  let requestingUserIsAMemberOfCorpusTeam = false;
×
204
  if (dbConn && dbConn.domain && dbConn.domain.indexOf('iriscouch') > -1) {
×
205
    dbConn.port = '6984';
×
206
  }
207
  debug(`${new Date()} ${requestingUser} requested the team on ${dbname}`);
×
208
  const nanoforpermissions = nano({
×
209
    requestDefaults: {
210
      headers: {
211
        'x-request-id': req.id || '',
×
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');
×
222
  let whichUserGroup = 'normalusers';
×
223
  if (requestingUser.indexOf('test') > -1 || requestingUser.indexOf('anonymous') > -1) {
×
224
    whichUserGroup = 'betatesters';
×
225
  }
226
  let userroles;
227
  return usersdb.view('users', whichUserGroup)
×
228
    .then((body) => {
229
      userroles = body.rows;
×
230
      /*
231
     * Get user masks from the server
232
     */
233
      const fieldDBUsersDB = nanoforpermissions.db.use(config.usersDbConnection.dbname);
×
234
      // Put the user in the database and callback
235
      return fieldDBUsersDB.view('users', 'usermasks');
×
236
    })
237
    .then((body) => {
238
      const usermasks = body.rows;
×
239
      const usersInThisGroup = {};
×
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
       */
247
      userroles.forEach(({ key: username, value: roles }) => {
×
248
        usersInThisGroup[username] = { username, roles };
×
249
      });
250
      // Put the gravatars in for users who are in this category
251
      usermasks.forEach(({ value: { gravatar, gravatar_email: email, username } }) => {
×
252
        // if this usermask isnt in this category of users, skip them.
253
        if (!usersInThisGroup[username]) {
×
254
          return;
×
255
        }
256
        // debug(username, usersInThisGroup[username]);
257
        usersInThisGroup[username].gravatar = gravatar;
×
258
        // debug(new Date() + "  the value of this user ", usermasks[userIndex]);
259
        usersInThisGroup[username].email = email;
×
260
      });
261
      // Put the users into the list of roles and users
262
      Object.keys(usersInThisGroup).forEach((username) => {
×
263
        if (!usersInThisGroup[username]) {
×
264
          return;
×
265
        }
266
        // debug(new Date() + " Looking at " + username);
267
        let userIsOnTeam = false;
×
268
        const thisUsersMask = usersInThisGroup[username];
×
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
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
279
        rolesAndUsers.allusers.push({
×
280
          username,
281
          gravatar: thisUsersMask.gravatar,
282
        });
283

284
        thisUsersMask.roles.forEach((role) => {
×
285
          if (role.indexOf(`${dbname}_`) !== 0) {
×
286
            return;
×
287
          }
288
          userIsOnTeam = true;
×
289
          // debug(new Date() + username + " is a member of this corpus: " + role);
290
          if (username === requestingUser) {
×
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
           */
297
          const roleType = `${role.replace(`${dbname}_`, '')}s`;
×
298
          rolesAndUsers[roleType] = rolesAndUsers[roleType] || [];
×
299
          rolesAndUsers[roleType].push({
×
300
            username,
301
            gravatar: thisUsersMask.gravatar,
302
          });
303
        });
304
        if (!userIsOnTeam) {
×
305
          rolesAndUsers.notonteam.push({
×
306
            username,
307
            gravatar: thisUsersMask.gravatar,
308
          });
309
        }
310
      });
311

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

337
      debug(`Requesting user \`${requestingUser}\` is not a member of the corpus team.${dbname}`);
×
338
      const err = new Error(`Requesting user \`${requestingUser}\` is not a member of the corpus team.`);
×
339
      err.status = 401;
×
340
      err.userFriendlyErrors = ['Unauthorized, you are not a member of this corpus team.'];
×
341
      throw err;
×
342
    // });
343
    })
344
    .catch((error) => {
345
      debug(`${new Date()} Error quering userroles: ${error}`);
×
346
      const err = {
×
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;
×
352
    });
353
}
354

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

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

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

382
    return corpus.createNewCorpus({
×
383
      req,
384
      user,
385
      title: potentialnewcorpusconnection.title,
386
      connection: potentialnewcorpusconnection,
387
    })
388
      .then(({ corpusDetails, info } = {}) => {
×
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
        // }
399
        return potentialnewcorpusconnection;
×
400
      })
401
      .catch((err) => {
402
        if (err.corpusexisted) {
×
403
          return potentialnewcorpusconnection;
×
404
        }
405
        throw err;
×
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({
×
419
  user: fieldDBUser,
420
  req = {
×
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) {
1!
431
    return Promise.reject(new Error('Please provide a username 8933'));
×
432
  }
433

434
  if (process.env.INSTALABLE !== 'true' && sampleUsers.indexOf(fieldDBUser.username) > -1) {
1!
435
    return Promise.resolve({
×
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
    requestDefaults: {
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) => {
458
      if (resultuser.ok) {
1!
459
        debug(`${new Date()} No error saving a user: ${JSON.stringify(resultuser)}`);
1✔
460
        // eslint-disable-next-line no-underscore-dangle, no-param-reassign
461
        fieldDBUser._rev = resultuser.rev;
1✔
462
        return {
1✔
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 = {
×
473
        message: error.message,
474
        status: error.status || error.statusCode,
×
475
        ...error,
476
      };
477
      let message = 'Error saving a user in the database. ';
×
478
      if (err.status === 409) {
×
479
        message = 'Conflict saving user in the database. Please try again.';
×
480
      }
481
      debug(`${new Date()} Error saving a user: ${JSON.stringify(error)}`);
×
482
      debug(`${new Date()} This is the user who was not saved: ${JSON.stringify(user)}`);
×
483
      err.userFriendlyErrors = [message];
×
484
      throw err;
×
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({
×
495
  username,
496
  req = {
×
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) {
1!
507
    return Promise.reject(new Error('username is required'));
×
508
  }
509

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

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

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

549
      if (error.error === 'unauthorized') {
×
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;
×
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({
×
564
  email,
565
  optionallyRestrictToIncorrectLoginAttempts,
566
  req = {
×
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) {
×
577
    return Promise.reject(new Error('Please provide an email'));
×
578
  }
579
  let usersQuery = 'usersByEmail';
×
580
  if (optionallyRestrictToIncorrectLoginAttempts) {
×
581
    usersQuery = 'userWhoHaveTroubleLoggingIn';
×
582
  }
583
  usersQuery = `${usersQuery}?key="${email}"`;
×
584
  const usersdb = nano({
×
585
    requestDefaults: {
586
      headers: {
587
        'x-request-id': req.id || '',
×
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)
×
594
    .then((body) => {
595
      debug(`${new Date()} ${usersQuery} requested users who have this email ${email} from the server, and recieved results `);
×
596
      const users = body.rows.map((row) => row.value);
×
597
      debug(`${new Date()} users ${JSON.stringify(users)}`);
×
598
      if (users.length === 0) {
×
599
        const err = new Error(`No matching users for ${optionallyRestrictToIncorrectLoginAttempts}`);
×
600
        err.status = 401;
×
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.`];
×
602
        throw err;
×
603
      }
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)}`);
×
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;
×
622
    });
623
}
624

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

635
      let shouldEmailWelcomeToCorpusToUser = false;
×
636
      // eslint-disable-next-line no-param-reassign
637
      user.serverlogs = user.serverlogs || {};
×
638
      // eslint-disable-next-line no-param-reassign
639
      user.serverlogs.welcomeToCorpusEmails = user.serverlogs.welcomeToCorpusEmails || {};
×
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

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
       */
654
      debug(`${new Date()} Here are the user ${user.username}'s known corpora ${JSON.stringify(user.corpora)}`);
×
655
      let alreadyAdded;
656
      user.corpora.forEach((connection) => {
×
657
        if (userPermissionResult.after.length === 0) {
×
658
          // removes from all servers, TODO this might be something we should ask the user about.
659
          if (connection.dbname === newConnection.dbname) {
×
660
            // console.log('User', user.clone)
661
            user.corpora.remove(connection, 1);
×
662
          }
663
          return;
×
664
        }
665
        if (alreadyAdded) {
×
666
          return;
×
667
        }
668
        if (connection.dbname === newConnection.dbname) {
×
669
          alreadyAdded = true;
×
670
        }
671
      });
672
      debug(`${user.username}, after ${JSON.stringify(userPermissionResult)}`);
×
673
      if (userPermissionResult.after.length > 0) {
×
674
        if (alreadyAdded) {
×
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.`;
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);
699
        debug('after removed new corpus from user ', newConnection, user.corpora);
×
700
      }
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
      // });
709
      return saveUpdateUserToDatabase({ user, req })
×
710
        .then(() => {
711
          // console.log('user after save', userPermissionResult);
712
          // If the user was removed we can exit now
713
          if (userPermissionResult.after.length === 0) {
×
714
            const message = `User ${user.username} was removed from the ${
×
715
              newConnection.dbname
716
            } team.`;
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);
×
765
      const err = {
×
766
        message: error.message,
767
        status: error.status || error.statusCode,
×
768
        userFriendlyErrors: ['Username doesnt exist on this server. This is a bug.'],
769
        ...error,
770
      };
771

772
      throw err;
×
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
  // }
791
  if (role && role.indexOf('_') > -1) {
×
792
    return `${dbname}_${role.substring(role.lastIndexOf('_') + 1)}`;
×
793
  }
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({
×
801
  req = {
×
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 = {};
×
812
  // If serverCode is present, request is coming from Spreadsheet app
813
  dbConn = req.body.connection;
×
814
  if (!dbConn || !dbConn.dbname || dbConn.dbname.indexOf('-') === -1) {
×
815
    debug(dbConn);
×
816
    const err = new Error('Client didnt define the dbname to modify.');
×
817
    err.status = 412;
×
818
    err.userFriendlyErrors = ['This app has made an invalid request. Please notify its developer. missing: corpusidentifier'];
×
819
    return Promise.reject(err);
×
820
  }
821
  if (!req || !req.body.username || !req.body.username) {
×
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) {
×
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)
×
833
    && (!req.body.users[0].remove || req.body.users[0].remove.length < 1)) {
834
    const err = new Error('Client didnt define the roles to add nor remove');
×
835
    err.status = 412;
×
836
    err.userFriendlyErrors = ['This app has made an invalid request. Please notify its developer. missing: roles to add or remove'];
×
837
    return Promise.reject(err);
×
838
  }
839

840
  return corpus.isRequestingUserAnAdminOnCorpus({
×
841
    connection: dbConn,
842
    req,
843
    username: req.body.username,
844
  }).then((result) => {
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
847
    const promises = req.body.users.map((userPermissionOriginal) => {
×
848
      const userPermission = {
×
849
        ...userPermissionOriginal,
850
      };
851
      if (userPermission.add && typeof userPermission.add.map === 'function') {
×
852
        userPermission.add = userPermission.add
×
853
          // TODO unify with corpus.addRoleToUserInfo
854
          .map((role) => normalizeRolesToDBname({ dbname: dbConn.dbname, role }));
×
855
      } else {
856
        userPermission.add = [];
×
857
      }
858

859
      if (userPermission.remove && typeof userPermission.remove.map === 'function') {
×
860
        userPermission.remove = userPermission.remove
×
861
          .map((role) => normalizeRolesToDBname({ dbname: dbConn.dbname, role }));
×
862
      } else {
863
        userPermission.remove = [];
×
864
      }
865
      debug('userPermission', userPermission);
×
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) => {
872
        debug('userPermission', userPermission);
×
873
        return corpus.addRoleToUserInfo({ connection: dbConn, userPermission })
×
874
          .then(({ userPermissionResult }) => {
875
            debug('corpus.addRoleToUserInfo result', userPermissionResult);
×
876
            return addCorpusToUser({
×
877
              username: userPermission.username, userPermissionResult, newConnection: dbConn, req,
878
            });
879
          })
880
          .then(({ userPermissionResult: userPermissionFinalResult }) => userPermissionFinalResult)
×
881
          .catch((error) => {
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
            };
888
            throw err;
×
889
          });
890
      });
891

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');
×
923
      throw err;
×
924
    });
925
  // });
926
}
927

928
function verifyPassword({
×
929
  password,
930
  user,
931
} = {}) {
932
  return new Promise((resolve, reject) => {
×
933
    setTimeout(() => {
×
934
      try {
×
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)
×
940
          .then((matches) => {
941
            if (matches) {
×
942
              return resolve({ user });
×
943
            }
944

945
            const err = new Error('Username or password is invalid. Please try again.');
×
946
            err.status = 401;
×
947
            return reject(err);
×
948
          })
949
          .catch(reject);
950
      } catch (err) {
951
        reject(err);
×
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({
×
967
  oldpassword,
968
  newpassword,
969
  username,
970
  req,
971
} = {}) {
972
  if (!username) {
×
973
    const err = new Error('Please provide a username');
×
974
    err.status = 412;
×
975
    return Promise.reject(err);
×
976
  }
977
  if (!oldpassword) {
×
978
    const err = new Error('Please provide your old password');
×
979
    err.status = 412;
×
980
    return Promise.reject(err);
×
981
  }
982
  if (!newpassword) {
×
983
    const err = new Error('Please provide your new password');
×
984
    err.status = 412;
×
985
    return Promise.reject(err);
×
986
  }
987
  const safeUsernameForCouchDB = Connection.validateUsername(username);
×
988
  if (username !== safeUsernameForCouchDB.identifier) {
×
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 })
×
995
    .then(({ user }) => {
996
      debug(`${new Date()} Found user in setPassword: ${JSON.stringify(user)}`);
×
997

998
      return verifyPassword({
×
999
        user,
1000
        password: oldpassword,
1001
      });
1002
    })
1003
    // Save new password to couch too
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);
1016
        debug(`${new Date()} There was success in creating changing the couchdb password: ${JSON.stringify(res)}\n`);
×
1017
        return saveUpdateUserToDatabase({ user, req });
×
1018
      }))
1019
    .then(({ user, info }) => {
1020
      // Change the success message to be more appropriate
1021
      if (info.message === 'User details saved.') {
×
1022
        // eslint-disable-next-line no-param-reassign
1023
        info.message = 'Your password has succesfully been updated.';
×
1024
      }
1025
      return { user, info };
×
1026
    })
1027
    .catch((err) => {
1028
      debug(`${new Date()} Error setPassword  ${username} : ${JSON.stringify(err.stack)}`);
×
1029

1030
      throw err;
×
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({
×
1041
  email,
1042
  req,
1043
} = {}) {
1044
  if (!email) {
×
1045
    const err = new Error('Please provide an email.');
×
1046
    err.status = 412;
×
1047
    return Promise.reject(err);
×
1048
  }
1049
  return findByEmail({
×
1050
    email,
1051
    optionallyRestrictToIncorrectLoginAttempts: 'onlyUsersWithIncorrectLogins',
1052
    req,
1053
  })
1054
    .then(({ users }) => {
1055
      const sameTempPasswordForAllTheirUsers = emailFunctions.makeRandomPassword();
×
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
      }));
1064
      let passwordChangeResults = '';
×
1065
      const forgotPasswordResults = {
×
1066
        status_codes: '',
1067
        error: {
1068
          status: 200,
1069
          error: '',
1070
        },
1071
        info: {
1072
          message: '',
1073
        },
1074
      };
1075

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);
×
1119
      const err = {
×
1120
        message: error.message,
1121
        status: error.status || error.statusCode,
×
1122
        // userFriendlyErrors: error.userFriendlyErrors,
1123
        ...error,
1124
      };
1125
      throw err;
×
1126
    });
1127
}
1128

1129
function handleInvalidPasswordAttempt({ user, req }) {
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
1135
  user.serverlogs = user.serverlogs || {};
×
1136
  // eslint-disable-next-line no-param-reassign
1137
  user.serverlogs.incorrectPasswordAttempts = user.serverlogs.incorrectPasswordAttempts || [];
×
1138
  user.serverlogs.incorrectPasswordAttempts.push(new Date());
×
1139
  // eslint-disable-next-line no-param-reassign
1140
  user.serverlogs.incorrectPasswordEmailSentCount = user.serverlogs.incorrectPasswordEmailSentCount || 0;
×
1141
  const incorrectPasswordAttemptsCount = user.serverlogs.incorrectPasswordAttempts.length;
×
1142
  // console.log('incorrectPasswordAttempts', user.serverlogs)
1143
  const timeToSendAnEmailEveryXattempts = incorrectPasswordAttemptsCount >= 5;
×
1144
  /* Dont reset the public user or lingllama's passwords */
1145
  if (user.username === 'public' || user.username === 'lingllama') {
×
1146
    return Promise.resolve({
×
1147
      user,
1148
    });
1149
  }
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

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
   */
1177
  if (!user.email || user.email.length < 5) {
×
1178
    debug(`${new Date()}User didn't not provide a valid email, so their temporary password was not sent by email.`);
×
1179
    return saveUpdateUserToDatabase({ user, req })
×
1180
      .then(() => {
1181
        debug(`${new Date()} Email doesnt appear to be vaild. ${user.email}`);
×
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

1190
  const temporaryPassword = emailFunctions.makeRandomPassword();
×
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
        }));
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({
×
1220
  username,
1221
  password,
1222
  syncDetails,
1223
  syncUserDetails,
1224
  req = {
×
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) {
×
1235
    const err = new Error(`Username was not specified. ${username}`);
×
1236
    err.status = 412;
×
1237
    err.userFriendlyErrors = ['Please supply a username.'];
×
1238
    return Promise.reject(err);
×
1239
  }
1240

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

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

1256
  return findByUsername({ username, req })
×
1257
    .then(({ user }) => verifyPassword({ password, user })
×
1258
      .catch((error) => handleInvalidPasswordAttempt({ user, req })
×
1259
        .then(({ info }) => {
1260
          // Don't tell them its because the password is wrong.
1261
          debug(`${new Date()} Returning: Username or password is invalid. Please try again.`);
×
1262
          const err = {
×
1263
            message: error.message,
1264
            userFriendlyErrors: [info.message],
1265
            status: error.status || 401,
×
1266
            ...error,
1267
          };
1268
          throw err;
×
1269
        })))
1270
    .then(({ user }) => {
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
       */
1276
      if (syncDetails !== 'true' && syncDetails !== true) {
×
1277
        return { user };
×
1278
      }
1279
      debug(`${new Date()} Here is syncUserDetails: ${JSON.stringify(syncUserDetails)}`);
×
1280
      let userToSave;
1281
      try {
×
1282
        userToSave = new User(syncUserDetails);
×
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
      }
1287
      if (!userToSave || !userToSave.newCorpora) {
×
1288
        return { user };
×
1289
      }
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.`);
×
1291
      return createNewCorpusesIfDontExist({ user, corpora: userToSave.newCorpora, req })
×
1292
        .then((corpora) => {
1293
          // TODO this corpora is not written into the user?
1294
          debug('createNewCorpusesIfDontExist corpora', corpora);
×
1295
          // user = new User(user);
1296
          // TODO remove newCorpora?
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;
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
1332
      user.dateModified = new Date();
×
1333
      // eslint-disable-next-line no-param-reassign
1334
      user.serverlogs = user.serverlogs || {};
×
1335
      // eslint-disable-next-line no-param-reassign
1336
      user.serverlogs.successfulLogins = user.serverlogs.successfulLogins || [];
×
1337
      user.serverlogs.successfulLogins.push(new Date());
×
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
      }
1347
      return saveUpdateUserToDatabase({ user, req });
×
1348
    })
1349
    .catch((error) => {
1350
      debug('error', error);
×
1351
      // Don't tell them its because the user doesnt exist
1352
      const err = {
×
1353
        message: error.message,
1354
        status: error.status,
1355
        ...error,
1356
      };
1357
      if (err.message === `User ${username} does not exist`) {
×
1358
        err.status = 401;
×
1359
        err.userFriendlyErrors = ['Username or password is invalid. Please try again.'];
×
1360
      }
1361
      throw err;
×
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({
×
1371
  req = {
×
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') {
1!
1383
    const err = new Error('Username is the default username');
×
1384
    err.status = 412;
×
1385
    err.userFriendlyErrors = ['Please type a username instead of yourusernamegoeshere.'];
×
1386
    return Promise.reject(err);
×
1387
  }
1388
  if (!req || !req.body.username || !req.body.username) {
1!
1389
    const err = new Error('Please provide a username');
×
1390
    err.status = 412;
×
1391
    return Promise.reject(err);
×
1392
  }
1393
  if (req.body.username.length < 3) {
1!
1394
    const err = new Error(`Please choose a longer username \`${req.body.username}\` is too short.`);
×
1395
    err.status = 412;
×
1396
    return Promise.reject(err);
×
1397
  }
1398
  const safeUsernameForCouchDB = Connection.validateUsername(req.body.username);
1✔
1399
  if (req.body.username !== safeUsernameForCouchDB.identifier) {
1!
1400
    const err = new Error('username is not safe for db names');
×
1401
    err.status = 406;
×
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)`];
×
1403
    return Promise.reject(err);
×
1404
  }
1405
  // Make sure the username doesn't exist.
1406
  return findByUsername({
1✔
1407
    username: req.body.username,
1408
    req,
1409
    // req: {
1410
    //   id: `pre-${req.id}`,
1411
    //   log: req.log
1412
    // },
1413
  })
1414
    .then(() => {
1415
      const err = new Error(`Username ${req.body.username} already exists, try a different username.`);
×
1416
      err.status = err.status || err.statusCode || 409;
×
1417
      throw err;
×
1418
    })
1419
    .catch((err) => {
1420
      if (err.message !== `User ${req.body.username} does not exist`) {
1!
1421
        throw err;
×
1422
      }
1423

1424
      debug(`${new Date()} Registering new user: ${req.body.username}`);
1✔
1425
      const user = new User(req.body);
1✔
1426
      const salt = bcrypt.genSaltSync(10);
1✔
1427
      user.hash = bcrypt.hashSync(req.body.password, salt);
1✔
1428
      user.dateCreated = user.dateCreated || Date.now();
1✔
1429
      user.authServerVersionWhenCreated = authServerVersion;
1✔
1430
      user.prefs = JSON.parse(JSON.stringify(DEFAULT_USER_PREFERENCES));
1✔
1431
      // FieldDB.User is not setting the gravatar from the username
1432
      if (!user.gravatar) {
1!
1433
        user.gravatar = md5(user.username);
×
1434
      }
1435
      let { appbrand } = req.body;
1✔
1436
      if (!appbrand) {
1!
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
      }
1449
      user.appbrand = appbrand;
1✔
1450
      debug('createCouchDBUser appbrand', appbrand);
1✔
1451

1452
      if (dontCreateDBsForLearnXUsersBecauseThereAreTooManyOfThem
1!
1453
      && req.body.username.indexOf('anonymous') === 0) {
1454
        debug(`${new Date()}  delaying creation of the dbs for ${req.body.username} until they can actually use them.`);
×
1455
        // user.newCorpora = user.corpora;
1456
        return emailFunctions.emailWelcomeToTheUser({ user })
×
1457
          .then(() => {
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
           */
1462
            return saveUpdateUserToDatabase({ user, req });
×
1463
          });
1464
      }
1465

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

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

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

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