Skip to content
This repository was archived by the owner on Sep 21, 2020. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions db.json-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,10 @@
"generalLocationName": {
"type": "string",
"description": "A name for the location, generally 'City, State'."
},
"hasProfile": {
"type": "boolean",
"description": "True if this user has set up a profile"
}
},
"required": ["generalLocation", "generalLocationName"]
Expand Down
2 changes: 1 addition & 1 deletion firestore.rules
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ service cloud.firestore {
// Public user profiles
match /users_public/{userId} {
allow read;
allow write: if false; // only written to by indexUser cloud function
allow write: if isThisUser(userId);
}

// Private request data
Expand Down
2 changes: 1 addition & 1 deletion src/components/ClickableMap/ClickableMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ function ClickableMap({ onLocationChange, locationInfo }) {
data-test="address-entry"
{...getInputProps({
...params,
placeholder: 'Enter Address',
placeholder: 'Address Search',
})}
InputProps={{
...params.InputProps,
Expand Down
2 changes: 2 additions & 0 deletions src/constants/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export const ACCOUNT_PATH = '/account';
export const LOGIN_PATH = '/login';
export const LOGOUT_PATH = '/logout';
export const SEARCH_PATH = '/search';
export const USER_PROFILE_PATH = '/user-profile';
export const PASSWORD_RESET_PATH = '/password-reset';

export const PRIVACY_POLICY_PATH = '/privacy-policy';
export const TERMS_OF_SERVICE_PATH = '/terms-of-service';
Expand Down
9 changes: 6 additions & 3 deletions src/containers/Navbar/AccountMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import MenuItem from '@material-ui/core/MenuItem';
import IconButton from '@material-ui/core/IconButton';
import AccountCircle from '@material-ui/icons/AccountCircle';
import { makeStyles } from '@material-ui/core/styles';
import { ACCOUNT_PATH, MY_REQUESTS_PATH } from 'constants/paths';
import { USER_PROFILE_PATH, MY_REQUESTS_PATH } from 'constants/paths';

const useStyles = makeStyles(() => ({
buttonRoot: {
Expand Down Expand Up @@ -54,8 +54,11 @@ function AccountMenu() {
onClick={closeAccountMenu}>
My Requests
</MenuItem>
<MenuItem component={Link} to={ACCOUNT_PATH} onClick={closeAccountMenu}>
Account
<MenuItem
component={Link}
to={USER_PROFILE_PATH}
onClick={closeAccountMenu}>
Profile
</MenuItem>
<MenuItem onClick={handleLogout}>Sign Out</MenuItem>
</Menu>
Expand Down
8 changes: 5 additions & 3 deletions src/routes/Login/components/LoginForm/LoginForm.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useForm } from 'react-hook-form';
import TextField from '@material-ui/core/TextField';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import { TextField, makeStyles, Button } from '@material-ui/core';
import { Link } from 'react-router-dom';
import { validateEmail } from 'utils/form';
import { USER_PROFILE_PATH, PASSWORD_RESET_PATH } from 'constants/paths';
import styles from './LoginForm.styles';

const useStyles = makeStyles(styles);
Expand Down Expand Up @@ -50,6 +50,7 @@ function LoginForm({ onSubmit }) {
error={!!errors.password}
helperText={errors.password && 'Password is required'}
/>
<Link to={PASSWORD_RESET_PATH}>Forgot Password</Link>{' '}
<div className={classes.submit}>
<Button
color="primary"
Expand All @@ -59,6 +60,7 @@ function LoginForm({ onSubmit }) {
{isSubmitting ? 'Loading' : 'Login'}
</Button>
</div>
<Link to={USER_PROFILE_PATH}>I&apos;m new, sign me up!</Link>{' '}
</form>
);
}
Expand Down
73 changes: 25 additions & 48 deletions src/routes/Login/components/LoginPage/LoginPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { useAuth, useFirestore } from 'reactfire';
import Paper from '@material-ui/core/Paper';
import { makeStyles } from '@material-ui/core/styles';
import GoogleButton from 'react-google-button';
import { NEW_USER_PATH, SEARCH_PATH } from 'constants/paths';
import { USERS_COLLECTION } from 'constants/collections';
import { USER_PROFILE_PATH, SEARCH_PATH } from 'constants/paths';
import { USERS_PUBLIC_COLLECTION } from 'constants/collections';
import useNotifications from 'modules/notification/useNotifications';
import LoadingSpinner from 'components/LoadingSpinner';
import LoginForm from '../LoginForm';
Expand All @@ -20,33 +20,18 @@ function LoginPage() {
const history = useHistory();
const firestore = useFirestore();
const [isLoading, setLoadingState] = useState(false);
const { showError } = useNotifications();
const { showError, showMessage } = useNotifications();

async function updateUserAndRedirect(authState) {
try {
// Write user profile if it doesn't exist, otherwise redirect to search page
const userSnap = await firestore
.doc(`${USERS_COLLECTION}/${authState.user.uid}`)
.get();
// Redirect to search page if user exists
if (userSnap.get('preciseLocation')) {
history.replace(SEARCH_PATH);
} else {
// Write user object then redirect to new user page
const { email, displayName, photoURL, providerData } = authState.user;
const newProfile = { email, displayName, photoURL };
if (providerData && providerData.length) {
newProfile.providerData = [{ ...providerData[0] }];
}
await userSnap.ref.set(newProfile, { merge: true });
window.setTimeout(() => {
history.replace(NEW_USER_PATH);
}, 1000);
}
} catch (err) {
setLoadingState(false);
showError(err.message);
async function hasProfile(user) {
const userSnap = await firestore
.doc(`${USERS_PUBLIC_COLLECTION}/${user.uid}`)
.get();
const data = await userSnap.data();
console.log('Checking account', data); // eslint-disable-line no-console
if (!!data && !!data.d && !!data.d.hasProfile) {
return true;
}
return false;
}

async function googleLogin() {
Expand All @@ -58,8 +43,12 @@ function LoginPage() {
const signInMethod = isMobile ? 'signInWithRedirect' : 'signInWithPopup';
try {
const authState = await auth[signInMethod](provider);
// Write user profile if it doesn't exist, otherwise redirect to search page
await updateUserAndRedirect(authState);
if (await hasProfile(authState.user)) {
history.replace(SEARCH_PATH);
return;
}
showMessage('You need to finish filling out your profile.');
history.replace(USER_PROFILE_PATH);
} catch (err) {
setLoadingState(false);
showError(err.message);
Expand All @@ -72,26 +61,14 @@ function LoginPage() {
creds.email,
creds.password,
);
// Write user profile if it doesn't exist, otherwise redirect to search page
await updateUserAndRedirect(authState);
} catch (err) {
try {
// Create user if they do not exist
if (err.code === 'auth/user-not-found') {
const authState = await auth.createUserWithEmailAndPassword(
creds.email,
creds.password,
);
// Write user profile if it doesn't exist, otherwise redirect to search page
await updateUserAndRedirect(authState);
}
} catch (err2) {
if (err2.message === 'auth/user-exists') {
showError(err.message);
} else {
showError(err2.message);
}
if (await hasProfile(authState.user)) {
history.replace(SEARCH_PATH);
return;
}
showMessage('You need to finish filling out your profile.');
history.replace(USER_PROFILE_PATH);
} catch (err) {
showError(err.message);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React from 'react';
import { validateEmail } from 'utils/form';
import { useAuth } from 'reactfire';
import { useForm } from 'react-hook-form';
import {
Paper,
Typography,
Grid,
TextField,
makeStyles,
Button,
Container,
} from '@material-ui/core';
import { Helmet } from 'react-helmet';
import useNotifications from 'modules/notification/useNotifications';
import styles from './PasswordResetPage.styles';

const useStyles = makeStyles(styles);

function PasswordReset() {
const classes = useStyles();
const auth = useAuth();
const { showError, showMessage } = useNotifications();

const {
register,
handleSubmit,
errors,
getValues,
formState: { isSubmitting, isValid },
} = useForm({
mode: 'onChange',
nativeValidation: false,
});

async function handleLinkSend() {
try {
await auth.sendPasswordResetEmail(getValues('email'));
} catch (err) {
showError(err.message);
return;
}
showMessage('Email sent.');
}

return (
<Container maxWidth="md">
<Helmet>
<title>Password Reset</title>
</Helmet>
<Typography variant="h4" gutterBottom>
Password Reset
</Typography>

<div className={classes.root}>
<Paper className={classes.paper}>
<form
className={classes.root}
onSubmit={handleSubmit(handleLinkSend)}>
<Grid container justify="center" spacing={1}>
<Typography gutterBottom>
Enter your email address, and if you have an account you will be
emailed a link to reset your password.
</Typography>
<Grid container justify="center" spacing={1}>
<Grid item sm={6}>
<TextField
type="email"
name="email"
label="Email"
variant="outlined"
margin="normal"
autoComplete="email"
fullWidth
inputRef={register({
required: true,
validate: validateEmail,
})}
error={!!errors.email}
helperText={errors.email && 'Email must be valid'}
/>
</Grid>
</Grid>

<div className={classes.submit}>
<Button
color="primary"
type="submit"
variant="contained"
disabled={isSubmitting || !isValid}>
{isSubmitting ? 'Sending...' : 'Send Link'}
</Button>
</div>
</Grid>
</form>
</Paper>
</div>
</Container>
);
}

export default PasswordReset;
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export default (theme) => ({
root: {
...theme.flexColumnCenter,
justifyContent: 'flex-start',
flexGrow: 1,
height: '100%',
width: '100%',
margin: '.2rem',
},
submit: {
...theme.flexColumnCenter,
justifyContent: 'center',
flexGrow: 1,
textAlign: 'center',
padding: '1.25rem',
minWidth: '192px',
marginTop: '1.5rem',
},
paper: {
paddingTop: theme.spacing(3),
paddingBottom: theme.spacing(1),
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import PasswordResetPage from './PasswordResetPage';

export default PasswordResetPage;
11 changes: 11 additions & 0 deletions src/routes/PasswordReset/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { loadable } from 'utils/router';
import { PASSWORD_RESET_PATH as path } from 'constants/paths';

export default {
path,
component: loadable(() =>
import(
/* webpackChunkName: 'Password Reset' */ './components/PasswordResetPage'
),
),
};
Loading