Feature(settings): complete settings page logic
This commit is contained in:
@ -9,7 +9,7 @@ export const updateTitle = createAction(types.updateTitle);
|
||||
// used in combination with fetch-user-saga
|
||||
export const fetchUser = createAction(types.fetchUser);
|
||||
|
||||
// setUser(
|
||||
// addUser(
|
||||
// entities: { [userId]: User }
|
||||
// ) => Action
|
||||
export const addUser = createAction(
|
||||
@ -25,11 +25,21 @@ export const updateUserPoints = createAction(
|
||||
types.updateUserPoints,
|
||||
(username, points) => ({ username, points })
|
||||
);
|
||||
// updateUserPoints(username: String, flag: String) => Action
|
||||
// updateUserFlag(username: String, flag: String) => Action
|
||||
export const updateUserFlag = createAction(
|
||||
types.updateUserFlag,
|
||||
(username, flag) => ({ username, flag })
|
||||
);
|
||||
// updateUserEmail(username: String, email: String) => Action
|
||||
export const updateUserEmail = createAction(
|
||||
types.updateUserFlag,
|
||||
(username, email) => ({ username, email })
|
||||
);
|
||||
// updateUserLang(username: String, lang: String) => Action
|
||||
export const updateUserLang = createAction(
|
||||
types.updateUserLang,
|
||||
(username, lang) => ({ username, lang })
|
||||
);
|
||||
// updateCompletedChallenges(username: String) => Action
|
||||
export const updateCompletedChallenges = createAction(
|
||||
types.updateCompletedChallenges
|
||||
@ -55,6 +65,16 @@ export const createErrorObservable = error => Observable.just({
|
||||
type: types.handleError,
|
||||
error
|
||||
});
|
||||
// doActionOnError(
|
||||
// actionCreator: (() => Action|Null)
|
||||
// ) => (error: Error) => Observable[Action]
|
||||
export const doActionOnError = actionCreator => error => Observable.of(
|
||||
{
|
||||
type: types.handleError,
|
||||
error
|
||||
},
|
||||
actionCreator()
|
||||
);
|
||||
|
||||
|
||||
// drawers
|
||||
|
@ -8,8 +8,13 @@ const initialState = {
|
||||
user: {}
|
||||
};
|
||||
|
||||
// future refactor(berks): Several of the actions here are just updating props
|
||||
// on the main user. These can be refactors into one response for all actions
|
||||
export default function entities(state = initialState, action) {
|
||||
const { type, payload: { username, points, flag } = {} } = action;
|
||||
const {
|
||||
type,
|
||||
payload: { email, username, points, flag, languageTag } = {}
|
||||
} = action;
|
||||
if (type === updateCompletedChallenges) {
|
||||
const username = action.payload;
|
||||
const completedChallengeMap = state.user[username].challengeMap || {};
|
||||
@ -26,6 +31,12 @@ export default function entities(state = initialState, action) {
|
||||
}, {})
|
||||
};
|
||||
}
|
||||
if (action.meta && action.meta.entities) {
|
||||
return {
|
||||
...state,
|
||||
...action.meta.entities
|
||||
};
|
||||
}
|
||||
if (type === updateUserPoints) {
|
||||
return {
|
||||
...state,
|
||||
@ -38,12 +49,6 @@ export default function entities(state = initialState, action) {
|
||||
}
|
||||
};
|
||||
}
|
||||
if (action.meta && action.meta.entities) {
|
||||
return {
|
||||
...state,
|
||||
...action.meta.entities
|
||||
};
|
||||
}
|
||||
if (action.type === types.updateUserFlag) {
|
||||
return {
|
||||
...state,
|
||||
@ -56,5 +61,29 @@ export default function entities(state = initialState, action) {
|
||||
}
|
||||
};
|
||||
}
|
||||
if (action.type === types.updateUserEmail) {
|
||||
return {
|
||||
...state,
|
||||
user: {
|
||||
...state.user,
|
||||
[username]: {
|
||||
...state.user[username],
|
||||
email
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
if (action.type === types.updateUserLang) {
|
||||
return {
|
||||
...state,
|
||||
user: {
|
||||
...state.user,
|
||||
[username]: {
|
||||
...state.user[username],
|
||||
languageTag
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
@ -8,6 +8,8 @@ export default createTypes([
|
||||
'updateThisUser',
|
||||
'updateUserPoints',
|
||||
'updateUserFlag',
|
||||
'updateUserEmail',
|
||||
'updateUserLang',
|
||||
'updateCompletedChallenges',
|
||||
'showSignIn',
|
||||
|
||||
|
@ -1,20 +1,22 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import { Button, Row, Col } from 'react-bootstrap';
|
||||
import FA from 'react-fontawesome';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export function UpdateEmailButton() {
|
||||
return (
|
||||
<Button
|
||||
block={ true }
|
||||
bsSize='lg'
|
||||
bsStyle='primary'
|
||||
className='btn-link-social'
|
||||
href='/update-email'
|
||||
>
|
||||
<FA name='envelope' />
|
||||
Update my Email
|
||||
</Button>
|
||||
<Link to='/settings/update-email'>
|
||||
<Button
|
||||
block={ true }
|
||||
bsSize='lg'
|
||||
bsStyle='primary'
|
||||
className='btn-link-social'
|
||||
>
|
||||
<FA name='envelope' />
|
||||
Update my Email
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,27 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { createSelector } from 'reselect';
|
||||
import { reduxForm } from 'redux-form';
|
||||
import { FormControl } from 'react-bootstrap';
|
||||
|
||||
import { updateMyLang } from '../redux/actions';
|
||||
import { userSelector } from '../../../redux/selectors';
|
||||
import langs from '../../../../utils/supported-languages';
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
userSelector,
|
||||
({ user: { languageTag } }) => ({
|
||||
// send null to prevent redux-form from initialize empty
|
||||
initialValues: languageTag ? { lang: languageTag } : null
|
||||
})
|
||||
);
|
||||
const actions = { updateMyLang };
|
||||
const fields = [ 'lang' ];
|
||||
const validator = values => {
|
||||
if (!langs[values.lang]) {
|
||||
return { lang: `${values.lang} is unsupported` };
|
||||
}
|
||||
return {};
|
||||
};
|
||||
const options = [(
|
||||
<option
|
||||
disabled={ true }
|
||||
@ -30,19 +50,58 @@ const options = [(
|
||||
)
|
||||
];
|
||||
|
||||
export default function LangaugeSettings({ userLang }) {
|
||||
return (
|
||||
<FormControl
|
||||
className='btn btn-block btn-primary btn-link-social'
|
||||
componentClass='select'
|
||||
defaultValue='not-the-momma'
|
||||
value={ userLang }
|
||||
>
|
||||
{ options }
|
||||
</FormControl>
|
||||
);
|
||||
export class LangaugeSettings extends React.Component {
|
||||
static propTypes = {
|
||||
fields: PropTypes.object,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
updateMyLang: PropTypes.func.isRequired
|
||||
};
|
||||
constructor(...props) {
|
||||
super(...props);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
// make sure to clear timeout if it exist
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
}
|
||||
|
||||
handleChange(e) {
|
||||
e.preventDefault();
|
||||
this.props.fields.lang.onChange(e);
|
||||
// give redux-form HOC state time to catch up before
|
||||
// attempting to submit
|
||||
this.timeout = setTimeout(
|
||||
() => this.props.handleSubmit(this.props.updateMyLang)(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
fields: { lang }
|
||||
} = this.props;
|
||||
return (
|
||||
<FormControl
|
||||
className='btn btn-block btn-primary btn-link-social'
|
||||
componentClass='select'
|
||||
{ ...lang }
|
||||
onChange={ this.handleChange }
|
||||
>
|
||||
{ options }
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LangaugeSettings.propTypes = {
|
||||
userLang: PropTypes.string
|
||||
};
|
||||
export default reduxForm(
|
||||
{
|
||||
form: 'lang',
|
||||
fields,
|
||||
validator,
|
||||
overwriteOnInitialValuesChange: false
|
||||
},
|
||||
mapStateToProps,
|
||||
actions
|
||||
)(LangaugeSettings);
|
||||
|
@ -14,9 +14,13 @@ import {
|
||||
openDeleteModal,
|
||||
hideDeleteModal
|
||||
} from '../redux/actions';
|
||||
import { toggleNightMode } from '../../../redux/actions';
|
||||
import {
|
||||
toggleNightMode,
|
||||
updateTitle
|
||||
} from '../../../redux/actions';
|
||||
|
||||
const actions = {
|
||||
updateTitle,
|
||||
toggleNightMode,
|
||||
openDeleteModal,
|
||||
hideDeleteModal,
|
||||
@ -57,8 +61,13 @@ const mapStateToProps = state => {
|
||||
};
|
||||
|
||||
export class Settings extends React.Component {
|
||||
constructor(...props) {
|
||||
super(...props);
|
||||
this.updateMyLang = this.updateMyLang.bind(this);
|
||||
}
|
||||
static displayName = 'Settings';
|
||||
static propTypes = {
|
||||
children: PropTypes.element,
|
||||
username: PropTypes.string,
|
||||
isDeleteOpen: PropTypes.bool,
|
||||
isLocked: PropTypes.bool,
|
||||
@ -69,17 +78,32 @@ export class Settings extends React.Component {
|
||||
sendMonthlyEmail: PropTypes.bool,
|
||||
sendNotificationEmail: PropTypes.bool,
|
||||
sendQuincyEmail: PropTypes.bool,
|
||||
toggleNightMode: PropTypes.func,
|
||||
toggleIsLocked: PropTypes.func,
|
||||
toggleQuincyEmail: PropTypes.func,
|
||||
toggleMonthlyEmail: PropTypes.func,
|
||||
toggleNotificationEmail: PropTypes.func,
|
||||
openDeleteModal: PropTypes.func,
|
||||
hideDeleteModal: PropTypes.func
|
||||
updateTitle: PropTypes.func.isRequired,
|
||||
toggleNightMode: PropTypes.func.isRequired,
|
||||
toggleIsLocked: PropTypes.func.isRequired,
|
||||
toggleQuincyEmail: PropTypes.func.isRequired,
|
||||
toggleMonthlyEmail: PropTypes.func.isRequired,
|
||||
toggleNotificationEmail: PropTypes.func.isRequired,
|
||||
openDeleteModal: PropTypes.func.isRequired,
|
||||
hideDeleteModal: PropTypes.func.isRequired,
|
||||
lang: PropTypes.string,
|
||||
initialLang: PropTypes.string,
|
||||
updateMyLang: PropTypes.func
|
||||
};
|
||||
|
||||
updateMyLang(e) {
|
||||
e.preventDefault();
|
||||
const lang = e.target.value;
|
||||
this.props.updateMyLang(lang);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.props.updateTitle('Settings');
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
username,
|
||||
isDeleteOpen,
|
||||
isLocked,
|
||||
@ -98,6 +122,18 @@ export class Settings extends React.Component {
|
||||
openDeleteModal,
|
||||
hideDeleteModal
|
||||
} = this.props;
|
||||
if (children) {
|
||||
return (
|
||||
<Row>
|
||||
<Col
|
||||
sm={ 4 }
|
||||
smOffset={ 4 }
|
||||
>
|
||||
{ children }
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Row>
|
||||
|
@ -1,6 +1,10 @@
|
||||
import Settings from './components/Settings.jsx';
|
||||
import updateEmail from './routes/update-email';
|
||||
|
||||
export default {
|
||||
path: 'settings',
|
||||
component: Settings
|
||||
component: Settings,
|
||||
childRoutes: [
|
||||
updateEmail
|
||||
]
|
||||
};
|
||||
|
@ -8,12 +8,19 @@ const initialState = {
|
||||
export const types = createTypes([
|
||||
'toggleUserFlag',
|
||||
'openDeleteModal',
|
||||
'hideDeleteModal'
|
||||
'hideDeleteModal',
|
||||
'updateMyEmail',
|
||||
'updateMyLang'
|
||||
], 'settings');
|
||||
|
||||
export const toggleUserFlag = createAction(types.toggleUserFlag);
|
||||
export const openDeleteModal = createAction(types.openDeleteModal);
|
||||
export const hideDeleteModal = createAction(types.hideDeleteModal);
|
||||
export const updateMyEmail = createAction(types.updateMyEmail);
|
||||
export const updateMyLang = createAction(
|
||||
types.updateMyLang,
|
||||
(values) => values.lang
|
||||
);
|
||||
|
||||
export default handleActions(
|
||||
{
|
||||
|
0
common/app/routes/settings/redux/selectors.js
Normal file
0
common/app/routes/settings/redux/selectors.js
Normal file
@ -1,7 +1,16 @@
|
||||
import { Observable } from 'rx';
|
||||
|
||||
import { types } from './actions';
|
||||
import combineSagas from '../../../utils/combine-sagas';
|
||||
import { makeToast } from '../../../toasts/redux/actions';
|
||||
import {
|
||||
updateUserFlag,
|
||||
updateUserEmail,
|
||||
updateUserLang,
|
||||
doActionOnError
|
||||
} from '../../../redux/actions';
|
||||
import { postJSON$ } from '../../../../utils/ajax-stream';
|
||||
import { updateUserFlag, createErrorObservable } from '../../../redux/actions';
|
||||
import langs from '../../../../utils/supported-languages';
|
||||
|
||||
const urlMap = {
|
||||
isLocked: 'lockdown',
|
||||
@ -9,7 +18,61 @@ const urlMap = {
|
||||
sendNotificationEmail: 'notification-email',
|
||||
sendMonthlyEmail: 'announcement-email'
|
||||
};
|
||||
export default function userUpdateSaga(actions$, getState) {
|
||||
|
||||
export function updateUserEmailSaga(actions$, getState) {
|
||||
return actions$
|
||||
.filter(({ type }) => type === types.updateMyEmail)
|
||||
.flatMap(({ payload: email }) => {
|
||||
const {
|
||||
app: { user: username, csrfToken: _csrf },
|
||||
entities: { user: userMap }
|
||||
} = getState();
|
||||
const { email: oldEmail } = userMap[username] || {};
|
||||
const body = { _csrf, email };
|
||||
const optimisticUpdate$ = Observable.just(
|
||||
updateUserEmail(username, email)
|
||||
);
|
||||
const ajaxUpdate$ = postJSON$('/update-my-email', body)
|
||||
.map(({ message }) => makeToast({ message }))
|
||||
.catch(doActionOnError(() => oldEmail ?
|
||||
updateUserFlag(username, oldEmail) :
|
||||
null
|
||||
));
|
||||
return Observable.merge(optimisticUpdate$, ajaxUpdate$);
|
||||
});
|
||||
}
|
||||
|
||||
export function updateUserLangSaga(actions$, getState) {
|
||||
const updateLang$ = actions$
|
||||
.filter(({ type, payload }) => (
|
||||
type === types.updateMyLang && !!langs[payload]
|
||||
))
|
||||
.map(({ payload }) => payload);
|
||||
const ajaxUpdate$ = updateLang$
|
||||
.flatMap(lang => {
|
||||
const {
|
||||
app: { user: username, csrfToken: _csrf },
|
||||
entities: { user: userMap }
|
||||
} = getState();
|
||||
const { languageTag: oldLang } = userMap[username] || {};
|
||||
const body = {
|
||||
_csrf,
|
||||
lang
|
||||
};
|
||||
return postJSON$('/update-my-lang', body)
|
||||
.map(({ message }) => makeToast({ message }))
|
||||
.catch(doActionOnError(() => {
|
||||
return updateUserLang(username, oldLang);
|
||||
}));
|
||||
});
|
||||
const optimistic$ = updateLang$
|
||||
.map(lang => {
|
||||
const { app: { user: username } } = getState();
|
||||
return updateUserLang(username, lang);
|
||||
});
|
||||
return Observable.merge(ajaxUpdate$, optimistic$);
|
||||
}
|
||||
export function updateUserFlagSaga(actions$, getState) {
|
||||
const toggleFlag$ = actions$
|
||||
.filter(({ type, payload }) => type === types.toggleUserFlag && payload)
|
||||
.map(({ payload }) => payload);
|
||||
@ -34,7 +97,15 @@ export default function userUpdateSaga(actions$, getState) {
|
||||
}
|
||||
return updateUserFlag(username, flag);
|
||||
})
|
||||
.catch(createErrorObservable);
|
||||
.catch(doActionOnError(() => {
|
||||
return updateUserFlag(username, currentValue);
|
||||
}));
|
||||
});
|
||||
return Observable.merge(optimistic$, serverUpdate$);
|
||||
}
|
||||
|
||||
export default combineSagas(
|
||||
updateUserFlagSaga,
|
||||
updateUserEmailSaga,
|
||||
updateUserLangSaga
|
||||
);
|
||||
|
144
common/app/routes/settings/routes/update-email/Update-Email.jsx
Normal file
144
common/app/routes/settings/routes/update-email/Update-Email.jsx
Normal file
@ -0,0 +1,144 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Button, HelpBlock, FormControl, FormGroup } from 'react-bootstrap';
|
||||
import { LinkContainer } from 'react-router-bootstrap';
|
||||
import { reduxForm } from 'redux-form';
|
||||
import { isEmail } from 'validator';
|
||||
import { getValidationState } from '../../../../utils/form';
|
||||
import { updateMyEmail } from '../../redux/actions';
|
||||
|
||||
const actions = {
|
||||
updateMyEmail
|
||||
};
|
||||
const fields = [
|
||||
'email',
|
||||
'duplicate'
|
||||
];
|
||||
const validateFields = ({ email, duplicate }) => {
|
||||
const errors = {};
|
||||
if (!isEmail('' + email)) {
|
||||
errors.email = 'This email is invalid.';
|
||||
}
|
||||
if (duplicate && email !== duplicate) {
|
||||
errors.duplicate = 'This email does not match the one above.';
|
||||
}
|
||||
return errors;
|
||||
};
|
||||
const mapStateToProps = state => {
|
||||
const {
|
||||
app: { user: username },
|
||||
entities: { user: userMap }
|
||||
} = state;
|
||||
const { email, emailVerified } = userMap[username] || {};
|
||||
return {
|
||||
initialValues: { email },
|
||||
isEmailThere: !!email,
|
||||
isVerified: !!emailVerified
|
||||
};
|
||||
};
|
||||
|
||||
export class UpdateEmail extends React.Component {
|
||||
constructor(...props) {
|
||||
super(...props);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
static displayName = 'UpdateEmail';
|
||||
static propTypes = {
|
||||
isEmailThere: PropTypes.bool,
|
||||
isVerified: PropTypes.bool,
|
||||
fields: PropTypes.object.isRequired,
|
||||
submitting: PropTypes.bool,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
updateMyEmail: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
this.props.handleSubmit(({ email }) => this.props.updateMyEmail(email))(e);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
isEmailThere,
|
||||
isVerified,
|
||||
fields: { email, duplicate },
|
||||
submitting
|
||||
} = this.props;
|
||||
const buttonCopy = !isEmailThere || isVerified ?
|
||||
'Update my Email' :
|
||||
'Verify Email';
|
||||
return (
|
||||
<div>
|
||||
<h2 className='text-center'>Update your email address here:</h2>
|
||||
<form
|
||||
name='update-email'
|
||||
onSubmit={ this.handleSubmit }
|
||||
>
|
||||
<FormGroup
|
||||
bsSize='lg'
|
||||
controlId='email'
|
||||
validationState={ getValidationState(email) }
|
||||
>
|
||||
<FormControl
|
||||
autofocus={ true }
|
||||
placeholder='Enter your new email'
|
||||
type='email'
|
||||
{ ...email }
|
||||
/>
|
||||
{
|
||||
!email.error ?
|
||||
null :
|
||||
<HelpBlock>{ email.error }</HelpBlock>
|
||||
}
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
bsSize='lg'
|
||||
controlId='duplicate'
|
||||
validationState={ getValidationState(duplicate) }
|
||||
>
|
||||
<FormControl
|
||||
placeholder='re-type your email address'
|
||||
type='email'
|
||||
{ ...duplicate }
|
||||
/>
|
||||
{
|
||||
!duplicate.error ?
|
||||
null :
|
||||
<HelpBlock>{ duplicate.error }</HelpBlock>
|
||||
}
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Button
|
||||
block={ true }
|
||||
bsSize='lg'
|
||||
bsStyle='primary'
|
||||
disabled={ submitting }
|
||||
type='submit'
|
||||
>
|
||||
{ buttonCopy }
|
||||
</Button>
|
||||
<div className='button-spacer' />
|
||||
<LinkContainer to='/settings'>
|
||||
<Button
|
||||
block={ true }
|
||||
bsSize='lg'
|
||||
bsStyle='primary'
|
||||
>
|
||||
Go back to Settings
|
||||
</Button>
|
||||
</LinkContainer>
|
||||
</FormGroup>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default reduxForm(
|
||||
{
|
||||
form: 'update-email',
|
||||
fields,
|
||||
validate: validateFields
|
||||
},
|
||||
mapStateToProps,
|
||||
actions
|
||||
)(UpdateEmail);
|
6
common/app/routes/settings/routes/update-email/index.js
Normal file
6
common/app/routes/settings/routes/update-email/index.js
Normal file
@ -0,0 +1,6 @@
|
||||
import UpdateEmail from './Update-Email.jsx';
|
||||
|
||||
export default {
|
||||
path: 'update-email',
|
||||
component: UpdateEmail
|
||||
};
|
9
common/app/utils/combine-sagas.js
Normal file
9
common/app/utils/combine-sagas.js
Normal file
@ -0,0 +1,9 @@
|
||||
import { Observable } from 'rx';
|
||||
|
||||
export default function combineSagas(...sagas) {
|
||||
return (actions$, getState, deps) => {
|
||||
return Observable.merge(
|
||||
sagas.map(saga => saga(actions$, getState, deps))
|
||||
);
|
||||
};
|
||||
}
|
@ -390,14 +390,14 @@ module.exports = function(User) {
|
||||
lastEmailSentAt.isBefore(fiveMinutesAgo) :
|
||||
true;
|
||||
|
||||
if (!isEmail(email)) {
|
||||
return Promise.reject(
|
||||
if (!isEmail('' + email)) {
|
||||
return Observable.throw(
|
||||
new Error('The submitted email not valid.')
|
||||
);
|
||||
}
|
||||
// email is already associated and verified with this account
|
||||
if (ownEmail && this.emailVerified) {
|
||||
return Promise.reject(new Error(
|
||||
return Observable.throw(new Error(
|
||||
`${email} is already associated with this account.`
|
||||
));
|
||||
}
|
||||
@ -410,13 +410,13 @@ module.exports = function(User) {
|
||||
`${minutesLeft} minute${minutesLeft > 1 ? 's' : ''}` :
|
||||
'a few seconds';
|
||||
|
||||
return Promise.reject(new Error(
|
||||
return Observable.throw(new Error(
|
||||
`Please wait ${timeToWait} to resend email verification.`
|
||||
));
|
||||
}
|
||||
|
||||
return User.doesExist(null, email)
|
||||
.then(exists => {
|
||||
return Observable.fromPromise(User.doesExist(null, email))
|
||||
.flatMap(exists => {
|
||||
// not associated with this account, but is associated with another
|
||||
if (!ownEmail && exists) {
|
||||
return Promise.reject(
|
||||
@ -434,67 +434,35 @@ module.exports = function(User) {
|
||||
this.email = email;
|
||||
this.emailVerified = emailVerified;
|
||||
this.emailVerifyTTL = new Date();
|
||||
})
|
||||
.flatMap(() => {
|
||||
var mailOptions = {
|
||||
type: 'email',
|
||||
to: email,
|
||||
from: 'Team@freecodecamp.com',
|
||||
subject: 'Welcome to Free Code Camp!',
|
||||
protocol: isDev ? null : 'https',
|
||||
host: isDev ? 'localhost' : 'freecodecamp.com',
|
||||
port: isDev ? null : 443,
|
||||
template: path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'server',
|
||||
'views',
|
||||
'emails',
|
||||
'user-email-verify.ejs'
|
||||
)
|
||||
};
|
||||
return this.verify(mailOptions);
|
||||
})
|
||||
.map(() => dedent`
|
||||
Please check your email.
|
||||
We sent you a link that you can click to verify your email address.
|
||||
`)
|
||||
.catch(error => {
|
||||
debug(error);
|
||||
return Observable.throw(
|
||||
'Oops, something went wrong, please try again later.'
|
||||
);
|
||||
})
|
||||
.toPromise();
|
||||
});
|
||||
});
|
||||
})
|
||||
.flatMap(() => {
|
||||
const mailOptions = {
|
||||
type: 'email',
|
||||
to: email,
|
||||
from: 'Team@freecodecamp.com',
|
||||
subject: 'Welcome to Free Code Camp!',
|
||||
protocol: isDev ? null : 'https',
|
||||
host: isDev ? 'localhost' : 'freecodecamp.com',
|
||||
port: isDev ? null : 443,
|
||||
template: path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'server',
|
||||
'views',
|
||||
'emails',
|
||||
'user-email-verify.ejs'
|
||||
)
|
||||
};
|
||||
return this.verify(mailOptions);
|
||||
})
|
||||
.map(() => dedent`
|
||||
Please check your email.
|
||||
We sent you a link that you can click to verify your email address.
|
||||
`);
|
||||
};
|
||||
|
||||
User.remoteMethod(
|
||||
'updateEmail',
|
||||
{
|
||||
isStatic: false,
|
||||
description: 'updates the email of the user object',
|
||||
accepts: [
|
||||
{
|
||||
arg: 'email',
|
||||
type: 'string',
|
||||
required: true
|
||||
}
|
||||
],
|
||||
returns: [
|
||||
{
|
||||
arg: 'message',
|
||||
type: 'string'
|
||||
}
|
||||
],
|
||||
http: {
|
||||
path: '/update-email',
|
||||
verb: 'POST'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
User.giveBrowniePoints =
|
||||
function giveBrowniePoints(receiver, giver, data = {}, dev = false, cb) {
|
||||
const findUser = observeMethod(User, 'findOne');
|
||||
|
3
server/boot/react.js
vendored
3
server/boot/react.js
vendored
@ -15,7 +15,8 @@ const routes = [
|
||||
'/challenges',
|
||||
'/challenges/*',
|
||||
'/map',
|
||||
'/settings'
|
||||
'/settings',
|
||||
'/settings/*'
|
||||
];
|
||||
|
||||
const devRoutes = [];
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { ifNoUser401 } from '../utils/middleware';
|
||||
import supportedLanguages from '../../common/utils/supported-languages.js';
|
||||
|
||||
export default function settingsController(app) {
|
||||
const api = app.loopback.Router();
|
||||
@ -15,6 +16,39 @@ export default function settingsController(app) {
|
||||
next
|
||||
);
|
||||
};
|
||||
|
||||
function updateMyEmail(req, res, next) {
|
||||
const { user, body: { email } } = req;
|
||||
return user.updateEmail(email)
|
||||
.subscribe(
|
||||
(message) => res.json({ message }),
|
||||
next
|
||||
);
|
||||
}
|
||||
|
||||
function updateMyLang(req, res, next) {
|
||||
const { user, body: { lang } = {} } = req;
|
||||
const langName = supportedLanguages[lang];
|
||||
const update = { languageTag: lang };
|
||||
if (!supportedLanguages[lang]) {
|
||||
return res.json({
|
||||
message: `${lang} is currently unsupported`
|
||||
});
|
||||
}
|
||||
if (user.langaugeTag === lang) {
|
||||
return res.json({
|
||||
message: `Your language is already set to ${langName}`
|
||||
});
|
||||
}
|
||||
return user.update$(update)
|
||||
.subscribe(
|
||||
() => res.json({
|
||||
message: `Your language has been updated to '${langName}'`
|
||||
}),
|
||||
next
|
||||
);
|
||||
}
|
||||
|
||||
api.post(
|
||||
'/toggle-lockdown',
|
||||
toggleUserFlag('isLocked')
|
||||
@ -34,5 +68,15 @@ export default function settingsController(app) {
|
||||
ifNoUser401,
|
||||
toggleUserFlag('sendQuincyEmail')
|
||||
);
|
||||
api.post(
|
||||
'/update-my-email',
|
||||
ifNoUser401,
|
||||
updateMyEmail
|
||||
);
|
||||
api.post(
|
||||
'/update-my-lang',
|
||||
ifNoUser401,
|
||||
updateMyLang
|
||||
);
|
||||
app.use(api);
|
||||
}
|
||||
|
Reference in New Issue
Block a user