From 8aa62b79a445a799358a2860c1159b2f05b95f6c Mon Sep 17 00:00:00 2001 From: Brian Lovin Date: Fri, 9 Nov 2018 10:39:47 -0800 Subject: [PATCH 01/14] Allow user to edit their own email address --- api/mutations/user/editUser.js | 59 +++++++++++---- api/types/User.js | 1 + shared/db/queries/user.js | 7 ++ src/views/userSettings/components/editForm.js | 74 ++++++++++++++++++- 4 files changed, 122 insertions(+), 19 deletions(-) diff --git a/api/mutations/user/editUser.js b/api/mutations/user/editUser.js index 4f5dbdcc0b..8e870438cd 100644 --- a/api/mutations/user/editUser.js +++ b/api/mutations/user/editUser.js @@ -7,26 +7,25 @@ import { getUserByUsername, getUserById, editUser, + getUsersByEmail, + setUserPendingEmail, } from 'shared/db/queries/user'; import { events } from 'shared/analytics'; import { trackQueue } from 'shared/bull/queues'; import { isAuthedResolver as requireAuth } from '../../utils/permissions'; +import isEmail from 'validator/lib/isEmail'; +import { sendEmailValidationEmailQueue } from 'shared/bull/queues'; export default requireAuth( - async ( - _: any, - args: EditUserInput, - { user, updateCookieUserData }: GraphQLContext - ) => { - const currentUser = user; + async (_: any, args: EditUserInput, ctx: GraphQLContext) => { + const { user: currentUser, updateCookieUserData } = ctx; + const { input } = args; + // If the user is trying to change their username check whether there's a person with that username already - if (args.input.username) { - if ( - args.input.username === 'null' || - args.input.username === 'undefined' - ) { + if (input.username) { + if (input.username === 'null' || input.username === 'undefined') { trackQueue.add({ - userId: user.id, + userId: currentUser.id, event: events.USER_EDITED_FAILED, properties: { reason: 'bad username input', @@ -36,10 +35,10 @@ export default requireAuth( return new UserError('Nice try! 😉'); } - const dbUser = await getUserByUsername(args.input.username); - if (dbUser && dbUser.id !== user.id) { + const dbUser = await getUserByUsername(input.username); + if (dbUser && dbUser.id !== currentUser.id) { trackQueue.add({ - userId: user.id, + userId: currentUser.id, event: events.USER_EDITED_FAILED, properties: { reason: 'username taken', @@ -52,7 +51,35 @@ export default requireAuth( } } - const editedUser = await editUser(args, user.id); + if (input.email) { + // if user is changing their email, make sure it's not taken by someone else + if (input.email !== currentUser.email) { + if (!isEmail(input.email)) { + return new UserError('Please enter a working email address'); + } + + const dbUsers = await getUsersByEmail(input.email); + if (dbUsers && dbUsers.length > 0) { + return new UserError( + 'That email address is already taken by another person on Spectrum.' + ); + } + + // the user will have to confirm their email for it to be saved in + // order to prevent spoofing your email as someone elses + await setUserPendingEmail(currentUser.id, input.email).then(() => { + // need this duplicate check for some reason for Flow to work properly + if (input.email) { + sendEmailValidationEmailQueue.add({ + email: input.email, + userId: currentUser.id, + }); + } + }); + } + } + + const editedUser = await editUser(args, currentUser.id); await updateCookieUserData({ ...editedUser, diff --git a/api/types/User.js b/api/types/User.js index c18909388b..47b5e89a15 100644 --- a/api/types/User.js +++ b/api/types/User.js @@ -142,6 +142,7 @@ const User = /* GraphQL */ ` website: String username: LowercaseString timezone: Int + email: String } input UpgradeToProInput { diff --git a/shared/db/queries/user.js b/shared/db/queries/user.js index 303757824f..9b594c7ca5 100644 --- a/shared/db/queries/user.js +++ b/shared/db/queries/user.js @@ -84,6 +84,12 @@ export const getUserByEmail = createReadQuery((email: string) => ({ tags: (user: ?DBUser) => (user ? [user.id] : []), })); +export const getUsersByEmail = createReadQuery((email: string) => ({ + query: db.table('users').getAll(email, { index: 'email' }), + process: (users: Array) => users, + tags: (users: Array) => (users ? users.map(u => u && u.id) : []), +})); + export const getUserByUsername = createReadQuery((username: string) => ({ query: db.table('users').getAll(username, { index: 'username' }), process: (users: ?Array) => (users && users[0]) || null, @@ -306,6 +312,7 @@ export type EditUserInput = { coverFile?: FileUpload, username?: string, timezone?: number, + email?: string, }, }; diff --git a/src/views/userSettings/components/editForm.js b/src/views/userSettings/components/editForm.js index 5285c1b686..81af751e72 100644 --- a/src/views/userSettings/components/editForm.js +++ b/src/views/userSettings/components/editForm.js @@ -14,6 +14,7 @@ import { Input, TextArea, Error, + Success, PhotoInput, CoverInput, } from 'src/components/formElements'; @@ -36,6 +37,8 @@ import { import { Notice } from 'src/components/listItems/style'; import { SectionCard, SectionTitle } from 'src/components/settingsViews/style'; import type { Dispatch } from 'redux'; +import type { GetCurrentUserSettingsType } from 'shared/graphql/queries/user/getCurrentUserSettings'; +import isEmail from 'validator/lib/isEmail'; type State = { website: ?string, @@ -52,6 +55,9 @@ type State = { isLoading: boolean, photoSizeError: string, usernameError: string, + email: string, + emailError: string, + didChangeEmail: boolean, }; type Props = { @@ -59,13 +65,14 @@ type Props = { dispatch: Dispatch, client: Object, editUser: Function, + user: GetCurrentUserSettingsType, }; class UserWithData extends React.Component { constructor(props) { super(props); - const user = this.props.currentUser; + const user = this.props.user; this.state = { website: user.website ? user.website : '', @@ -82,6 +89,9 @@ class UserWithData extends React.Component { isLoading: false, photoSizeError: '', usernameError: '', + email: user.email ? user.email : '', + emailError: '', + didChangeEmail: false, }; } @@ -101,6 +111,24 @@ class UserWithData extends React.Component { }); }; + changeEmail = e => { + const email = e.target.value; + + if (!email || email.length === 0) { + return this.setState({ + email, + emailError: 'Your email can’t be blank', + didChangeEmail: false, + }); + } + + this.setState({ + email, + emailError: '', + didChangeEmail: false, + }); + }; + changeDescription = e => { const description = e.target.value; if (description.length >= 140) { @@ -199,8 +227,12 @@ class UserWithData extends React.Component { photoSizeError, username, usernameError, + email, + emailError, } = this.state; + const { user } = this.props; + const input = { name, description, @@ -208,9 +240,22 @@ class UserWithData extends React.Component { file, coverFile, username, + email, }; - if (photoSizeError || usernameError) { + if (!isEmail(email)) { + return this.setState({ + emailError: 'Please add a valid email address.', + }); + } + + if (email !== user.email) { + this.setState({ + didChangeEmail: true, + }); + } + + if (photoSizeError || usernameError || emailError) { return; } @@ -275,6 +320,9 @@ class UserWithData extends React.Component { isLoading, photoSizeError, usernameError, + email, + emailError, + didChangeEmail, } = this.state; const postAuthRedirectPath = `?r=${CLIENT_URL}/users/${username}/settings`; @@ -351,6 +399,21 @@ class UserWithData extends React.Component { Optional: Add your website + + Email + + + {didChangeEmail && ( + A confirmation email has been sent to {email}. + )} + {emailError && {emailError}} + { @@ -383,7 +446,12 @@ class UserWithData extends React.Component {