Feat(privacy): Add granular privacy controls of public profile (#17178)

* feat(privacy): Add granular privacy controls of public profile

* feat(certs): Hide certs if showCerts is false
This commit is contained in:
Stuart Taylor
2018-05-20 04:07:41 +01:00
committed by Quincy Larson
parent a1f2fc7c5c
commit bb4bcbfb45
17 changed files with 441 additions and 95 deletions

View File

@ -20,6 +20,7 @@ export const types = createTypes([
'optoUpdatePortfolio', 'optoUpdatePortfolio',
'regresPortfolio', 'regresPortfolio',
'resetFullBlocks', 'resetFullBlocks',
'updateLocalProfileUI',
'updateMultipleUserFlags', 'updateMultipleUserFlags',
'updateTheme', 'updateTheme',
'updateUserFlag', 'updateUserFlag',
@ -56,6 +57,8 @@ export const updateUserLang = createAction(
(username, lang) => ({ username, languageTag: lang }) (username, lang) => ({ username, languageTag: lang })
); );
export const updateLocalProfileUI = createAction(types.updateLocalProfileUI);
export const resetFullBlocks = createAction(types.resetFullBlocks); export const resetFullBlocks = createAction(types.resetFullBlocks);
export const updateUserCurrentChallenge = createAction( export const updateUserCurrentChallenge = createAction(
@ -294,6 +297,23 @@ export default composeReducers(
languageTag languageTag
} }
} }
}),
[types.updateLocalProfileUI]:
(
state,
{ payload: { username, profileUI } }
) => ({
...state,
user: {
...state.user,
[username]: {
...state.user[username],
profileUI: {
...state.user[username].profileUI,
...profileUI
}
}
}
}) })
}), }),
defaultState defaultState

View File

@ -38,7 +38,20 @@ const mapStateToProps = createSelector(
userFoundSelector, userFoundSelector,
( (
isSignedIn, isSignedIn,
{ isLocked, username: requestedUsername }, {
username: requestedUsername,
profileUI: {
isLocked,
showAbout,
showCerts,
showHeatMap,
showLocation,
showName,
showPoints,
showPortfolio,
showTimeLine
} = {}
},
{ username: paramsUsername }, { username: paramsUsername },
currentUsername, currentUsername,
showLoading, showLoading,
@ -47,12 +60,20 @@ const mapStateToProps = createSelector(
isSignedIn, isSignedIn,
currentUsername, currentUsername,
isCurrentUserProfile: paramsUsername === currentUsername, isCurrentUserProfile: paramsUsername === currentUsername,
isLocked,
isUserFound, isUserFound,
fetchOtherUserCompleted: typeof isUserFound === 'boolean', fetchOtherUserCompleted: typeof isUserFound === 'boolean',
paramsUsername, paramsUsername,
requestedUsername, requestedUsername,
showLoading isLocked,
showLoading,
showAbout,
showCerts,
showHeatMap,
showLocation,
showName,
showPoints,
showPortfolio,
showTimeLine
}) })
); );
@ -71,7 +92,15 @@ const propTypes = {
isUserFound: PropTypes.bool, isUserFound: PropTypes.bool,
paramsUsername: PropTypes.string, paramsUsername: PropTypes.string,
requestedUsername: PropTypes.string, requestedUsername: PropTypes.string,
showAbout: PropTypes.bool,
showCerts: PropTypes.bool,
showHeatMap: PropTypes.bool,
showLoading: PropTypes.bool, showLoading: PropTypes.bool,
showLocation: PropTypes.bool,
showName: PropTypes.bool,
showPoints: PropTypes.bool,
showPortfolio: PropTypes.bool,
showTimeLine: PropTypes.bool,
updateTitle: PropTypes.func.isRequired updateTitle: PropTypes.func.isRequired
}; };
@ -93,7 +122,15 @@ class Profile extends Component {
isLocked, isLocked,
isUserFound, isUserFound,
isCurrentUserProfile, isCurrentUserProfile,
paramsUsername paramsUsername,
showAbout,
showLocation,
showName,
showPoints,
showHeatMap,
showCerts,
showPortfolio,
showTimeLine
} = this.props; } = this.props;
const takeMeToChallenges = ( const takeMeToChallenges = (
<a href='/challenges/current-challenge'> <a href='/challenges/current-challenge'>
@ -113,8 +150,8 @@ class Profile extends Component {
<Alert bsStyle='info'> <Alert bsStyle='info'>
<p> <p>
{ {
'In order to view their progress through the freeCodeCamp ' + 'In order to view their freeCodeCamp certiciations, ' +
'curriculum, they need to make all of thie solutions public' 'they need to make their profile public'
} }
</p> </p>
</Alert> </Alert>
@ -136,11 +173,16 @@ class Profile extends Component {
} }
return ( return (
<div> <div>
<CamperHOC /> <CamperHOC
<HeatMap /> showAbout={ showAbout }
<Certificates /> showLocation={ showLocation }
<Portfolio /> showName={ showName }
<Timeline className='timelime-container' /> showPoints={ showPoints }
/>
{ showHeatMap ? <HeatMap /> : null }
{ showCerts ? <Certificates /> : null }
{ showPortfolio ? <Portfolio /> : null }
{ showTimeLine ? <Timeline className='timelime-container' /> : null }
</div> </div>
); );
} }

View File

@ -31,6 +31,10 @@ const propTypes = {
name: PropTypes.string, name: PropTypes.string,
picture: PropTypes.string, picture: PropTypes.string,
points: PropTypes.number, points: PropTypes.number,
showAbout: PropTypes.bool,
showLocation: PropTypes.bool,
showName: PropTypes.bool,
showPoints: PropTypes.bool,
username: PropTypes.string username: PropTypes.string
}; };
@ -40,17 +44,21 @@ function CamperHOC({
location, location,
points, points,
picture, picture,
about about,
showAbout,
showLocation,
showName,
showPoints
}) { }) {
return ( return (
<div> <div>
<Camper <Camper
about={ about } about={ showAbout && about }
location={ location } location={ showLocation && location }
name={ name } name={ showName && name }
picture={ picture } picture={ picture }
points={ points } points={ showPoints && points }
username={ username } username={ username }
/> />
<hr /> <hr />

View File

@ -15,6 +15,7 @@ import EmailSettings from './components/Email-Settings.jsx';
import DangerZone from './components/DangerZone.jsx'; import DangerZone from './components/DangerZone.jsx';
import CertificationSettings from './components/Cert-Settings.jsx'; import CertificationSettings from './components/Cert-Settings.jsx';
import PortfolioSettings from './components/Portfolio-Settings.jsx'; import PortfolioSettings from './components/Portfolio-Settings.jsx';
import PrivacySettings from './components/Privacy-Settings.jsx';
import Honesty from './components/Honesty.jsx'; import Honesty from './components/Honesty.jsx';
import { import {
@ -101,6 +102,8 @@ export class Settings extends React.Component {
<h1 className='text-center'>{ `Account Settings for ${username}` }</h1> <h1 className='text-center'>{ `Account Settings for ${username}` }</h1>
<AboutSettings /> <AboutSettings />
<Spacer /> <Spacer />
<PrivacySettings />
<Spacer />
<EmailSettings /> <EmailSettings />
<Spacer /> <Spacer />
<InternetSettings /> <InternetSettings />

View File

@ -9,15 +9,12 @@ import {
} from 'react-bootstrap'; } from 'react-bootstrap';
import { FullWidthRow, Spacer } from '../../../helperComponents'; import { FullWidthRow, Spacer } from '../../../helperComponents';
import LockedSettings from './Locked-Settings.jsx';
import ThemeSettings from './ThemeSettings.jsx'; import ThemeSettings from './ThemeSettings.jsx';
import Camper from './Camper.jsx'; import Camper from './Camper.jsx';
import UsernameSettings from './UsernameSettings.jsx'; import UsernameSettings from './UsernameSettings.jsx';
import SectionHeader from './SectionHeader.jsx'; import SectionHeader from './SectionHeader.jsx';
import { userSelector, toggleNightMode } from '../../../redux'; import { userSelector, toggleNightMode } from '../../../redux';
import { import { updateUserBackend } from '../redux';
updateUserBackend
} from '../redux';
import { import {
BlockSaveButton, BlockSaveButton,
BlockSaveWrapper, BlockSaveWrapper,
@ -33,7 +30,6 @@ const mapStateToProps = createSelector(
( (
{ {
about, about,
isLocked,
location, location,
name, name,
picture, picture,
@ -45,7 +41,6 @@ const mapStateToProps = createSelector(
about, about,
currentTheme: theme, currentTheme: theme,
initialValues: { name, location, about, picture }, initialValues: { name, location, about, picture },
isLocked,
location, location,
name, name,
picture, picture,
@ -79,7 +74,6 @@ const propTypes = {
currentTheme: PropTypes.string, currentTheme: PropTypes.string,
fields: PropTypes.object, fields: PropTypes.object,
handleSubmit: PropTypes.func.isRequired, handleSubmit: PropTypes.func.isRequired,
isLocked: PropTypes.bool,
location: PropTypes.string, location: PropTypes.string,
name: PropTypes.string, name: PropTypes.string,
picture: PropTypes.string, picture: PropTypes.string,
@ -163,14 +157,11 @@ class AboutSettings extends PureComponent {
currentTheme, currentTheme,
fields: { _meta: { allPristine } }, fields: { _meta: { allPristine } },
handleSubmit, handleSubmit,
isLocked,
toggleNightMode, toggleNightMode,
updateUserBackend,
username username
} = this.props; } = this.props;
const { view } = this.state; const { view } = this.state;
const toggleIsLocked = () => updateUserBackend({ isLocked: !isLocked });
const toggleTheme = () => toggleNightMode(username, currentTheme); const toggleTheme = () => toggleNightMode(username, currentTheme);
return ( return (
<div className='about-settings'> <div className='about-settings'>
@ -205,12 +196,6 @@ class AboutSettings extends PureComponent {
</form> </form>
</FullWidthRow> </FullWidthRow>
<Spacer /> <Spacer />
<FullWidthRow>
<LockedSettings
isLocked={ isLocked }
toggleIsLocked={ toggleIsLocked }
/>
</FullWidthRow>
<FullWidthRow> <FullWidthRow>
<ThemeSettings <ThemeSettings
currentTheme={ currentTheme } currentTheme={ currentTheme }

View File

@ -42,9 +42,13 @@ function Camper({
{ name && <p className='text-center name'>{ name }</p> } { name && <p className='text-center name'>{ name }</p> }
{ location && <p className='text-center location'>{ location }</p> } { location && <p className='text-center location'>{ location }</p> }
{ about && <p className='bio text-center'>{ about }</p> } { about && <p className='bio text-center'>{ about }</p> }
{
typeof points === 'number' ? (
<p className='text-center points'> <p className='text-center points'>
{ `${points} ${pluralise('point', points > 1)}` } { `${points} ${pluralise('point', points > 1)}` }
</p> </p>
) : null
}
<br/> <br/>
</div> </div>
); );

View File

@ -1,42 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import {
Row,
Col,
ControlLabel
} from 'react-bootstrap';
import TB from '../Toggle-Button';
const propTypes = {
isLocked: PropTypes.bool,
toggleIsLocked: PropTypes.func.isRequired
};
export default function LockSettings({ isLocked, toggleIsLocked }) {
return (
<Row className='inline-form'>
<Col sm={ 8 } xs={ 12 }>
<ControlLabel htmlFor='isLocked'>
<p>
<strong>
Make all of my solutions private
<br />
<em>(this disables your certificates)</em>
</strong>
</p>
</ControlLabel>
</Col>
<Col sm={ 4 } xs={ 12 }>
<TB
name='isLocked'
onChange={ toggleIsLocked }
value={ isLocked }
/>
</Col>
</Row>
);
}
LockSettings.displayName = 'LockSettings';
LockSettings.propTypes = propTypes;

View File

@ -0,0 +1,126 @@
import React from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { updateMyProfileUI } from '../redux';
import { userSelector } from '../../../redux';
import { FullWidthRow } from '../../../helperComponents';
import SectionHeader from './SectionHeader.jsx';
import ToggleSetting from './ToggleSetting.jsx';
const mapStateToProps = createSelector(
userSelector,
({
profileUI = {}
}) => ({
...profileUI
})
);
const mapDispatchToProps = dispatch =>
bindActionCreators({ updateMyProfileUI }, dispatch);
const propTypes = {
isLocked: PropTypes.bool,
showAbout: PropTypes.bool,
showCerts: PropTypes.bool,
showHeatMap: PropTypes.bool,
showLocation: PropTypes.bool,
showName: PropTypes.bool,
showPoints: PropTypes.bool,
showPortfolio: PropTypes.bool,
showTimeLine: PropTypes.bool,
updateMyProfileUI: PropTypes.func.isRequired
};
function PrivacySettings(props) {
const {
isLocked,
showAbout,
showCerts,
showHeatMap,
showLocation,
showName,
showPoints,
showPortfolio,
showTimeLine,
updateMyProfileUI
} = props;
const toggleFlag = flag =>
() => updateMyProfileUI({ profileUI: { [flag]: !props[flag] } });
return (
<div className='privacy-settings'>
<SectionHeader>Privacy Settings</SectionHeader>
<FullWidthRow>
<p>
The settings in this section enable you to control what is show on{' '}
your freeCodeCamp public profile.
</p>
<ToggleSetting
action='Make my profile completely private'
explain='Your certifications will be disabled'
flag={ isLocked }
flagName='isLocked'
toggleFlag={ toggleFlag('isLocked') }
/>
<ToggleSetting
action='Make my name completely private'
flag={ !showName }
flagName='name'
toggleFlag={ toggleFlag('showName') }
/>
<ToggleSetting
action='Make my location completely private'
flag={ !showLocation }
flagName='showLocation'
toggleFlag={ toggleFlag('showLocation') }
/>
<ToggleSetting
action='Make my "about me" completely private'
flag={ !showAbout }
flagName='showAbout'
toggleFlag={ toggleFlag('showAbout') }
/>
<ToggleSetting
action='Make my points completely private'
flag={ !showPoints }
flagName='showPoints'
toggleFlag={ toggleFlag('showPoints') }
/>
<ToggleSetting
action='Make my heat map completely private'
flag={ !showHeatMap }
flagName='showHeatMap'
toggleFlag={ toggleFlag('showHeatMap') }
/>
<ToggleSetting
action='Make my certifications completely private'
explain='Your certifications will be disabled'
flag={ !showCerts }
flagName='showCerts'
toggleFlag={ toggleFlag('showCerts') }
/>
<ToggleSetting
action='Make my portfolio completely private'
flag={ !showPortfolio }
flagName='showPortfolio'
toggleFlag={ toggleFlag('showPortfolio') }
/>
<ToggleSetting
action='Make my time line completely private'
flag={ !showTimeLine }
flagName='showTimeLine'
toggleFlag={ toggleFlag('showTimeLine') }
/>
</FullWidthRow>
</div>
);
}
PrivacySettings.displayName = 'PrivacySettings';
PrivacySettings.propTypes = propTypes;
export default connect(mapStateToProps, mapDispatchToProps)(PrivacySettings);

View File

@ -0,0 +1,53 @@
import PropTypes from 'prop-types';
import React from 'react';
import {
Row,
Col,
ControlLabel
} from 'react-bootstrap';
import TB from '../Toggle-Button';
const propTypes = {
action: PropTypes.string.isRequired,
explain: PropTypes.string,
flag: PropTypes.bool.isRequired,
flagName: PropTypes.string.isRequired,
toggleFlag: PropTypes.func.isRequired
};
export default function ToggleSetting({
action,
explain,
flag,
flagName,
toggleFlag
}) {
return (
<Row className='inline-form'>
<Col sm={ 8 } xs={ 12 }>
<ControlLabel htmlFor={ flagName }>
<p>
<strong>
{ action }
</strong>
<br />
{
explain ? <em>{explain}</em> : null
}
</p>
</ControlLabel>
</Col>
<Col sm={ 4 } xs={ 12 }>
<TB
name={ flagName }
onChange={ toggleFlag }
value={ flag }
/>
</Col>
</Row>
);
}
ToggleSetting.displayName = 'ToggleSetting';
ToggleSetting.propTypes = propTypes;

View File

@ -34,6 +34,7 @@ export const types = createTypes([
createAsyncTypes('updateUserBackend'), createAsyncTypes('updateUserBackend'),
createAsyncTypes('deletePortfolio'), createAsyncTypes('deletePortfolio'),
createAsyncTypes('updateMyPortfolio'), createAsyncTypes('updateMyPortfolio'),
createAsyncTypes('updateMyProfileUI'),
'updateNewUsernameValidity', 'updateNewUsernameValidity',
createAsyncTypes('validateUsername'), createAsyncTypes('validateUsername'),
createAsyncTypes('refetchCompletedChallenges'), createAsyncTypes('refetchCompletedChallenges'),
@ -85,6 +86,14 @@ export const updateMyPortfolioError = createAction(
export const deletePortfolio = createAction(types.deletePortfolio.start); export const deletePortfolio = createAction(types.deletePortfolio.start);
export const deletePortfolioError = createAction(types.deletePortfolio.error); export const deletePortfolioError = createAction(types.deletePortfolio.error);
export const updateMyProfileUI = createAction(types.updateMyProfileUI.start);
export const updateMyProfileUIComplete = createAction(
types.updateMyProfileUI.complete
);
export const updateMyProfileUIError = createAction(
types.updateMyProfileUI.error
);
export const resetProgress = createAction(types.resetProgress.start); export const resetProgress = createAction(types.resetProgress.start);
export const resetProgressComplete = createAction(types.resetProgress.complete); export const resetProgressComplete = createAction(types.resetProgress.complete);
export const resetProgressError = createAction( export const resetProgressError = createAction(

View File

@ -5,7 +5,8 @@ import {
types, types,
refetchCompletedChallenges, refetchCompletedChallenges,
updateUserBackendComplete, updateUserBackendComplete,
updateMyPortfolioComplete updateMyPortfolioComplete,
updateMyProfileUIComplete
} from './'; } from './';
import { makeToast } from '../../../Toasts/redux'; import { makeToast } from '../../../Toasts/redux';
import { import {
@ -18,7 +19,8 @@ import {
updateUserEmail, updateUserEmail,
updateMultipleUserFlags, updateMultipleUserFlags,
regresPortfolio, regresPortfolio,
optoUpdatePortfolio optoUpdatePortfolio,
updateLocalProfileUI
} from '../../../entities'; } from '../../../entities';
import { postJSON$ } from '../../../../utils/ajax-stream'; import { postJSON$ } from '../../../../utils/ajax-stream';
@ -188,9 +190,41 @@ function updateUserEmailEpic(actions, { getState }) {
}); });
} }
function updateMyProfileUIEpic(action$, { getState }) {
const toggle = action$::ofType(types.updateMyProfileUI.start);
const server = toggle.flatMap(({payload: { profileUI }}) => {
const state = getState();
const { csrfToken: _csrf } = state.app;
const username = usernameSelector(state);
const oldUI = { ...userSelector(state).profileUI };
return postJSON$('/update-my-profile-ui', { _csrf, profileUI })
.map(updateMyProfileUIComplete)
.catch(
doActionOnError(
() => Observable.of(
makeToast({
message:
'Something went wrong saving your privacy settings, ' +
'please try again.'
}),
updateLocalProfileUI({username, profileUI: oldUI })
)
)
);
});
const optimistic = toggle.flatMap(({payload: { profileUI }}) => {
const username = usernameSelector(getState());
return Observable.of(updateLocalProfileUI({username, profileUI}));
});
return Observable.merge(server, optimistic);
}
export default combineEpics( export default combineEpics(
backendUserUpdateEpic, backendUserUpdateEpic,
refetchCompletedChallengesEpic, refetchCompletedChallengesEpic,
updateMyPortfolioEpic, updateMyPortfolioEpic,
updateUserEmailEpic updateUserEmailEpic,
updateMyProfileUIEpic
); );

View File

@ -63,6 +63,24 @@
} }
} }
.privacy-settings {
.inline-form {
display: flex;
align-items: center;
.btn-group > label {
margin: 10px 0;
}
}
label {
em {
font-weight: 400;
}
}
}
.@{ns}-email-container { .@{ns}-email-container {
.below(sm, { .below(sm, {
text-align: center; text-align: center;

View File

@ -747,6 +747,22 @@ module.exports = function(User) {
`); `);
}; };
User.prototype.updateMyProfileUI = function updateMyProfileUI(profileUI) {
const oldUI = { ...this.profileUI };
const update = {
profileUI: {
...oldUI,
...profileUI
}
};
return this.update$(update)
.do(() => Object.assign(this, update))
.map(() => dedent`
Your privacy settings have been updated.
`);
};
User.prototype.updateMyUsername = function updateMyUsername(newUsername) { User.prototype.updateMyUsername = function updateMyUsername(newUsername) {
return Observable.defer( return Observable.defer(
() => { () => {

View File

@ -74,10 +74,6 @@
}, },
"require": true "require": true
}, },
"bio": {
"type": "string",
"default": ""
},
"about": { "about": {
"type": "string", "type": "string",
"default": "" "default": ""
@ -107,11 +103,6 @@
"type": "boolean", "type": "boolean",
"default": true "default": true
}, },
"isLocked": {
"type": "boolean",
"description": "Campers profile does not show challenges/certificates to the public",
"default": false
},
"currentChallengeId": { "currentChallengeId": {
"type": "string", "type": "string",
"description": "The challenge last visited by the user", "description": "The challenge last visited by the user",
@ -210,6 +201,55 @@
"type": "string", "type": "string",
"default": "default" "default": "default"
}, },
"profileUI": {
"type": {
"isLocked": {
"type": "boolean",
"description": "Campers profile shows only their username and avatar to the public",
"default": true
},
"showAbout": {
"type": "boolean",
"description": "For granular control of what is shown to the public",
"default": false
},
"showCerts": {
"type": "boolean",
"description": "For granular control of what is shown to the public",
"default": false
},
"showHeatMap": {
"type": "boolean",
"description": "For granular control of what is shown to the public",
"default": false
},
"showLocation": {
"type": "boolean",
"description": "For granular control of what is shown to the public",
"default": false
},
"showName": {
"type": "boolean",
"description": "For granular control of what is shown to the public",
"default": false
},
"showPoints": {
"type": "boolean",
"description": "For granular control of what is shown to the public",
"default": false
},
"showPortfolio": {
"type": "boolean",
"description": "For granular control of what is shown to the public",
"default": false
},
"showTimeLine": {
"type": "boolean",
"description": "For granular control of what is shown to the public",
"default": false
}
}
},
"badges": { "badges": {
"type": { "type": {
"coreTeam": { "coreTeam": {

View File

@ -308,7 +308,6 @@ export default function certificate(app) {
username, username,
{ {
isCheater: true, isCheater: true,
isLocked: true,
isFrontEndCert: true, isFrontEndCert: true,
isBackEndCert: true, isBackEndCert: true,
isFullStackCert: true, isFullStackCert: true,
@ -322,11 +321,13 @@ export default function certificate(app) {
isHonest: true, isHonest: true,
username: true, username: true,
name: true, name: true,
completedChallenges: true completedChallenges: true,
profileUI: true
} }
) )
.subscribe( .subscribe(
user => { user => {
const { isLocked, showCerts } = user.profileUI;
const profile = `/portfolio/${user.username}`; const profile = `/portfolio/${user.username}`;
if (!user) { if (!user) {
req.flash( req.flash(
@ -341,7 +342,7 @@ export default function certificate(app) {
'danger', 'danger',
dedent` dedent`
This user needs to add their name to their account This user needs to add their name to their account
in order for others to be able to view their certificate. in order for others to be able to view their certification.
` `
); );
return res.redirect(profile); return res.redirect(profile);
@ -351,13 +352,25 @@ export default function certificate(app) {
return res.redirect(profile); return res.redirect(profile);
} }
if (user.isLocked) { if (isLocked) {
req.flash( req.flash(
'danger', 'danger',
dedent` dedent`
${username} has chosen to make their profile ${username} has chosen to make their profile
private. They will need to make their profile public private. They will need to make their profile public
in order for others to be able to view their certificate. in order for others to be able to view their certification.
`
);
return res.redirect('/');
}
if (!showCerts) {
req.flash(
'danger',
dedent`
${username} has chosen to make their certifications
private. They will need to make their certifications public
in order for others to be able to view them.
` `
); );
return res.redirect('/'); return res.redirect('/');

View File

@ -114,6 +114,18 @@ export default function settingsController(app) {
); );
} }
function updateMyProfileUI(req, res, next) {
const {
user,
body: { profileUI }
} = req;
return user.updateMyProfileUI(profileUI)
.subscribe(
message => res.json({ message }),
next
);
}
function updateMyProjects(req, res, next) { function updateMyProjects(req, res, next) {
const { const {
user, user,
@ -164,6 +176,11 @@ export default function settingsController(app) {
ifNoUser401, ifNoUser401,
updateMyPortfolio updateMyPortfolio
); );
api.post(
'/update-my-profile-ui',
ifNoUser401,
updateMyProfileUI
);
api.post( api.post(
'/update-my-projects', '/update-my-projects',
ifNoUser401, ifNoUser401,

View File

@ -23,13 +23,13 @@ export const publicUserProps = [
'isHonest', 'isHonest',
'isInfosecQaCert', 'isInfosecQaCert',
'isJsAlgoDataStructCert', 'isJsAlgoDataStructCert',
'isLocked',
'isRespWebDesignCert', 'isRespWebDesignCert',
'linkedin', 'linkedin',
'location', 'location',
'name', 'name',
'points', 'points',
'portfolio', 'portfolio',
'profileUI',
'projects', 'projects',
'streak', 'streak',
'twitter', 'twitter',