diff --git a/api-server/common/models/user.js b/api-server/common/models/user.js index 7ff1db25c0..7db7b13e78 100644 --- a/api-server/common/models/user.js +++ b/api-server/common/models/user.js @@ -127,6 +127,9 @@ function nextTick(fn) { const getRandomNumber = () => Math.random(); function populateRequiredFields(user) { + // by default, the displayUsername will have + // the same value as the username + user.displayUsername = user.username; user.username = user.username.trim().toLowerCase(); user.email = typeof user.email === 'string' @@ -347,6 +350,7 @@ export default function(User) { if (!username && (!email || !isEmail(email))) { return Promise.resolve(false); } + username = username.toLowerCase(); log('checking existence'); // check to see if username is on blacklist @@ -395,6 +399,7 @@ export default function(User) { cb(null, {}); }); } + username = username.toLowerCase(); return User.findOne({ where: { username } }, (err, user) => { if (err) { return cb(err); @@ -724,13 +729,13 @@ export default function(User) { User.prototype.updateMyUsername = function updateMyUsername(newUsername) { return Observable.defer(() => { - const isOwnUsername = isTheSame(newUsername, this.username); + const isOwnUsername = isTheSame(newUsername.toLowerCase(), this.username); if (isOwnUsername) { return Observable.of(dedent` ${newUsername} is already associated with this account. `); } - return Observable.fromPromise(User.doesExist(newUsername)); + return Observable.fromPromise(User.doesExist(newUsername.toLowerCase())); }).flatMap(boolOrMessage => { if (typeof boolOrMessage === 'string') { return Observable.of(boolOrMessage); @@ -741,14 +746,20 @@ export default function(User) { `); } - const usernameUpdate = new Promise((resolve, reject) => - this.updateAttribute('username', newUsername, err => { - if (err) { - return reject(err); + const usernameUpdate = new Promise((resolve, reject) => { + this.updateAttributes( + { + username: newUsername.toLowerCase(), + displayUsername: newUsername + }, + err => { + if (err) { + return reject(err); + } + return resolve(); } - return resolve(); - }) - ); + ); + }); return Observable.fromPromise(usernameUpdate).map( () => dedent` diff --git a/api-server/common/models/user.json b/api-server/common/models/user.json index fc5d1aaec4..0f7a16fd54 100644 --- a/api-server/common/models/user.json +++ b/api-server/common/models/user.json @@ -74,6 +74,10 @@ }, "require": true }, + "displayUsername": { + "type": "string", + "default":"" + }, "about": { "type": "string", "default": "" diff --git a/api-server/server/boot/settings.js b/api-server/server/boot/settings.js index 4e366391bd..9941ea06cb 100644 --- a/api-server/server/boot/settings.js +++ b/api-server/server/boot/settings.js @@ -194,7 +194,7 @@ function createUpdateMyUsername(app) { user, body: { username } } = req; - if (username === user.username) { + if (username.toLowerCase() === user.username) { return res.json({ type: 'info', message: 'Username is already associated with this account' @@ -209,7 +209,7 @@ function createUpdateMyUsername(app) { }); } - const exists = await User.doesExist(username); + const exists = await User.doesExist(username.toLowerCase()); if (exists) { return res.json({ @@ -218,16 +218,19 @@ function createUpdateMyUsername(app) { }); } - return user.updateAttribute('username', username, err => { - if (err) { - res.status(500).json(standardErrorMessage); - return next(err); + return user.updateAttributes( + { username: username.toLowerCase(), displayUsername: username }, + err => { + if (err) { + res.status(500).json(standardErrorMessage); + return next(err); + } + return res.status(200).json({ + type: 'success', + message: `We have updated your username to ${username}` + }); } - return res.status(200).json({ - type: 'success', - message: `We have updated your username to ${username}` - }); - }); + ); }; } diff --git a/api-server/server/utils/auth.js b/api-server/server/utils/auth.js index dd6d9aad0e..3960799da6 100644 --- a/api-server/server/utils/auth.js +++ b/api-server/server/utils/auth.js @@ -37,6 +37,7 @@ function createProfileAttributesFromGithub(profile) { return { name, username: username.toLowerCase(), + displayUsername: username, location, bio, website, diff --git a/api-server/server/utils/publicUserProps.js b/api-server/server/utils/publicUserProps.js index 71becb1ac3..cde2aa6e9d 100644 --- a/api-server/server/utils/publicUserProps.js +++ b/api-server/server/utils/publicUserProps.js @@ -11,6 +11,7 @@ export const publicUserProps = [ 'about', 'calendar', 'completedChallenges', + 'displayUsername', 'githubProfile', 'isApisMicroservicesCert', 'isBackEndCert', diff --git a/client/src/client-only-routes/ShowProfileOrFourOhFour.js b/client/src/client-only-routes/ShowProfileOrFourOhFour.js index b8a01477d5..d5a76f423f 100644 --- a/client/src/client-only-routes/ShowProfileOrFourOhFour.js +++ b/client/src/client-only-routes/ShowProfileOrFourOhFour.js @@ -22,6 +22,7 @@ const propTypes = { navigate: PropTypes.func.isRequired, requestedUser: PropTypes.shape({ username: PropTypes.string, + displayUsername: PropTypes.string, profileUI: PropTypes.object }), showLoading: PropTypes.bool diff --git a/client/src/client-only-routes/ShowSettings.js b/client/src/client-only-routes/ShowSettings.js index 8b4e60be3b..ef216e2252 100644 --- a/client/src/client-only-routes/ShowSettings.js +++ b/client/src/client-only-routes/ShowSettings.js @@ -48,6 +48,7 @@ const propTypes = { files: PropTypes.array }) ), + displayUsername: PropTypes.string, email: PropTypes.string, githubProfile: PropTypes.string, is2018DataVisCert: PropTypes.bool, @@ -121,6 +122,7 @@ export function ShowSettings(props) { toggleNightMode, user: { completedChallenges, + displayUsername, email, is2018DataVisCert, isApisMicroservicesCert, @@ -191,9 +193,12 @@ export function ShowSettings(props) { -

- {`Account Settings for ${username}`} -

+

{`Account Settings for ${ + displayUsername ? displayUsername : username + }`}

diff --git a/client/src/components/profile/Profile.js b/client/src/components/profile/Profile.js index 4369b8fc2f..7f1a3ef452 100644 --- a/client/src/components/profile/Profile.js +++ b/client/src/components/profile/Profile.js @@ -27,6 +27,7 @@ const propTypes = { showPortfolio: PropTypes.bool, showTimeLine: PropTypes.bool }), + displayUsername: PropTypes.string, calendar: PropTypes.object, streak: PropTypes.shape({ current: PropTypes.number, @@ -121,13 +122,15 @@ function renderProfile(user) { picture, portfolio, about, - yearsTopContributor + yearsTopContributor, + displayUsername } = user; return ( : null} {showPortfolio ? : null} {showTimeLine ? ( - + ) : null} diff --git a/client/src/components/profile/components/Camper.js b/client/src/components/profile/components/Camper.js index cb8d137ad9..23b3724575 100644 --- a/client/src/components/profile/components/Camper.js +++ b/client/src/components/profile/components/Camper.js @@ -11,6 +11,7 @@ import './camper.css'; const propTypes = { about: PropTypes.string, + displayUsername: PropTypes.string, githubProfile: PropTypes.string, isGithub: PropTypes.bool, isLinkedIn: PropTypes.bool, @@ -48,6 +49,7 @@ function joinArray(array) { function Camper({ name, username, + displayUsername, location, points, picture, @@ -74,7 +76,7 @@ function Camper({ /> ) : ( {username
-

@{username}

+

+ @{displayUsername ? displayUsername : username} +

{name &&

{name}

} {location &&

{location}

} {about &&

{about}

} diff --git a/client/src/components/profile/components/TimeLine.js b/client/src/components/profile/components/TimeLine.js index be93b87895..d967ab8c09 100644 --- a/client/src/components/profile/components/TimeLine.js +++ b/client/src/components/profile/components/TimeLine.js @@ -27,6 +27,7 @@ const propTypes = { ) }) ), + displayUsername: PropTypes.string, username: PropTypes.string }; @@ -130,6 +131,7 @@ class TimelineInner extends Component { render() { const { completedMap, + displayUsername, idToNameMap, username, sortedTimeline, @@ -171,9 +173,9 @@ class TimelineInner extends Component { > - {`${username}'s Solution to ${ - idToNameMap.get(id).challengeTitle - }`} + {`${ + displayUsername ? displayUsername : username + }'s Solution to ${idToNameMap.get(id).challengeTitle}`} diff --git a/client/src/components/settings/Username.js b/client/src/components/settings/Username.js index 4242e78cf1..b9ddf60a06 100644 --- a/client/src/components/settings/Username.js +++ b/client/src/components/settings/Username.js @@ -20,6 +20,7 @@ import FullWidthRow from '../helpers/FullWidthRow'; import { isValidUsername } from '../../../../utils/validate'; const propTypes = { + displayUsername: PropTypes.string, isValidUsername: PropTypes.bool, submitNewUsername: PropTypes.func.isRequired, username: PropTypes.string, @@ -54,6 +55,9 @@ class UsernameSettings extends Component { this.state = { isFormPristine: true, formValue: props.username, + formDisplayValue: props.displayUsername + ? props.displayUsername + : props.username, characterValidation: { valid: false, error: null }, submitClicked: false, isUserNew: tempUserRegex.test(props.username) @@ -84,21 +88,23 @@ class UsernameSettings extends Component { e.preventDefault(); const { submitNewUsername } = this.props; const { - formValue, + formDisplayValue, characterValidation: { valid } } = this.state; return this.setState({ submitClicked: true }, () => - valid ? submitNewUsername(formValue) : null + valid ? submitNewUsername(formDisplayValue) : null ); } handleChange(e) { e.preventDefault(); const { username, validateUsername } = this.props; - const newValue = e.target.value.toLowerCase(); + const newDisplayUsernameValue = e.target.value; + const newValue = newDisplayUsernameValue.toLowerCase(); return this.setState( { + formDisplayValue: newDisplayUsernameValue, formValue: newValue, isFormPristine: username === newValue, characterValidation: this.validateFormInput(newValue), @@ -160,7 +166,7 @@ class UsernameSettings extends Component { render() { const { isFormPristine, - formValue, + formDisplayValue, characterValidation: { valid, error }, submitClicked } = this.state; @@ -177,7 +183,7 @@ class UsernameSettings extends Component { diff --git a/client/src/redux/fetch-user-saga.js b/client/src/redux/fetch-user-saga.js index abed96dc3f..b8e3fa621c 100644 --- a/client/src/redux/fetch-user-saga.js +++ b/client/src/redux/fetch-user-saga.js @@ -20,7 +20,11 @@ function* fetchSessionUser() { } = yield call(getSessionUser); const appUser = user[result] || {}; yield put( - fetchUserComplete({ user: appUser, username: result, sessionMeta }) + fetchUserComplete({ + user: appUser, + username: result, + sessionMeta + }) ); } catch (e) { yield put(fetchUserError(e)); diff --git a/tools/scripts/seed/seedAuthUser.js b/tools/scripts/seed/seedAuthUser.js index 61e54c43b9..d5efa7c2b1 100644 --- a/tools/scripts/seed/seedAuthUser.js +++ b/tools/scripts/seed/seedAuthUser.js @@ -45,6 +45,7 @@ MongoClient.connect(MONGOHQ_URL, { useNewUrlParser: true }, function( isBanned: false, isCheater: false, username: 'developmentuser', + displayUsername: 'DevelopmentUser', about: '', name: 'Development User', location: '',