Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Implement email login flow #1183

Closed
wants to merge 1 commit into from
Closed
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"dependencies": {
"apollo-client": "^1.0.0-rc.2",
"babel-polyfill": "^6.22.0",
"bcrypt": "^1.0.2",
"bluebird": "^3.4.7",
"body-parser": "^1.16.0",
"classnames": "^2.2.5",
Expand Down
7 changes: 7 additions & 0 deletions src/actions/login.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
mutation login($usernameOrEmail: String!, $password: String!) {
login(usernameOrEmail: $usernameOrEmail, password: $password) {
id
email
username
}
}
39 changes: 39 additions & 0 deletions src/actions/login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
LOGIN_START,
LOGIN_SUCCESS,
LOGIN_ERROR,
} from '../constants';


import mutateLogin from './login.graphql';


export default function login({ usernameOrEmail, password }) {
return async (dispatch, getState, { graphqlRequest }) => {
dispatch({
type: LOGIN_START,
});

try {
const { data } = await graphqlRequest(mutateLogin,
{ usernameOrEmail, password }, { skipCache: true });
const user = data.user;
console.log(data, user);

dispatch({
type: LOGIN_SUCCESS,
payload: user,
});
// TODO save token
} catch (error) {
dispatch({
type: LOGIN_ERROR,
payload: {
error,
},
});
return false;
}
return true;
};
}
5 changes: 4 additions & 1 deletion src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ export const analytics = {

export const auth = {

jwt: { secret: process.env.JWT_SECRET || 'React Starter Kit' },
jwt: {
secret: process.env.JWT_SECRET || 'React Starter Kit',
expires: 60 * 60 * 24 * 180, // 180 days
},

// https://developers.facebook.com/
facebook: {
Expand Down
4 changes: 4 additions & 0 deletions src/constants/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/* eslint-disable import/prefer-default-export */

export const SET_RUNTIME_VARIABLE = 'SET_RUNTIME_VARIABLE';

export const LOGIN_START = 'LOGIN_START';
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
export const LOGIN_ERROR = 'LOGIN_ERROR';
12 changes: 12 additions & 0 deletions src/data/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,15 @@ const User = Model.define('User', {
primaryKey: true,
},

username: {
type: DataType.STRING(100),
unique: true,
allowNull: false,
},

email: {
type: DataType.STRING(255),
unique: true,
validate: { isEmail: true },
},

Expand All @@ -28,6 +35,11 @@ const User = Model.define('User', {
defaultValue: false,
},

password: {
type: DataType.STRING,
allowNull: false,
},

}, {

indexes: [
Expand Down
39 changes: 39 additions & 0 deletions src/data/mutations/login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@

import {
GraphQLString as StringType,
GraphQLNonNull as NonNull,
} from 'graphql';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';

import { User } from '../models';
import { auth } from '../../config';
import UserType from '../types/UserType';

const login = {
type: UserType,
args: {
usernameOrEmail: { type: new NonNull(StringType) },
password: { type: new NonNull(StringType) },
},
async resolve({ request }, { usernameOrEmail, password }) {
const usernameOrEmailLC = usernameOrEmail.toLowerCase();
const passwordHashed = bcrypt.hashSync(password.trim(), 10);

const user = User.findOne({
attributes: ['id', 'email', 'username'],
where: {
$or: [{ username: usernameOrEmailLC }, { email: usernameOrEmailLC }],
password: passwordHashed,
},
});

if (!user) {
throw new Error('username/email or password is wrong');
}
user.token = jwt.sign({ id: user.id }, auth.jwt.secret, { expiresIn: auth.jwt.expires });
return user;
},
};

export default login;
2 changes: 2 additions & 0 deletions src/data/queries/me.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const me = {
return request.user && {
id: request.user.id,
email: request.user.email,
username: request.user.username,
token: request.user.token,
};
},
};
Expand Down
9 changes: 9 additions & 0 deletions src/data/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
import me from './queries/me';
import news from './queries/news';

import login from './mutations/login';

const schema = new Schema({
query: new ObjectType({
name: 'Query',
Expand All @@ -23,6 +25,13 @@ const schema = new Schema({
news,
},
}),

mutation: new ObjectType({
name: 'Mutation',
fields: {
login,
},
}),
});

export default schema;
2 changes: 2 additions & 0 deletions src/data/types/UserType.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const UserType = new ObjectType({
fields: {
id: { type: new NonNull(ID) },
email: { type: StringType },
username: { type: StringType },
token: { type: StringType },
},
});

Expand Down
33 changes: 33 additions & 0 deletions src/reducers/user.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,38 @@
import {
LOGIN_START,
LOGIN_SUCCESS,
LOGIN_ERROR,
} from '../constants';

const INITIAL_STATE = {
loading: false,
};

export default function user(state = {}, action) {
if (state === null) { // server doesn't suppprt state = {}
return INITIAL_STATE;
}
switch (action.type) {
case LOGIN_START:
return {
...state,
loading: true,
user: null,
};
case LOGIN_SUCCESS:
return {
...state,
loading: false,
error: null,
user: action.payload.user,
};
case LOGIN_ERROR:
return {
...state,
loading: false,
error: action.payload.error,
user: null,
};
default:
return state;
}
Expand Down
39 changes: 36 additions & 3 deletions src/routes/login/Login.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,31 @@
*/

import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import withStyles from 'isomorphic-style-loader/lib/withStyles';

import login from '../../actions/login';

import s from './Login.css';

class Login extends React.Component {
static propTypes = {
title: PropTypes.string.isRequired,
};

state = {
usernameOrEmail: 'jamalx31',
password: 'hello',
}

handleSubmit = (e) => {
e.preventDefault();
const { usernameOrEmail, password } = this.state;
this.props.login({ usernameOrEmail, password });
}

render() {
const { usernameOrEmail, password } = this.state;
return (
<div className={s.root}>
<div className={s.container}>
Expand Down Expand Up @@ -84,13 +100,15 @@ class Login extends React.Component {
</a>
</div>
<strong className={s.lineThrough}>OR</strong>
<form method="post">
<form onSubmit={this.handleSubmit}>
<div className={s.formGroup}>
<label className={s.label} htmlFor="usernameOrEmail">
Username or email address:
</label>
<input
className={s.input}
value={usernameOrEmail}
onChange={e => this.setState({ usernameOrEmail: e.target.value })}
id="usernameOrEmail"
type="text"
name="usernameOrEmail"
Expand All @@ -103,13 +121,15 @@ class Login extends React.Component {
</label>
<input
className={s.input}
value={password}
onChange={e => this.setState({ password: e.target.value })}
id="password"
type="password"
name="password"
/>
</div>
<div className={s.formGroup}>
<button className={s.button} type="submit">
<button className={s.button} type="submit" disabled={this.props.loading}>
Log in
</button>
</div>
Expand All @@ -120,4 +140,17 @@ class Login extends React.Component {
}
}

export default withStyles(s)(Login);
Login.propTypes = {
loading: PropTypes.bool.isRequired,
login: PropTypes.func.isRequired,
};

const mapState = state => ({
loading: state.user.loading,
});

const mapDispatch = {
login,
};

export default connect(mapState, mapDispatch)(withStyles(s)(Login));