Completed Login/Logout/Protected Routes

This commit is contained in:
Alexander Wong 2017-09-03 10:41:10 -06:00
parent 83ddd34c7c
commit 1bf1dad0b9
14 changed files with 342 additions and 54 deletions

View File

@ -8,7 +8,7 @@ import {
CLEAR_EMAIL_VERIFICATION_SUCCESS, CLEAR_EMAIL_VERIFICATION_SUCCESS,
SET_EMAIL_VERIFICATION_ERROR, SET_EMAIL_VERIFICATION_ERROR,
CLEAR_EMAIL_VERIFICATION_ERROR, CLEAR_EMAIL_VERIFICATION_ERROR,
SET_SELF_USER, SET_SELF_USER_TOKEN,
SET_FORM_EMAIL, SET_FORM_EMAIL,
SET_FORM_PASSWORD, SET_FORM_PASSWORD,
SET_FORM_PASSWORD_CONFIRMATION, SET_FORM_PASSWORD_CONFIRMATION,
@ -103,9 +103,9 @@ export function clearEmailVerificationSuccess() {
}; };
} }
export function setSelfUser(selfUser) { export function setSelfUserToken(selfUser) {
return { return {
type: SET_SELF_USER, type: SET_SELF_USER_TOKEN,
data: selfUser data: selfUser
}; };
} }

View File

@ -1,18 +1,33 @@
import { import {
SEND_REGISTER_REQUEST, SEND_REGISTER_REQUEST,
SEND_EMAIL_VERIFICATION_REQUEST SEND_EMAIL_VERIFICATION_REQUEST,
SEND_LOGIN_REQUEST,
SEND_LOGOUT_REQUEST
} from "../../constants/auth.constants"; } from "../../constants/auth.constants";
export function sendRegisterRequest(postbody) { export function sendRegisterRequest(postBody) {
return { return {
type: SEND_REGISTER_REQUEST, type: SEND_REGISTER_REQUEST,
data: postbody data: postBody
}; };
} }
export function sendEmailVerificationRequest(postbody) { export function sendEmailVerificationRequest(postBody) {
return { return {
type: SEND_EMAIL_VERIFICATION_REQUEST, type: SEND_EMAIL_VERIFICATION_REQUEST,
data: postbody data: postBody
}; };
} }
export function sendLoginRequest(postBody) {
return {
type: SEND_LOGIN_REQUEST,
data: postBody
};
}
export function sendLogoutRequest() {
return {
type: SEND_LOGOUT_REQUEST
}
}

View File

@ -11,15 +11,33 @@ export function registerUser(email, password1, password2) {
email, email,
password1, password1,
password2 password2
}).then(response => { }).then(resp => Promise.resolve(resp));
return Promise.resolve(response);
});
} }
/**
* Function wrapping POST request for email validation
* @param {string} emailKey - key for email validation
*/
export function verifyEmail(emailKey) { export function verifyEmail(emailKey) {
return post("/rest-auth/registration/verify-email/", { return post("/rest-auth/registration/verify-email/", {
key: emailKey key: emailKey
}).then(response => { }).then(resp => Promise.resolve(resp));
return Promise.resolve(response); }
});
/**
* Function wrapping POST request for user login
* @param {string} email - email of user to login
* @param {string} password - password of user to login
*/
export function loginUser(email, password) {
return post("/rest-auth/login/", { email, password }).then(resp =>
Promise.resolve(resp)
);
}
/**
* Function wrapping POST request for user logout
*/
export function logoutUser() {
return post("/rest-auth/logout/").then(resp => Promise.resolve(resp));
} }

View File

@ -9,7 +9,7 @@ const localStorage = global.process && process.env.NODE_ENV === "test"
: global.window.localStorage; : global.window.localStorage;
function headers() { function headers() {
const token = localStorage.getItem("token") || ""; const token = localStorage.getItem("userToken") || "";
return { return {
Accept: "application/json", Accept: "application/json",

View File

@ -3,7 +3,9 @@ import { Route, Switch } from "react-router-dom";
import Login from "./Auth/Login"; import Login from "./Auth/Login";
import Register from "./Auth/Register"; import Register from "./Auth/Register";
import Settings from "./Auth/Settings";
import VerifyEmail from "./Auth/VerifyEmail"; import VerifyEmail from "./Auth/VerifyEmail";
import PrivateRoute from "./Shared/PrivateRoute";
import About from "./Static/About"; import About from "./Static/About";
import Footer from "./Static/Footer"; import Footer from "./Static/Footer";
import Home from "./Static/Home"; import Home from "./Static/Home";
@ -31,6 +33,7 @@ class App extends Component {
path="/auth/verify-email/:emailKey" path="/auth/verify-email/:emailKey"
component={VerifyEmail} component={VerifyEmail}
/> />
<PrivateRoute path="/auth/settings" component={Settings} />
<Route component={NoMatch} /> <Route component={NoMatch} />
</Switch> </Switch>
</div> </div>

View File

@ -1,16 +1,111 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { Container } from "semantic-ui-react"; import { connect } from "react-redux";
import { Redirect } from "react-router-dom";
import { Container, Form, Header, Message } from "semantic-ui-react";
import {
clearAuthRequestError,
clearAuthRequestSuccess,
setFormEmail,
setFormPassword
} from "../../actions/auth/reducer.actions";
import { sendLoginRequest } from "../../actions/auth/saga.actions";
import Error from "../Shared/Error";
class Login extends Component { class Login extends Component {
constructor(props) {
super(props);
this.props.dispatch(clearAuthRequestError());
this.props.dispatch(clearAuthRequestSuccess());
}
changeEmail = event => {
this.props.dispatch(setFormEmail(event.target.value));
};
changePassword = event => {
this.props.dispatch(setFormPassword(event.target.value));
};
onSubmitLogin = event => {
event.preventDefault();
const { dispatch, email, password } = this.props;
dispatch(sendLoginRequest({ email, password }));
};
render() { render() {
return <LoginView />; const {
isSendingAuthRequest,
authRequestError,
authRequestSuccess,
email,
password,
userToken
} = this.props;
if (userToken) return <Redirect to={"/"} />;
return (
<LoginView
isSendingAuthRequest={isSendingAuthRequest}
authRequestError={authRequestError}
authRequestSuccess={authRequestSuccess}
email={email}
password={password}
changeEmail={this.changeEmail}
changePassword={this.changePassword}
onSubmitLogin={this.onSubmitLogin}
/>
);
} }
} }
const LoginView = () => ( function mapStateToProps(state) {
return { ...state.auth };
}
const LoginView = ({
isSendingAuthRequest,
authRequestError,
authRequestSuccess,
email,
password,
changeEmail,
changePassword,
onSubmitLogin
}) => (
<Container> <Container>
<p>Login</p> <Header>Login</Header>
<Form
loading={isSendingAuthRequest}
onSubmit={onSubmitLogin}
error={!!authRequestError}
success={!!authRequestSuccess}
>
<Form.Field>
<label>Email</label>
<input
placeholder="bob@gmail.com"
type="email"
value={email}
onChange={changeEmail}
/>
</Form.Field>
<Form.Field>
<label>Password</label>
<input
placeholder="••••••••"
type="password"
value={password}
onChange={changePassword}
/>
</Form.Field>
<Error header="Login failed!" error={authRequestError} />
<Message success>
<Message.Header>Login successful!</Message.Header>
<p>Redirecting you now...</p>
</Message>
<Form.Button>Login</Form.Button>
</Form>
</Container> </Container>
); );
export default Login; export default connect(mapStateToProps)(Login);

View File

@ -1,5 +1,6 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Redirect } from "react-router-dom";
import { Container, Form, Header, Message } from "semantic-ui-react"; import { Container, Form, Header, Message } from "semantic-ui-react";
import { import {
@ -50,8 +51,10 @@ class Register extends Component {
authRequestSuccess, authRequestSuccess,
email, email,
password, password,
passwordConfirmation passwordConfirmation,
userToken
} = this.props; } = this.props;
if (userToken) return <Redirect to={"/"} />;
return ( return (
<RegisterView <RegisterView
isSendingAuthRequest={isSendingAuthRequest} isSendingAuthRequest={isSendingAuthRequest}

View File

@ -0,0 +1,22 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import { Container } from "semantic-ui-react";
class Settings extends Component {
render() {
return <SettingsView />;
}
}
function mapStateToProps(state) {
return { ...state.auth };
}
const SettingsView = () => (
<Container>
<h1>Settings</h1>
<p>todo, change password</p>
</Container>
);
export default connect(mapStateToProps)(Settings);

View File

@ -1,28 +1,61 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Menu } from "semantic-ui-react"; import { Dropdown, Menu } from "semantic-ui-react";
import { sendLogoutRequest } from "../actions/auth/saga.actions";
class Navbar extends Component { class Navbar extends Component {
dispatchLogoutRequest = () => {
this.props.dispatch(sendLogoutRequest());
};
render() { render() {
const { userToken } = this.props;
return ( return (
<Menu> <NavbarView
<Menu.Item as={Link} to="/"> isAuthenticated={!!userToken}
Caremyway dispatchLogoutRequest={this.dispatchLogoutRequest}
</Menu.Item> />
<Menu.Item as={Link} to="/about">
About
</Menu.Item>
<Menu.Menu position="right">
<Menu.Item as={Link} to="/auth/login">
Login
</Menu.Item>
<Menu.Item as={Link} to="/auth/register">
Register
</Menu.Item>
</Menu.Menu>
</Menu>
); );
} }
} }
export default Navbar; function mapStateToProps(state) {
return { ...state.auth };
}
const NavbarView = ({ isAuthenticated, dispatchLogoutRequest }) => (
<Menu>
<Menu.Item as={Link} to="/">
Caremyway
</Menu.Item>
<Menu.Item as={Link} to="/about">
About
</Menu.Item>
{!isAuthenticated &&
<Menu.Menu position="right">
<Menu.Item as={Link} to="/auth/login">
Login
</Menu.Item>
<Menu.Item as={Link} to="/auth/register">
Register
</Menu.Item>
</Menu.Menu>}
{!!isAuthenticated &&
<Menu.Menu position="right">
<Dropdown item text="Account">
<Dropdown.Menu>
<Dropdown.Item as={Link} to="/auth/settings">
Settings
</Dropdown.Item>
<Dropdown.Item onClick={dispatchLogoutRequest}>
Logout
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</Menu.Menu>}
</Menu>
);
export default connect(mapStateToProps)(Navbar);

View File

@ -0,0 +1,29 @@
import PropTypes from "prop-types";
import React from "react";
import { connect } from "react-redux";
import { Redirect, Route } from "react-router-dom";
const propTypes = {
userToken: PropTypes.string,
component: PropTypes.oneOfType([PropTypes.element, PropTypes.func]).isRequired
};
const PrivateRoute = ({ userToken, component, ...rest }) => {
return (
<Route
{...rest}
render={props => {
if (!!userToken) return React.createElement(component, props);
return <Redirect to="/auth/login" />;
}}
/>
);
};
PrivateRoute.propTypes = propTypes;
const mapStateToProps = state => ({
userToken: state.auth.userToken
});
export default connect(mapStateToProps)(PrivateRoute);

View File

@ -9,7 +9,7 @@ export const CLEAR_EMAIL_VERIFICATION_SUCCESS =
"CLEAR_EMAIL_VERIFICATION_SUCCESS"; "CLEAR_EMAIL_VERIFICATION_SUCCESS";
export const SET_EMAIL_VERIFICATION_ERROR = "SET_EMAIL_VERIFICATION_ERROR"; export const SET_EMAIL_VERIFICATION_ERROR = "SET_EMAIL_VERIFICATION_ERROR";
export const CLEAR_EMAIL_VERIFICATION_ERROR = "CLEAR_EMAIL_VERIFICATION_ERROR"; export const CLEAR_EMAIL_VERIFICATION_ERROR = "CLEAR_EMAIL_VERIFICATION_ERROR";
export const SET_SELF_USER = "SET_SELF_USER"; export const SET_SELF_USER_TOKEN = "SET_SELF_USER_TOKEN";
export const SET_FORM_EMAIL = "SET_FORM_EMAIL"; export const SET_FORM_EMAIL = "SET_FORM_EMAIL";
export const SET_FORM_PASSWORD = "SET_FORM_PASSWORD"; export const SET_FORM_PASSWORD = "SET_FORM_PASSWORD";
export const SET_FORM_PASSWORD_CONFIRMATION = "SET_FORM_PASSWORD_CONFIRMATION"; export const SET_FORM_PASSWORD_CONFIRMATION = "SET_FORM_PASSWORD_CONFIRMATION";
@ -19,3 +19,5 @@ export const SET_FORM_EMAIL_VERIFICATION = "SET_FORM_EMAIL_VERIFICATION";
export const SEND_REGISTER_REQUEST = "SEND_REGISTER_REQUEST"; export const SEND_REGISTER_REQUEST = "SEND_REGISTER_REQUEST";
export const SEND_EMAIL_VERIFICATION_REQUEST = export const SEND_EMAIL_VERIFICATION_REQUEST =
"SEND_EMAIL_VERIFICATION_REQUEST"; "SEND_EMAIL_VERIFICATION_REQUEST";
export const SEND_LOGIN_REQUEST = "SEND_LOGIN_REQUEST";
export const SEND_LOGOUT_REQUEST = "SEND_LOGOUT_REQUEST";

View File

@ -8,20 +8,32 @@ import {
CLEAR_EMAIL_VERIFICATION_ERROR, CLEAR_EMAIL_VERIFICATION_ERROR,
SET_EMAIL_VERIFICATION_SUCCESS, SET_EMAIL_VERIFICATION_SUCCESS,
CLEAR_EMAIL_VERIFICATION_SUCCESS, CLEAR_EMAIL_VERIFICATION_SUCCESS,
SET_SELF_USER, SET_SELF_USER_TOKEN,
SET_FORM_EMAIL, SET_FORM_EMAIL,
SET_FORM_PASSWORD, SET_FORM_PASSWORD,
SET_FORM_PASSWORD_CONFIRMATION, SET_FORM_PASSWORD_CONFIRMATION,
SET_FORM_EMAIL_VERIFICATION SET_FORM_EMAIL_VERIFICATION
} from "../constants/auth.constants"; } from "../constants/auth.constants";
/**
* Set the user's auth token, and return the value
* @param {string} newToken
*/
function me(newToken) {
if (typeof newToken === "string") {
localStorage.setItem("userToken", newToken);
}
const userToken = localStorage.getItem("userToken");
return userToken ? userToken : "";
}
const initialState = { const initialState = {
isSendingAuthRequest: false, isSendingAuthRequest: false,
authRequestError: "", authRequestError: "",
authRequestSuccess: "", authRequestSuccess: "",
emailVerificationRequestError: "", emailVerificationRequestError: "",
emailVerificationRequestSuccess: "", emailVerificationRequestSuccess: "",
currentUser: {}, userToken: me(null),
email: "", email: "",
password: "", password: "",
passwordConfirmation: "", passwordConfirmation: "",
@ -75,10 +87,10 @@ function authReducer(state = initialState, action) {
...state, ...state,
emailVerificationRequestSuccess: "" emailVerificationRequestSuccess: ""
}; };
case SET_SELF_USER: case SET_SELF_USER_TOKEN:
return { return {
...state, ...state,
currentUser: action.data userToken: me(action.data)
}; };
case SET_FORM_EMAIL: case SET_FORM_EMAIL:
return { return {

View File

@ -12,9 +12,15 @@ import {
setFormEmail, setFormEmail,
setFormPassword, setFormPassword,
setFormPasswordConfirmation, setFormPasswordConfirmation,
setFormEmailVerification setFormEmailVerification,
setSelfUserToken
} from "../actions/auth/reducer.actions"; } from "../actions/auth/reducer.actions";
import { registerUser, verifyEmail } from "../api/auth.api"; import {
registerUser,
verifyEmail,
loginUser,
logoutUser
} from "../api/auth.api";
function* registerUserCall(postBody) { function* registerUserCall(postBody) {
yield effects.put(isSendingAuthRequest(true)); yield effects.put(isSendingAuthRequest(true));
@ -42,12 +48,29 @@ function* verifyEmailCall(postBody) {
} }
} }
function* loginUserCall(postBody) {
yield effects.put(isSendingAuthRequest(true));
const { email, password } = postBody;
try {
return yield effects.call(loginUser, email, password);
} catch (exception) {
yield effects.put(setAuthRequestError(exception));
return false;
} finally {
yield effects.put(isSendingAuthRequest(false));
}
}
function* logoutUserCall() {
yield effects.call(logoutUser);
}
export function* registerUserFlow(request) { export function* registerUserFlow(request) {
yield effects.put(clearAuthRequestSuccess()); yield effects.put(clearAuthRequestSuccess());
yield effects.put(clearAuthRequestError()); yield effects.put(clearAuthRequestError());
const wasSucessful = yield effects.call(registerUserCall, request.data); const wasSuccessful = yield effects.call(registerUserCall, request.data);
if (wasSucessful) { if (wasSuccessful) {
yield effects.put(setAuthRequestSuccess(wasSucessful)); yield effects.put(setAuthRequestSuccess(wasSuccessful));
yield effects.put(clearAuthRequestError()); yield effects.put(clearAuthRequestError());
yield effects.put(setFormEmail("")); yield effects.put(setFormEmail(""));
yield effects.put(setFormPassword("")); yield effects.put(setFormPassword(""));
@ -58,10 +81,31 @@ export function* registerUserFlow(request) {
export function* verifyEmailFlow(request) { export function* verifyEmailFlow(request) {
yield effects.put(clearEmailVerificationSuccess()); yield effects.put(clearEmailVerificationSuccess());
yield effects.put(clearEmailVerificationError()); yield effects.put(clearEmailVerificationError());
const wasSucessful = yield effects.call(verifyEmailCall, request.data); const wasSuccessful = yield effects.call(verifyEmailCall, request.data);
if (wasSucessful) { if (wasSuccessful) {
yield effects.put(setEmailVerificationSuccess(wasSucessful)); yield effects.put(setEmailVerificationSuccess(wasSuccessful));
yield effects.put(clearEmailVerificationError()); yield effects.put(clearEmailVerificationError());
yield effects.put(setFormEmailVerification("")); yield effects.put(setFormEmailVerification(""));
} }
} }
export function* loginUserFlow(request) {
yield effects.put(clearAuthRequestSuccess());
yield effects.put(clearAuthRequestError());
const wasSuccessful = yield effects.call(loginUserCall, request.data);
if (wasSuccessful) {
yield effects.put(setSelfUserToken(wasSuccessful.key));
yield effects.put(setAuthRequestSuccess(wasSuccessful));
yield effects.put(clearAuthRequestError());
yield effects.put(setFormEmail(""));
yield effects.put(setFormPassword(""));
yield effects.put(setFormPasswordConfirmation(""));
}
}
export function* logoutUserFlow(request) {
yield effects.put(clearAuthRequestSuccess());
yield effects.put(clearAuthRequestError());
yield effects.call(logoutUserCall);
yield effects.put(setSelfUserToken(""));
}

View File

@ -1,8 +1,20 @@
import { takeLatest } from "redux-saga/effects"; import { takeLatest } from "redux-saga/effects";
import { SEND_REGISTER_REQUEST, SEND_EMAIL_VERIFICATION_REQUEST } from "../constants/auth.constants"; import {
import { registerUserFlow, verifyEmailFlow } from "./auth.sagas"; SEND_REGISTER_REQUEST,
SEND_EMAIL_VERIFICATION_REQUEST,
SEND_LOGIN_REQUEST,
SEND_LOGOUT_REQUEST
} from "../constants/auth.constants";
import {
registerUserFlow,
verifyEmailFlow,
loginUserFlow,
logoutUserFlow
} from "./auth.sagas";
export default function* rootSaga() { export default function* rootSaga() {
yield takeLatest(SEND_REGISTER_REQUEST, registerUserFlow); yield takeLatest(SEND_REGISTER_REQUEST, registerUserFlow);
yield takeLatest(SEND_EMAIL_VERIFICATION_REQUEST, verifyEmailFlow); yield takeLatest(SEND_EMAIL_VERIFICATION_REQUEST, verifyEmailFlow);
yield takeLatest(SEND_LOGIN_REQUEST, loginUserFlow);
yield takeLatest(SEND_LOGOUT_REQUEST, logoutUserFlow);
} }