Files
freeCodeCamp/api-server/src/server/boot/settings.js
2023-10-11 11:00:50 +02:00

355 lines
9.6 KiB
JavaScript

import debug from 'debug';
import { check } from 'express-validator';
import _ from 'lodash';
import isURL from 'validator/lib/isURL';
import { isValidUsername } from '../../../../shared/utils/validate';
import { alertTypes } from '../../common/utils/flash.js';
import {
deprecatedEndpoint,
temporarilyDisabledEndpoint
} from '../utils/disabled-endpoints';
import { ifNoUser401, createValidatorErrorHandler } from '../utils/middleware';
const log = debug('fcc:boot:settings');
export default function settingsController(app) {
const api = app.loopback.Router();
const updateMyUsername = createUpdateMyUsername(app);
api.put('/update-privacy-terms', ifNoUser401, updatePrivacyTerms);
api.post('/refetch-user-completed-challenges', deprecatedEndpoint);
// Re-enable once we can handle the traffic
// api.post(
// '/update-my-current-challenge',
// ifNoUser401,
// updateMyCurrentChallengeValidators,
// createValidatorErrorHandler(alertTypes.danger),
// updateMyCurrentChallenge
// );
api.post('/update-my-current-challenge', temporarilyDisabledEndpoint);
api.put('/update-my-portfolio', ifNoUser401, updateMyPortfolio);
api.put('/update-my-theme', ifNoUser401, updateMyTheme);
api.put('/update-my-about', ifNoUser401, updateMyAbout);
api.put(
'/update-my-email',
ifNoUser401,
updateMyEmailValidators,
createValidatorErrorHandler(alertTypes.danger),
updateMyEmail
);
api.put('/update-my-profileui', ifNoUser401, updateMyProfileUI);
api.put('/update-my-username', ifNoUser401, updateMyUsername);
api.put('/update-user-flag', ifNoUser401, updateUserFlag);
api.put('/update-my-socials', ifNoUser401, updateMySocials);
api.put(
'/update-my-keyboard-shortcuts',
ifNoUser401,
updateMyKeyboardShortcuts
);
api.put('/update-my-honesty', ifNoUser401, updateMyHonesty);
api.put('/update-my-quincy-email', ifNoUser401, updateMyQuincyEmail);
api.put('/update-my-classroom-mode', ifNoUser401, updateMyClassroomMode);
app.use(api);
}
const standardErrorMessage = {
type: 'danger',
message: 'flash.wrong-updating'
};
const createStandardHandler = (req, res, next, alertMessage) => err => {
if (err) {
res.status(500).json(standardErrorMessage);
return next(err);
}
return res.status(200).json({ type: 'success', message: alertMessage });
};
const createUpdateUserProperties = (buildUpdate, validate, successMessage) => {
return (req, res, next) => {
const { user, body } = req;
const update = buildUpdate(body);
if (validate(update)) {
user.updateAttributes(
update,
createStandardHandler(req, res, next, successMessage)
);
} else {
handleInvalidUpdate(res);
}
};
};
const updateMyEmailValidators = [
check('email').isEmail().withMessage('Email format is invalid.')
];
function updateMyEmail(req, res, next) {
const {
user,
body: { email }
} = req;
return user
.requestUpdateEmail(email)
.subscribe(message => res.json({ message }), next);
}
// Re-enable once we can handle the traffic
// const updateMyCurrentChallengeValidators = [
// check('currentChallengeId')
// .isMongoId()
// .withMessage('currentChallengeId is not a valid challenge ID')
// ];
// Re-enable once we can handle the traffic
// function updateMyCurrentChallenge(req, res, next) {
// const {
// user,
// body: { currentChallengeId }
// } = req;
// return user.updateAttribute(
// 'currentChallengeId',
// currentChallengeId,
// (err, updatedUser) => {
// if (err) {
// return next(err);
// }
// const { currentChallengeId } = updatedUser;
// return res.status(200).json(currentChallengeId);
// }
// );
// }
function updateMyPortfolio(...args) {
const portfolioKeys = ['id', 'title', 'description', 'url', 'image'];
const buildUpdate = body => {
const portfolio = body?.portfolio?.map(elem => _.pick(elem, portfolioKeys));
return { portfolio };
};
const validate = ({ portfolio }) => portfolio?.every(isPortfolioElement);
const isPortfolioElement = elem =>
Object.values(elem).every(val => typeof val == 'string');
createUpdateUserProperties(
buildUpdate,
validate,
'flash.portfolio-item-updated'
)(...args);
}
// This API is responsible for what campers decide to make public in their profile, and what is private.
function updateMyProfileUI(req, res, next) {
const {
user,
body: { profileUI }
} = req;
user.updateAttribute(
'profileUI',
profileUI,
createStandardHandler(req, res, next, 'flash.privacy-updated')
);
}
function updateMyAbout(req, res, next) {
const {
user,
body: { name, location, about, picture }
} = req;
log(name, location, picture, about);
// prevent dataurls from being stored
const update = isURL(picture, { require_protocol: true })
? { name, location, about, picture }
: { name, location, about };
return user.updateAttributes(
update,
createStandardHandler(req, res, next, 'flash.updated-about-me')
);
}
function createUpdateMyUsername(app) {
const { User } = app.models;
return async function updateMyUsername(req, res, next) {
const { user, body } = req;
const usernameDisplay = body.username.trim();
const username = usernameDisplay.toLowerCase();
if (
username === user.username &&
user.usernameDisplay &&
usernameDisplay === user.usernameDisplay
) {
return res.json({
type: 'info',
message: 'flash.username-used'
});
}
const validation = isValidUsername(username);
if (!validation.valid) {
return res.json({
type: 'info',
message: `Username ${username} ${validation.error}`
});
}
const exists =
username === user.username ? false : await User.doesExist(username);
if (exists) {
return res.json({
type: 'info',
message: 'flash.username-taken'
});
}
return user.updateAttributes({ username, usernameDisplay }, err => {
if (err) {
res.status(500).json(standardErrorMessage);
return next(err);
}
return res.status(200).json({
type: 'success',
message: `flash.username-updated`,
variables: { username: usernameDisplay }
});
});
};
}
const updatePrivacyTerms = (req, res, next) => {
const {
user,
body: { quincyEmails }
} = req;
const update = {
acceptedPrivacyTerms: true,
sendQuincyEmail: !!quincyEmails
};
return user.updateAttributes(
update,
createStandardHandler(req, res, next, 'flash.privacy-updated')
);
};
const allowedSocialsAndDomains = {
githubProfile: 'github.com',
linkedin: 'linkedin.com',
twitter: 'twitter.com',
website: ''
};
const socialVals = Object.keys(allowedSocialsAndDomains);
export function updateMySocials(...args) {
const buildUpdate = body => _.pick(body, socialVals);
const validate = update => {
// Socials should point to their respective domains
// or be empty strings
return Object.keys(update).every(key => {
const val = update[key];
if (val === '') {
return true;
}
if (key === 'website') {
return isURL(val, { require_protocol: true });
}
const domain = allowedSocialsAndDomains[key];
try {
const url = new URL(val);
const topDomain = url.hostname.split('.').slice(-2);
if (topDomain.length === 2) {
return topDomain.join('.') === domain;
}
return false;
} catch (e) {
return false;
}
});
};
createUpdateUserProperties(
buildUpdate,
validate,
'flash.updated-socials'
)(...args);
}
function updateMyTheme(...args) {
const buildUpdate = body => _.pick(body, 'theme');
const validate = ({ theme }) => theme == 'default' || theme == 'night';
createUpdateUserProperties(
buildUpdate,
validate,
'flash.updated-themes'
)(...args);
}
function updateMyKeyboardShortcuts(...args) {
const buildUpdate = body => _.pick(body, 'keyboardShortcuts');
const validate = ({ keyboardShortcuts }) =>
typeof keyboardShortcuts === 'boolean';
createUpdateUserProperties(
buildUpdate,
validate,
'flash.keyboard-shortcut-updated'
)(...args);
}
function updateMyHonesty(...args) {
const buildUpdate = body => _.pick(body, 'isHonest');
const validate = ({ isHonest }) => isHonest === true;
createUpdateUserProperties(
buildUpdate,
validate,
'buttons.accepted-honesty'
)(...args);
}
function updateMyQuincyEmail(...args) {
const buildUpdate = body => _.pick(body, 'sendQuincyEmail');
const validate = ({ sendQuincyEmail }) =>
typeof sendQuincyEmail === 'boolean';
createUpdateUserProperties(
buildUpdate,
validate,
'flash.subscribe-to-quincy-updated'
)(...args);
}
export function updateMyClassroomMode(...args) {
const buildUpdate = body => _.pick(body, 'isClassroomAccount');
const validate = ({ isClassroomAccount }) =>
typeof isClassroomAccount === 'boolean';
createUpdateUserProperties(
buildUpdate,
validate,
'flash.classroom-mode-updated'
)(...args);
}
function handleInvalidUpdate(res) {
res.status(403).json({
type: 'danger',
message: 'flash.wrong-updating'
});
}
function updateUserFlag(req, res, next) {
const { user, body: update } = req;
const allowedKeys = ['githubProfile', 'linkedin', 'twitter', 'website'];
if (Object.keys(update).every(key => allowedKeys.includes(key))) {
return user.updateAttributes(
update,
createStandardHandler(req, res, next, 'flash.updated-socials')
);
}
return res.status(403).json({
type: 'danger',
message: 'flash.invalid-update-flag'
});
}