mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-06-16 21:06:35 +08:00
871 lines
23 KiB
JavaScript
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`, |