diff --git a/api-server/server/boot/restApi.js b/api-server/server/boot/restApi.js index e81c692006..d12911326a 100644 --- a/api-server/server/boot/restApi.js +++ b/api-server/server/boot/restApi.js @@ -1,4 +1,6 @@ module.exports = function mountRestApi(app) { - var restApiRoot = app.get('restApiRoot'); - app.use(restApiRoot, app.loopback.rest()); + const restApi = app.loopback.rest(); + const restApiRoot = app.get('restApiRoot'); + app.use(restApiRoot, restApi); + app.use(`/internal${restApiRoot}`, restApi); }; diff --git a/api-server/server/boot/settings.js b/api-server/server/boot/settings.js index 4de5360cbd..3f0c25a431 100644 --- a/api-server/server/boot/settings.js +++ b/api-server/server/boot/settings.js @@ -5,167 +5,8 @@ import { alertTypes } from '../../common/utils/flash.js'; export default function settingsController(app) { const api = app.loopback.Router(); - const toggleUserFlag = (flag, req, res, next) => { - const { user } = req; - const currentValue = user[flag]; - return user.update$({ [flag]: !currentValue }).subscribe( - () => - res.status(200).json({ - flag, - value: !currentValue - }), - next - ); - }; - function refetchCompletedChallenges(req, res, next) { - const { user } = req; - return user - .requestCompletedChallenges() - .subscribe(completedChallenges => res.json({ completedChallenges }), next); - } - - 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); - } - - const updateMyCurrentChallengeValidators = [ - check('currentChallengeId') - .isMongoId() - .withMessage('currentChallengeId is not a valid challenge ID') - ]; - - function updateMyCurrentChallenge(req, res, next) { - const { - user, - body: { currentChallengeId } - } = req; - return user.update$({ currentChallengeId }).subscribe( - () => - res.json({ - message: `your current challenge has been updated to ${currentChallengeId}` - }), - next - ); - } - - const updateMyThemeValidators = [ - check('theme') - .isIn(Object.keys(themes)) - .withMessage('Theme is invalid.') - ]; - - function updateMyTheme(req, res, next) { - const { - body: { theme } - } = req; - if (req.user.theme === theme) { - return res.sendFlash(alertTypes.info, 'Theme already set'); - } - return req.user - .updateTheme(theme) - .then( - () => res.sendFlash(alertTypes.info, 'Your theme has been updated'), - next - ); - } - - function updateFlags(req, res, next) { - const { - user, - body: { values } - } = req; - const keys = Object.keys(values); - if (keys.length === 1 && typeof keys[0] === 'boolean') { - return toggleUserFlag(keys[0], req, res, next); - } - return user - .requestUpdateFlags(values) - .subscribe(message => res.json({ message }), next); - } - - function updateMyPortfolio(req, res, next) { - const { - user, - body: { portfolio } - } = req; - // if we only have one key, it should be the id - // user cannot send only one key to this route - // other than to remove a portfolio item - const requestDelete = Object.keys(portfolio).length === 1; - return user - .updateMyPortfolio(portfolio, requestDelete) - .subscribe(message => res.json({ message }), next); - } - - 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) { - const { - user, - body: { projects: project } - } = req; - return user - .updateMyProjects(project) - .subscribe(message => res.json({ message }), next); - } - - function updateMyUsername(req, res, next) { - const { - user, - body: { username } - } = req; - return user - .updateMyUsername(username) - .subscribe(message => res.json({ message }), next); - } - - const updatePrivacyTerms = (req, res) => { - const { - user, - body: { quincyEmails } - } = req; - const update = { - acceptedPrivacyTerms: true, - sendQuincyEmail: !!quincyEmails - }; - return user.updateAttributes(update, err => { - if (err) { - return res.status(500).json({ - type: 'warning', - message: - 'Something went wrong updating your preferences. ' + - 'Please try again.' - }); - } - return res.status(200).json({ - type: 'success', - message: - 'We have updated your preferences. ' + - 'You can now continue using freeCodeCamp.' - }); - }); - }; + const updateMyUsername = createUpdateMyUsername(app); api.put('/update-privacy-terms', ifNoUser401, updatePrivacyTerms); @@ -206,9 +47,192 @@ export default function settingsController(app) { createValidatorErrorHandler(alertTypes.danger), updateMyTheme ); - api.post('/update-my-username', ifNoUser401, updateMyUsername); + api.put('/update-my-username', ifNoUser401, updateMyUsername); - app.use('/external', api); app.use('/internal', api); app.use(api); } + +const standardErrorMessage = { + type: 'danger', + message: + 'Something went wrong updating your account. Please check and try again' +}; + +const toggleUserFlag = (flag, req, res, next) => { + const { user } = req; + const currentValue = user[flag]; + return user.update$({ [flag]: !currentValue }).subscribe( + () => + res.status(200).json({ + flag, + value: !currentValue + }), + next + ); +}; + +function refetchCompletedChallenges(req, res, next) { + const { user } = req; + return user + .requestCompletedChallenges() + .subscribe(completedChallenges => res.json({ completedChallenges }), next); +} + +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); +} + +const updateMyCurrentChallengeValidators = [ + check('currentChallengeId') + .isMongoId() + .withMessage('currentChallengeId is not a valid challenge ID') +]; + +function updateMyCurrentChallenge(req, res, next) { + const { + user, + body: { currentChallengeId } + } = req; + return user + .update$({ currentChallengeId }) + .subscribe(() => res.status(200), next); +} + +const updateMyThemeValidators = [ + check('theme') + .isIn(Object.keys(themes)) + .withMessage('Theme is invalid.') +]; + +function updateMyTheme(req, res, next) { + const { + body: { theme } + } = req; + if (req.user.theme === theme) { + return res.sendFlash(alertTypes.info, 'Theme already set'); + } + return req.user + .updateTheme(theme) + .then( + () => res.sendFlash(alertTypes.info, 'Your theme has been updated'), + next + ); +} + +function updateFlags(req, res, next) { + const { + user, + body: { values } + } = req; + const keys = Object.keys(values); + if (keys.length === 1 && typeof keys[0] === 'boolean') { + return toggleUserFlag(keys[0], req, res, next); + } + return user + .requestUpdateFlags(values) + .subscribe(message => res.json({ message }), next); +} + +function updateMyPortfolio(req, res, next) { + const { + user, + body: { portfolio } + } = req; + // if we only have one key, it should be the id + // user cannot send only one key to this route + // other than to remove a portfolio item + const requestDelete = Object.keys(portfolio).length === 1; + return user + .updateMyPortfolio(portfolio, requestDelete) + .subscribe(message => res.json({ message }), next); +} + +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) { + const { + user, + body: { projects: project } + } = req; + return user + .updateMyProjects(project) + .subscribe(message => res.json({ message }), next); +} + +function createUpdateMyUsername(app) { + const { User } = app.models; + return async function updateMyUsername(req, res, next) { + const { + user, + body: { username } + } = req; + if (username === user.username) { + return res.json({ + type: 'info', + message: 'Username is already associated with this account' + }); + } + const exists = await User.doesExist(username); + + if (exists) { + return res.json({ + type: 'info', + message: 'Username is already associated with a different account' + }); + } + + return user.updateAttribute('username', 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}` + }); + }); + }; +} + +const updatePrivacyTerms = (req, res) => { + const { + user, + body: { quincyEmails } + } = req; + const update = { + acceptedPrivacyTerms: true, + sendQuincyEmail: !!quincyEmails + }; + return user.updateAttributes(update, err => { + if (err) { + return res.status(500).json(standardErrorMessage); + } + return res.status(200).json({ + type: 'success', + message: + 'We have updated your preferences. ' + + 'You can now continue using freeCodeCamp.' + }); + }); +}; diff --git a/client/src/client-only-routes/ShowSettings.js b/client/src/client-only-routes/ShowSettings.js index abc6a888cd..d6c2f85597 100644 --- a/client/src/client-only-routes/ShowSettings.js +++ b/client/src/client-only-routes/ShowSettings.js @@ -92,7 +92,7 @@ function ShowSettings(props) {

{`Account Settings for ${username}`}

- {/* - + {/* diff --git a/client/src/components/global.css b/client/src/components/global.css index 21caf596cf..04c52aa971 100644 --- a/client/src/components/global.css +++ b/client/src/components/global.css @@ -22,4 +22,10 @@ h6 { .green-text { color: #006400; +} + +.btn-invert { + background-color: #fff; + color: #006400; + } \ No newline at end of file diff --git a/client/src/components/settings/About.js b/client/src/components/settings/About.js new file mode 100644 index 0000000000..21d3e4564b --- /dev/null +++ b/client/src/components/settings/About.js @@ -0,0 +1,227 @@ +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { + Nav, + NavItem, + FormGroup, + ControlLabel, + FormControl +} from '@freecodecamp/react-bootstrap'; + +import { FullWidthRow, Spacer } from '../helpers'; +import ThemeSettings from './Theme'; +import Camper from './Camper'; +import UsernameSettings from './Username'; +import BlockSaveButton from '../helpers/form/BlockSaveButton'; +import BlockSaveWrapper from '../helpers/form/BlockSaveWrapper'; + +const mapStateToProps = () => ({}); + +const mapDispatchToProps = dispatch => bindActionCreators({}, dispatch); + +const propTypes = { + about: PropTypes.string, + currentTheme: PropTypes.string, + location: PropTypes.string, + name: PropTypes.string, + picture: PropTypes.string, + points: PropTypes.number, + username: PropTypes.string +}; + +class AboutSettings extends Component { + constructor(props) { + super(props); + + const { name = '', location = '', picture = '', about = '' } = props; + + this.state = { + view: 'edit', + formValues: { + name, + location, + picture, + about + }, + isFormPristine: true + }; + + this.handleSubmit = this.handleSubmit.bind(this); + this.handleTabSelect = this.handleTabSelect.bind(this); + this.renderEdit = this.renderEdit.bind(this); + this.renderPreview = this.renderPreview.bind(this); + this.show = { + edit: this.renderEdit, + preview: this.renderPreview + }; + } + + handleSubmit(e) { + e.preventDefault(); + const { formValues } = this.state; + console.log(formValues) + } + + handleNameChange = e => { + const value = e.target.value.slice(0); + return this.setState(state => ({ + formValues: { + ...state.formValues, + name: value + } + })); + }; + + handleLocationChange = e => { + const value = e.target.value.slice(0); + return this.setState(state => ({ + formValues: { + ...state.formValues, + location: value + } + })); + }; + + handlePictureChange = e => { + const value = e.target.value.slice(0); + return this.setState(state => ({ + formValues: { + ...state.formValues, + picture: value + } + })); + }; + + handleAboutChange = e => { + const value = e.target.value.slice(0); + return this.setState(state => ({ + formValues: { + ...state.formValues, + about: value + } + })); + }; + + handleTabSelect(key) { + return this.setState(state => ({ + ...state, + view: key + })); + } + + renderEdit() { + const { + formValues: { name, location, picture, about } + } = this.state; + return ( + + + + Name + + + + + + Location + + + + + + Picture + + + + + + About + + + + + ); + } + + renderPreview() { + const { about, picture, points, username, name, location } = this.props; + return ( + + ); + } + + render() { + const { currentTheme, username } = this.props; + const { view, isFormPristine } = this.state; + + const toggleTheme = () => {}; + return ( +
+ + + + +
+ +
+ {this.show[view]()} + + +
+ + + + +
+ ); + } +} + +AboutSettings.displayName = 'AboutSettings'; +AboutSettings.propTypes = propTypes; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(AboutSettings); diff --git a/client/src/components/settings/Camper.js b/client/src/components/settings/Camper.js new file mode 100644 index 0000000000..536e4166c3 --- /dev/null +++ b/client/src/components/settings/Camper.js @@ -0,0 +1,9 @@ +import React from 'react'; + +function CamperSettings() { + return (

CamperSettings

); +} + +CamperSettings.displayName = 'CamperSettings'; + +export default CamperSettings; diff --git a/client/src/components/settings/Theme.js b/client/src/components/settings/Theme.js new file mode 100644 index 0000000000..83bb323ecd --- /dev/null +++ b/client/src/components/settings/Theme.js @@ -0,0 +1,9 @@ +import React from 'react'; + +function ThemeSettings() { + return (

ThemeSettings

); +} + +ThemeSettings.displayName = 'ThemeSettings'; + +export default ThemeSettings; diff --git a/client/src/components/settings/Username.js b/client/src/components/settings/Username.js new file mode 100644 index 0000000000..5d0b2880ca --- /dev/null +++ b/client/src/components/settings/Username.js @@ -0,0 +1,209 @@ +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { + ControlLabel, + FormControl, + Alert, + FormGroup +} from '@freecodecamp/react-bootstrap'; +import isAscii from 'validator/lib/isAscii'; + +import { + validateUsername, + usernameValidationSelector, + submitNewUsername +} from '../../redux/settings'; +import BlockSaveButton from '../helpers/form/BlockSaveButton'; +import FullWidthRow from '../helpers/FullWidthRow'; + +const propTypes = { + isValidUsername: PropTypes.bool, + submitNewUsername: PropTypes.func.isRequired, + username: PropTypes.string, + validateUsername: PropTypes.func.isRequired, + validating: PropTypes.bool +}; + +const mapStateToProps = createSelector( + usernameValidationSelector, + ({ isValidUsername, fetchState }) => ({ + isValidUsername, + validating: fetchState.pending + }) +); + +const mapDispatchToProps = dispatch => + bindActionCreators( + { + submitNewUsername, + validateUsername + }, + dispatch + ); + +const invalidCharsRE = /[/\s?:@=&"'<>#%{}|\\^~[\]`,.;!*()$]/; +const invlaidCharError = { + valid: false, + error: 'Username contains invalid characters' +}; +const valididationSuccess = { valid: true, error: null }; +const usernameTooShort = { valid: false, error: 'Username is too short' }; + +class UsernameSettings extends Component { + constructor(props) { + super(props); + + this.state = { + isFormPristine: true, + formValue: props.username, + characterValidation: { valid: false, error: null }, + sumbitClicked: false + }; + + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.validateFormInput = this.validateFormInput.bind(this); + } + + componentDidUpdate(prevProps, prevState) { + const { username: prevUsername } = prevProps; + const { formValue: prevFormValue } = prevState; + const { username } = this.props; + const { formValue } = this.state; + if (prevUsername !== username && prevFormValue === formValue) { + /* eslint-disable-next-line react/no-did-update-set-state */ + return this.setState({ + isFormPristine: username === formValue, + sumbitClicked: false + }); + } + return null; + } + + handleSubmit(e) { + e.preventDefault(); + const { submitNewUsername } = this.props; + const { + formValue, + characterValidation: { valid } + } = this.state; + + return this.setState( + { sumbitClicked: true }, + () => (valid ? submitNewUsername(formValue) : null) + ); + } + + handleChange(e) { + e.preventDefault(); + const { username, validateUsername } = this.props; + const newValue = e.target.value.toLowerCase(); + return this.setState( + { + formValue: newValue, + isFormPristine: username === newValue, + characterValidation: this.validateFormInput(newValue) + }, + () => + this.state.isFormPristine || this.state.characterValidation.error + ? null + : validateUsername(this.state.formValue) + ); + } + + validateFormInput(formValue) { + if (formValue.length < 3) { + return usernameTooShort; + } + + if (!isAscii(formValue)) { + return invlaidCharError; + } + if (invalidCharsRE.test(formValue)) { + return invlaidCharError; + } + return valididationSuccess; + } + + renderAlerts(validating, error, isValidUsername) { + if (!validating && error) { + return ( + + {error} + + ); + } + if (!validating && !isValidUsername) { + console.log(this.props, this.state); + return ( + + Username not available + + ); + } + if (validating) { + return ( + + Validating username + + ); + } + if (!validating && isValidUsername) { + return ( + + Username is available + + ); + } + return null; + } + + render() { + const { + isFormPristine, + formValue, + characterValidation: { valid, error }, + sumbitClicked + } = this.state; + const { isValidUsername, validating } = this.props; + + return ( + +
+ + + + Username + + + + + {!isFormPristine && + this.renderAlerts(validating, error, isValidUsername)} + + + +
+
+ ); + } +} + +UsernameSettings.displayName = 'UsernameSettings'; +UsernameSettings.propTypes = propTypes; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(UsernameSettings); diff --git a/client/src/redux/index.js b/client/src/redux/index.js index 7f7b99a2b7..3fc20bd74e 100644 --- a/client/src/redux/index.js +++ b/client/src/redux/index.js @@ -8,6 +8,8 @@ import { createReportUserSaga } from './report-user-saga'; import { createShowCertSaga } from './show-cert-saga'; import { createUpdateMyEmailSaga } from './update-email-saga'; +import { types as settingsTypes } from './settings'; + const ns = 'app'; const defaultFetchState = { @@ -74,6 +76,9 @@ export const updateMyEmailError = createAction(types.updateMyEmailError); export const isSignedInSelector = state => !!Object.keys(state[ns].user).length; +export const signInLoadingSelector = state => + userFetchStateSelector(state).pending; + export const showCertSelector = state => state[ns].showCert; export const showCertFetchStateSelector = state => state[ns].showCertFetchState; @@ -83,7 +88,11 @@ export const userByNameSelector = username => state => { }; export const userFetchStateSelector = state => state[ns].userFetchState; export const usernameSelector = state => state[ns].appUsername; -export const userSelector = state => state[ns].user; +export const userSelector = state => { + const username = usernameSelector(state); + + return state[ns].user[username] || {}; +}; export const reducer = handleActions( { @@ -93,7 +102,10 @@ export const reducer = handleActions( }), [types.fetchUserComplete]: (state, { payload: { user, username } }) => ({ ...state, - user, + user: { + ...state.user, + [username]: user + }, appUsername: username, userFetchState: { pending: false, @@ -135,7 +147,20 @@ export const reducer = handleActions( errored: true, error: payload } - }) + }), + [settingsTypes.submitNewUsernameComplete]: (state, { payload }) => + payload + ? { + ...state, + user: { + ...state.user, + [state.appUsername]: { + ...state.user[state.appUsername], + username: payload + } + } + } + : state }, initialState ); diff --git a/client/src/redux/rootReducer.js b/client/src/redux/rootReducer.js index fb3e1b9832..f1ba6b2071 100644 --- a/client/src/redux/rootReducer.js +++ b/client/src/redux/rootReducer.js @@ -2,8 +2,10 @@ import { combineReducers } from 'redux'; import { reducer as app } from './'; import { reducer as flash } from '../components/Flash/redux'; +import { reducer as settings } from './settings'; export default combineReducers({ app, - flash + flash, + settings }); diff --git a/client/src/redux/rootSaga.js b/client/src/redux/rootSaga.js index a10b2e35f1..d5965a2a40 100644 --- a/client/src/redux/rootSaga.js +++ b/client/src/redux/rootSaga.js @@ -1,7 +1,8 @@ import { all } from 'redux-saga/effects'; import { sagas as appSagas } from './'; +import { sagas as settingsSagas } from './settings'; export default function* rootSaga() { - yield all([...appSagas]); + yield all([...appSagas, ...settingsSagas]); } diff --git a/client/src/redux/settings/index.js b/client/src/redux/settings/index.js new file mode 100644 index 0000000000..be03c2bd85 --- /dev/null +++ b/client/src/redux/settings/index.js @@ -0,0 +1,73 @@ +import { createAction, handleActions } from 'redux-actions'; + +import { createTypes, createAsyncTypes } from '../../utils/createTypes'; +import { createSettingsSagas } from './settings-sagas'; + +const ns = 'settings'; + +const defaultFetchState = { + pending: false, + complete: false, + errored: false, + error: null +}; + +const initialState = { + usernameValidation: { + isValidUsername: false, + fetchState: { ...defaultFetchState } + } +}; + +export const types = createTypes( + [ + ...createAsyncTypes('validateUsername'), + ...createAsyncTypes('submitNewUsername') + ], + ns +); + +export const sagas = [...createSettingsSagas(types)]; + +export const submitNewUsername = createAction(types.submitNewUsername); +export const submitNewUsernameComplete = createAction( + types.submitNewUsernameComplete, + ({ type, username }) => (type === 'success' ? username : null) +); +export const submitNewUsernameError = createAction( + types.submitNewUsernameError +); + +export const validateUsername = createAction(types.validateUsername); +export const validateUsernameComplete = createAction( + types.validateUsernameComplete +); +export const validateUsernameError = createAction(types.validateUsernameError); + +export const usernameValidationSelector = state => state[ns].usernameValidation; + +export const reducer = handleActions( + { + [types.submitNewUsernameComplete]: state => ({ + ...state, + usernameValidation: { ...initialState.usernameValidation } + }), + [types.validateUsername]: state => ({ + ...state, + usernameValidation: { + ...state.usernameValidation, + isValidUsername: false, + fetchState: { ...defaultFetchState, pending: true } + } + }), + [types.validateUsernameComplete]: (state, { payload }) => ({ + ...state, + usernameValidation: { + ...state.usernameValidation, + isValidUsername: !payload, + fetchState: { ...defaultFetchState, pending: false, complete: true } + } + }) + }, + initialState +); diff --git a/client/src/redux/settings/settings-sagas.js b/client/src/redux/settings/settings-sagas.js new file mode 100644 index 0000000000..d61d83560b --- /dev/null +++ b/client/src/redux/settings/settings-sagas.js @@ -0,0 +1,41 @@ +import { delay } from 'redux-saga'; +import { call, put, takeLatest } from 'redux-saga/effects'; + +import { + validateUsernameComplete, + validateUsernameError, + submitNewUsernameComplete, + submitNewUsernameError +} from './'; + +import { getUsernameExists, putUpdateMyUsername } from '../../utils/ajax'; +import { createFlashMessage } from '../../components/Flash/redux'; + +function* validateUsernameSaga({ payload }) { + try { + yield delay(500); + const { + data: { exists } + } = yield call(getUsernameExists, payload); + yield put(validateUsernameComplete(exists)); + } catch (e) { + yield put(validateUsernameError(e)); + } +} + +function* submitNEwUswernameSaga({ payload: username }) { + try { + const { data: response } = yield call(putUpdateMyUsername, username); + yield put(submitNewUsernameComplete({...response, username})); + yield put(createFlashMessage(response)); + } catch (e) { + yield put(submitNewUsernameError(e)); + } +} + +export function createSettingsSagas(types) { + return [ + takeLatest(types.validateUsername, validateUsernameSaga), + takeLatest(types.submitNewUsername, submitNEwUswernameSaga) + ]; +} diff --git a/client/src/utils/ajax.js b/client/src/utils/ajax.js index 3f9249b374..b7091a1427 100644 --- a/client/src/utils/ajax.js +++ b/client/src/utils/ajax.js @@ -28,6 +28,10 @@ export function getShowCert(username, cert) { return get(`/certificate/showCert/${username}/${cert}`); } +export function getUsernameExists(username) { + return get(`/api/users/exists?username=${username}`); +} + /** POST **/ export function postReportUser(body) { @@ -36,6 +40,10 @@ export function postReportUser(body) { /** PUT **/ +export function putUpdateMyUsername(username) { + return put('/update-my-username', { username }); +} + export function putUserAcceptsTerms(quincyEmails) { return put('/update-privacy-terms', { quincyEmails }); }