freeCodeCamp/api-server/common/models/user.js
2019-03-05 15:57:46 +05:30

871 lines
23 KiB
JavaScript

/**
*
* Any ref to fixCompletedChallengesItem should be removed post
* a db migration to fix all completedChallenges
*
*/
import { Observable } from 'rx';
import uuid from 'uuid/v4';
import moment from 'moment';
import dedent from 'dedent';
import debugFactory from 'debug';
import { isEmail } from 'validator';
import _ from 'lodash';
import generate from 'nanoid/generate';
import { apiLocation } from '../../../config/env';
import { fixCompletedChallengeItem } from '../utils';
import { saveUser, observeMethod } from '../../server/utils/rx.js';
import { blacklistedUsernames } from '../../server/utils/constants.js';
import { wrapHandledError } from '../../server/utils/create-handled-error.js';
import {
normaliseUserFields,
getProgress,
publicUserProps
} from '../../server/utils/publicUserProps';
import {
setAccessTokenToResponse,
removeCookies
} from '../../server/utils/getSetAccessToken';
const log = debugFactory('fcc:models:user');
const BROWNIEPOINTS_TIMEOUT = [1, 'hour'];
const nanoidCharSet =
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
const createEmailError = redirectTo =>
wrapHandledError(new Error('email format is invalid'), {
type: 'info',
message: 'Please check to make sure the email is a valid email address.',
redirectTo
});
function destroyAll(id, Model) {
return Observable.fromNodeCallback(Model.destroyAll, Model)({ userId: id });
}
function buildCompletedChallengesUpdate(completedChallenges, project) {
const key = Object.keys(project)[0];
const solutions = project[key];
const solutionKeys = Object.keys(solutions);
const currentCompletedChallenges = [
...completedChallenges.map(fixCompletedChallengeItem)
];
const currentCompletedProjects = currentCompletedChallenges.filter(({ id }) =>
solutionKeys.includes(id)
);
const now = Date.now();
const update = solutionKeys.reduce((update, currentId) => {
const indexOfCurrentId = _.findIndex(update, ({ id }) => id === currentId);
const isCurrentlyCompleted = indexOfCurrentId !== -1;
if (isCurrentlyCompleted) {
update[indexOfCurrentId] = {
..._.find(update, ({ id }) => id === currentId),
solution: solutions[currentId]
};
}
if (!isCurrentlyCompleted) {
return [
...update,
{
id: currentId,
solution: solutions[currentId],
challengeType: 3,
completedDate: now
}
];
}
return update;
}, currentCompletedProjects);
const updatedExisting = _.uniqBy(
[...update, ...currentCompletedChallenges],
'id'
);
return {
updated: updatedExisting,
isNewCompletionCount: updatedExisting.length - completedChallenges.length
};
}
function isTheSame(val1, val2) {
return val1 === val2;
}
function getAboutProfile({
username,
githubProfile: github,
progressTimestamps = [],
bio
}) {
return {
username,
github,
browniePoints: progressTimestamps.length,
bio
};
}
function nextTick(fn) {
return process.nextTick(fn);
}
const getRandomNumber = () => Math.random();
function populateRequiredFields(user) {
user.username = user.username.trim().toLowerCase();
user.email =
typeof user.email === 'string'
? user.email.trim().toLowerCase()
: user.email;
if (!user.progressTimestamps) {
user.progressTimestamps = [];
}
if (user.progressTimestamps.length === 0) {
user.progressTimestamps.push(Date.now());
}
if (!user.externalId) {
user.externalId = uuid();
}
if (!user.unsubscribeId) {
user.unsubscribeId = generate(nanoidCharSet, 20);
}
return;
}
export default function(User) {
// set salt factor for passwords
User.settings.saltWorkFactor = 5;
// set user.rand to random number
User.definition.rawProperties.rand.default = getRandomNumber;
User.definition.properties.rand.default = getRandomNumber;
// increase user accessToken ttl to 900 days
User.settings.ttl = 900 * 24 * 60 * 60 * 1000;
// username should not be in blacklist
User.validatesExclusionOf('username', {
in: blacklistedUsernames,
message: 'is taken'
});
// username should be unique
User.validatesUniquenessOf('username');
User.settings.emailVerificationRequired = false;
User.on('dataSourceAttached', () => {
User.findOne$ = Observable.fromNodeCallback(User.findOne, User);
User.count$ = Observable.fromNodeCallback(User.count, User);
User.create$ = Observable.fromNodeCallback(User.create.bind(User));
User.prototype.createAccessToken$ = Observable.fromNodeCallback(
User.prototype.createAccessToken
);
});
User.observe('before save', function(ctx) {
const beforeCreate = Observable.of(ctx)
.filter(({ isNewInstance }) => isNewInstance)
// User.create
.map(({ instance }) => instance)
.flatMap(user => {
// note(berks): we now require all new users to supply an email
// this was not always the case
if (typeof user.email !== 'string' || !isEmail(user.email)) {
throw createEmailError();
}
// assign random username to new users
user.username = 'fcc' + uuid();
populateRequiredFields(user);
return Observable.fromPromise(User.doesExist(null, user.email)).do(
exists => {
if (exists) {
throw wrapHandledError(new Error('user already exists'), {
redirectTo: `${apiLocation}/signin`,