3 var env = require('gitter-web-env');
4 var winston = env.logger;
5 var assert = require('assert');
6 var _ = require('lodash');
7 var Promise = require('bluebird');
8 var persistence = require('gitter-web-persistence');
9 var uriLookupService = require('gitter-web-uri-resolver/lib/uri-lookup-service');
10 var mongooseUtils = require('gitter-web-persistence-utils/lib/mongoose-utils');
11 var StatusError = require('statuserror');
13 async function validateUsername(username) {
14 // If the username is reserved, then you can't have it
15 const reservedUsernameEntry = await persistence.ReservedUsername.findOne({
16 lcUsername: username.toLowerCase()
18 if (reservedUsernameEntry) {
19 throw new StatusError(
21 'You are not allowed to create a user with that username (reserved)'
25 // Deal with spammer situation of 25 Sep 2016
27 /^[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}$/.test(
31 winston.info('Rejecting spam account', {
34 throw new StatusError(403, 'You are not allowed to create a user with that username');
38 /** FIXME: the insert fields should simply extend from options or a key in options.
40 * @return the promise of a new user
42 function newUser(options) {
43 var githubId = options.githubId;
45 assert(githubId, 'githubId required');
46 assert(options.username, 'username required');
50 githubUserToken: options.githubUserToken,
51 githubToken: options.githubToken,
52 githubScopes: options.githubScopes,
53 gravatarImageUrl: options.gravatarImageUrl,
54 gravatarVersion: options.gravatarVersion,
55 username: options.username,
56 invitedByUser: options.invitedByUser,
57 displayName: options.displayName,
61 if (options.emails && options.emails.length) {
62 insertFields.emails = options.emails.map(email => email.toLowerCase());
65 // Remove undefined fields
66 Object.keys(insertFields).forEach(function(k) {
67 if (insertFields[k] === undefined) {
68 delete insertFields[k];
75 { githubId: githubId },
77 $setOnInsert: insertFields
80 .spread(function(user /*, updateExisting*/) {
81 //if(raw.updatedExisting) return user;
83 // New record was inserted
84 //return emailAddressService(user)
85 // .then(function(email) {
86 // stats.userUpdate(_.extend({ email: email }, user.toJSON()));
92 .then(function(user) {
93 // Reserve the URI for the user so that we don't need to figure it out
94 // manually later (which will involve dodgy calls to github)
95 return uriLookupService.reserveUriForUsername(user._id, user.username).thenReturn(user);
99 function sanitiseUserSearchTerm(term) {
100 // remove non username chars
103 .replace(/[^0-9a-z\-]/gi, '')
105 .replace(/\-/gi, '\\-')
110 findOrCreateUserForGithubId: async function(options, callback) {
111 winston.info('Locating or creating user', options);
113 await validateUsername(options.username);
116 .findByGithubId(options.githubId)
117 .then(function(user) {
118 if (user) return user;
120 return newUser(options);
126 * Add the user if one doesn't exist for this identity and set the data for
127 * that provider for the user whether the user is new or not.
128 * @return promise of [user, isNewIdentity]
130 findOrCreateUserForProvider: async function(userData, identityData) {
131 winston.info('Locating or creating user', {
133 identityData: identityData
136 // This is not for GitHub. Only for newer providers. At least until we
137 // finally migrate all the github data one day.
138 assert.notEqual(identityData.provider, 'github');
140 await validateUsername(userData.username);
142 // TODO: should we assert all the required user and identity fields?
147 provider: identityData.provider,
148 providerKey: identityData.providerKey
153 const userInsertData = _.extend(
157 provider: identityData.provider,
158 providerKey: identityData.providerKey
165 const [user, isExistingUser] = await mongooseUtils.upsert(persistence.User, userQuery, {
166 $setOnInsert: userInsertData
168 if (user && user.state === 'DISABLED') {
169 throw new StatusError(403, 'Account temporarily disabled. Please contact support@gitter.im');
172 const isNewUser = !isExistingUser;
173 const identityQuery = {
174 provider: identityData.provider,
178 const identitySetData = _.extend(
185 await mongooseUtils.upsert(persistence.Identity, identityQuery, {
186 // NOTE: set the identity fields regardless, because the tokens and
187 // things could be newer than what we have if this is a login and
189 $set: identitySetData
191 await uriLookupService.reserveUriForUsername(user._id, user.username);
192 await this.unremoveUser(user);
193 return [user, isNewUser];
196 findById: function(id, callback) {
197 return persistence.User.findById(id)
203 * Returns a hash of booleans if the given usernames exist in gitter
205 githubUsersExists: function(usernames, callback) {
206 return persistence.User.find(
207 { username: { $in: usernames } },
208 { username: 1, _id: 0 },
212 .then(function(results) {
213 return results.reduce(function(memo, index) {
214 memo[index.username] = true;
221 findByGithubId: function(githubId, callback) {
222 return persistence.User.findOne({ githubId: githubId })
227 findByGithubIdOrUsername: function(githubId, username, callback) {
228 return persistence.User.findOne({ $or: [{ githubId: githubId }, { username: username }] })
233 findByEmail: function(email, callback) {
234 return this.findAllByEmail(email)
241 findAllByEmail: async function(email) {
242 const identity = await persistence.Identity.findOne({ email: email }).exec();
244 let usersFromIdentity = [];
246 usersFromIdentity = await persistence.User.find({
249 provider: identity.provider,
250 providerKey: identity.providerKey
256 const usersFromEmail = await persistence.User.find({
257 emails: email.toLowerCase()
260 // Remove the duplicates
261 const userMap = usersFromEmail.concat(usersFromIdentity).reduce(function(memo, user) {
262 memo[user.id] = user;
266 // Using Promise.resolve to return a Bluebird flavor promise for downstream code that uses it
267 return Promise.resolve(Object.values(userMap));
270 findByEmailsIndexed: function(emails, callback) {
271 emails = emails.map(function(email) {
272 return email.toLowerCase();
275 return persistence.User.find({ $or: [{ email: { $in: emails } }, { emails: { $in: emails } }] })
277 .then(function(users) {
278 return users.reduce(function(memo, user) {
279 memo[user.email] = user;
281 user.emails.forEach(function(email) {
291 findByUsername: function(username, callback) {
292 return persistence.User.findOne({ username: username })
297 findByIds: function(ids, { read } = {}) {
298 return mongooseUtils.findByIds(persistence.User, ids, { read });
301 findByIdsLean: function(ids, select) {
302 return mongooseUtils.findByIdsLean(persistence.User, ids, select);
305 findByIdsAndSearchTerm: function(ids, searchTerm, limit, callback) {
306 if (!ids || !ids.length || !searchTerm || !searchTerm.length) {
307 return Promise.resolve([]).nodeify(callback);
310 var searchPattern = '^' + sanitiseUserSearchTerm(searchTerm);
311 return persistence.User.find({
314 { username: { $regex: searchPattern, $options: 'i' } },
315 { displayName: { $regex: searchPattern, $options: 'i' } }
323 findByUsernames: function(usernames, callback) {
324 if (!usernames || !usernames.length) return Promise.resolve([]).nodeify(callback);
326 return persistence.User.where('username')
332 findByLogin: function(login, callback) {
333 var byEmail = login.indexOf('@') >= 0;
334 var find = byEmail ? userService.findByEmail(login) : userService.findByUsername(login);
337 .then(function(user) {
344 * Find the username of a single user
345 * @return promise of a username or undefined if user or username does not exist
347 findUsernameForUserId: function(userId) {
348 return persistence.User.findOne({ _id: userId }, 'username')
350 .then(function(user) {
351 return user && user.username;
355 deleteAllUsedInvitesForUser: function(user) {
356 persistence.Invite.remove({ userId: user.id, status: 'USED' });
359 destroyTokensForUserId: function(userId) {
360 return persistence.User.update(
362 { $set: { githubToken: null, githubScopes: {}, githubUserToken: null } }
366 /* Update the timezone information for a user */
367 updateTzInfo: function(userId, timezoneInfo) {
370 function setUnset(key, value) {
372 if (!update.$set) update.$set = {};
373 update.$set['tz.' + key] = value;
375 if (!update.$unset) update.$unset = {};
376 update.$unset['tz.' + key] = true;
380 setUnset('offset', timezoneInfo.offset);
381 setUnset('abbr', timezoneInfo.abbr);
382 setUnset('iana', timezoneInfo.iana);
384 return persistence.User.update({ _id: userId }, update).exec();
387 reserveUsername: async function(username) {
390 return mongooseUtils.upsert(
391 persistence.ReservedUsername,
393 lcUsername: username.toLowerCase()
398 lcUsername: username.toLowerCase()
404 unreserveUsername: async function(username) {
405 return persistence.ReservedUsername.remove({
406 lcUsername: username.toLowerCase()
410 hellbanUser: async function(userId) {
411 const username = await this.findUsernameForUserId(userId);
413 // Reserve their username to make the ban permanent
414 // so when the bad actor tries to delete/ghost their account and then come back,
415 // it doesn't allow them to create their user again.
416 await this.reserveUsername(username);
418 return persistence.User.update(
428 unhellbanUser: async function(userId) {
429 const username = await this.findUsernameForUserId(userId);
431 await this.unreserveUsername(username);
433 return persistence.User.update(
443 // This removes the state of the user when its value is 'REMOVED'.
444 // This is typically called when the user just logged in, and is useful after
445 // the user deleted their account.
446 unremoveUser: async user => {
447 if (user.state === 'REMOVED') {
448 user.state = undefined;
454 module.exports = userService;