Finished functionally complete client registration workflow

This commit is contained in:
Alexander Wong
2017-08-29 19:51:45 -06:00
parent aaad6ea3eb
commit 415aea74a3
25 changed files with 587 additions and 66 deletions

View File

@@ -0,0 +1,91 @@
import {
IS_SENDING_AUTH_REQUEST,
SET_AUTH_REQUEST_ERROR,
CLEAR_AUTH_REQUEST_ERROR,
SET_AUTH_REQUEST_SUCCESS,
CLEAR_AUTH_REQUEST_SUCCESS,
SET_SELF_USER,
SET_FORM_EMAIL,
SET_FORM_PASSWORD,
SET_FORM_PASSWORD_CONFIRMATION
} from "../../constants/auth.constants";
import { parseError } from "../common.actions";
export function isSendingAuthRequest(sendingRequest) {
return {
type: IS_SENDING_AUTH_REQUEST,
data: sendingRequest
};
}
export function setAuthRequestError(exception) {
let rawError = parseError(exception);
if (rawError.email) {
rawError["Email"] = rawError.email;
delete rawError["email"];
}
if (rawError.password1) {
rawError["Password"] = rawError.password1;
delete rawError["password1"];
}
if (rawError.password2) {
rawError["Password Confirmation"] = rawError.password2;
delete rawError["password2"];
}
if (rawError.non_field_errors) {
rawError["Non Field Errors"] = rawError.non_field_errors;
delete rawError["non_field_errors"];
}
return {
type: SET_AUTH_REQUEST_ERROR,
data: parseError(exception)
};
}
export function clearAuthRequestError() {
return {
type: CLEAR_AUTH_REQUEST_ERROR
};
}
export function setAuthRequestSuccess(response) {
return {
type: SET_AUTH_REQUEST_SUCCESS,
data: response.detail || response
}
}
export function clearAuthRequestSuccess() {
return {
type: CLEAR_AUTH_REQUEST_SUCCESS
}
}
export function setSelfUser(selfUser) {
return {
type: SET_SELF_USER,
data: selfUser
};
}
export function setFormEmail(email) {
return {
type: SET_FORM_EMAIL,
data: email
};
}
export function setFormPassword(password) {
return {
type: SET_FORM_PASSWORD,
data: password
};
}
export function setFormPasswordConfirmation(passwordConfirmation) {
return {
type: SET_FORM_PASSWORD_CONFIRMATION,
data: passwordConfirmation
};
}

View File

@@ -0,0 +1,10 @@
import {
SEND_REGISTER_REQUEST
} from "../../constants/auth.constants";
export function sendRegisterRequest(postbody) {
return {
type: SEND_REGISTER_REQUEST,
data: postbody
}
}

View File

@@ -0,0 +1,13 @@
/**
* Given an exception return the list of errors, the singular error, or generic error
* @param {object|string} exception - axios returned exception
*/
export function parseError(exception) {
let response = exception.response || {};
let data = response.data || {};
let err = "" + exception;
if (response.status) {
err = `${response.status} ${response.statusText}`;
}
return data || err
}

17
src/api/auth.api.js Normal file
View File

@@ -0,0 +1,17 @@
import { post } from "./baseApi";
/**
* Function wrapping POST request for user registration
* @param {string} email - email of user to register
* @param {string} password1 - password of user to register
* @param {string} password2 - server side password confirmation
*/
export function registerUser(email, password1, password2) {
return post("/rest-auth/registration/", {
email,
password1,
password2
}).then(response => {
return Promise.resolve(response);
});
}

52
src/api/baseApi.js Normal file
View File

@@ -0,0 +1,52 @@
import axios from "axios";
const API_ENDPOINT = process.env.REACT_APP_API_ENDPOINT;
// If testing, use localStorage polyfill, else use browser localStorage
const localStorage = global.process && process.env.NODE_ENV === "test"
? // eslint-disable-next-line import/no-extraneous-dependencies
require("localStorage")
: global.window.localStorage;
function headers() {
const token = localStorage.getItem("token") || "";
return {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Token: ${token}`
};
}
const apiInstance = axios.create({
baseURL: API_ENDPOINT,
timeout: 3000,
});
export function get(url, params = {}) {
return apiInstance
.get(url, {params, headers: headers()})
.then(response => response.data)
.catch(error => Promise.reject(error));
}
export function post(url, data) {
return apiInstance
.post(url, data, {headers: headers()})
.then(response => response.data)
.catch(error => Promise.reject(error));
}
export function patch(url, data) {
return apiInstance
.patch(url, data, {headers: headers()})
.then(response => response.data)
.catch(error => Promise.reject(error));
}
export function del(url) {
return apiInstance
.delete(url, {headers: headers()})
.then(response => response.data)
.catch(error => Promise.reject(error));
}

View File

@@ -1,23 +1,32 @@
import React, { Component } from "react";
import { Route, Switch } from "react-router-dom";
import Login from "./Auth/Login";
import Register from "./Auth/Register";
import About from "./Static/About";
import Footer from "./Static/Footer";
import Home from "./Static/Home";
import NoMatch from "./Static/NoMatch";
import Navbar from "./Navbar";
import Footer from "./Footer";
import Home from "./Home";
import About from "./About";
import Topics from "./Topics";
class App extends Component {
render() {
const footSmash = {
display: "flex",
minHeight: "calc(100vh - 1px)",
flexDirection: "column"
};
return (
<div>
<Navbar />
<div style={{ display: "flex", minHeight: "calc(100vh - 1px)", flexDirection: "column" }}>
<div style={footSmash}>
<div style={{ flex: "1" }}>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/topics" component={Topics} />
<Route path="/auth/login" component={Login} />
<Route path="/auth/register" component={Register} />
<Route component={NoMatch} />
</Switch>
</div>
<Footer />

View File

@@ -0,0 +1,16 @@
import React, { Component } from "react";
import { Container } from "semantic-ui-react";
class Login extends Component {
render() {
return <LoginView />;
}
}
const LoginView = () => (
<Container>
<p>Login</p>
</Container>
);
export default Login;

View File

@@ -0,0 +1,135 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import { Container, Form, Header, Message } from "semantic-ui-react";
import {
clearAuthRequestError,
setFormEmail,
setFormPassword,
setFormPasswordConfirmation
} from "../../actions/auth/reducer.actions";
import { sendRegisterRequest } from "../../actions/auth/saga.actions";
import Error from "../Shared/Error";
class Register extends Component {
constructor(props) {
super(props);
this.props.dispatch(clearAuthRequestError());
}
changeEmail = event => {
this.props.dispatch(setFormEmail(event.target.value));
};
changePassword = event => {
this.props.dispatch(setFormPassword(event.target.value));
};
changePasswordConfirmation = event => {
this.props.dispatch(setFormPasswordConfirmation(event.target.value));
};
onSubmitRegistration = event => {
event.preventDefault();
const { dispatch, email, password, passwordConfirmation } = this.props;
dispatch(
sendRegisterRequest({
email,
password1: password,
password2: passwordConfirmation
})
);
};
render() {
const {
isSendingAuthRequest,
authRequestError,
authRequestSuccess,
email,
password,
passwordConfirmation
} = this.props;
return (
<RegisterView
isSendingAuthRequest={isSendingAuthRequest}
authRequestError={authRequestError}
authRequestSuccess={authRequestSuccess}
email={email}
password={password}
passwordConfirmation={passwordConfirmation}
changeEmail={this.changeEmail}
changePassword={this.changePassword}
changePasswordConfirmation={this.changePasswordConfirmation}
onSubmitRegistration={this.onSubmitRegistration}
/>
);
}
}
function mapStateToProps(state) {
return { ...state.auth };
}
/**
* Functional view component for Register logic
*/
const RegisterView = ({
isSendingAuthRequest,
authRequestError,
authRequestSuccess,
email,
password,
passwordConfirmation,
changeEmail,
changePassword,
changePasswordConfirmation,
onSubmitRegistration
}) => (
<Container>
<Header>Register</Header>
<Form
loading={isSendingAuthRequest}
onSubmit={onSubmitRegistration}
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>
<Form.Field>
<label>Password Confirmation</label>
<input
placeholder="••••••••"
type="password"
value={passwordConfirmation}
onChange={changePasswordConfirmation}
/>
</Form.Field>
<Error header="Register failed!" error={authRequestError} />
<Message
success
header="Registration Sent"
content="A confirmation email has been sent to confirm your registration."
/>
<Form.Button>Submit</Form.Button>
</Form>
</Container>
);
export default connect(mapStateToProps)(Register);

View File

@@ -12,9 +12,14 @@ class Navbar extends Component {
<Menu.Item as={Link} to="/about">
About
</Menu.Item>
<Menu.Item as={Link} to="/topics">
Topics
</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>
);
}

View File

@@ -0,0 +1,41 @@
import PropTypes from "prop-types";
import React from "react";
import { Message } from "semantic-ui-react";
const propTypes = {
error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
header: PropTypes.string
};
const defaultProps = {
error: "",
header: ""
};
const Error = ({ error, header }) => {
if (typeof error === "string") {
const hasError = !!error;
if (hasError) {
return <Message error={hasError} header={header} content={error} />;
}
} else if (typeof error === "object" && Object.keys(error).length > 0) {
const hasError = !!Object.keys(error);
if (hasError) {
return (
<Message
error={hasError}
header={header}
list={Object.keys(error).map(p => (
<Message.Item key={p}> {p}: {error[p]}</Message.Item>
))}
/>
);
}
}
return null;
};
Error.propTypes = propTypes;
Error.defaultProps = defaultProps;
export default Error;

View File

@@ -0,0 +1,13 @@
import React from "react";
import { Link } from "react-router-dom";
import { Container } from "semantic-ui-react";
const NoMatch = ({ location }) => (
<Container>
<h3>Page not found!</h3>
<p>No match found for <code>{location.pathname}</code></p>
<p><Link to="/">Go to the home page </Link></p>
</Container>
);
export default NoMatch;

View File

@@ -1,41 +0,0 @@
import React from "react";
import { Route, Link } from "react-router-dom";
import { Container } from "semantic-ui-react";
const Topics = ({ match }) => (
<Container>
<h2>Topics</h2>
<ul>
<li>
<Link to={`${match.url}/rendering`}>
Rendering with React
</Link>
</li>
<li>
<Link to={`${match.url}/components`}>
Components
</Link>
</li>
<li>
<Link to={`${match.url}/props-v-state`}>
Props v. State
</Link>
</li>
</ul>
<Route path={`${match.url}/:topicId`} component={Topic} />
<Route
exact
path={match.url}
render={() => <h3>Please select a topic.</h3>}
/>
</Container>
);
const Topic = ({ match }) => (
<div>
<h3>{match.params.topicId}</h3>
</div>
);
export default Topics;

View File

@@ -0,0 +1,13 @@
// Reducer Auth Action Constants
export const IS_SENDING_AUTH_REQUEST = "IS_SENDING_AUTH_REQUEST";
export const SET_AUTH_REQUEST_ERROR = "SET_AUTH_REQUEST_ERROR";
export const CLEAR_AUTH_REQUEST_ERROR = "CLEAR_AUTH_REQUEST_ERROR";
export const SET_AUTH_REQUEST_SUCCESS = "SET_AUTH_REQUEST_SUCCESS";
export const CLEAR_AUTH_REQUEST_SUCCESS = "CLEAR_AUTH_REQUEST_SUCCESS";
export const SET_SELF_USER = "SET_SELF_USER";
export const SET_FORM_EMAIL = "SET_FORM_EMAIL";
export const SET_FORM_PASSWORD = "SET_FORM_PASSWORD";
export const SET_FORM_PASSWORD_CONFIRMATION = "SET_FORM_PASSWORD_CONFIRMATION";
// Saga Auth Action Constants
export const SEND_REGISTER_REQUEST = "SEND_REGISTER_REQUEST";

View File

@@ -1,26 +1,15 @@
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import { createStore, applyMiddleware } from "redux";
import createSagaMiddleware from "redux-saga";
import { Provider } from "react-redux";
import { createLogger } from "redux-logger";
import rootReducer from "./reducers";
import rootSaga from "./sagas";
import configureStore from "./store";
import App from "./components/App";
import { unregister } from "./registerServiceWorker";
const sagaMiddleware = createSagaMiddleware();
const debugLogger = createLogger();
const store = createStore(
rootReducer,
applyMiddleware(debugLogger, sagaMiddleware)
);
const store = configureStore();
const supportsHistory = "pushState" in window.history;
sagaMiddleware.run(rootSaga);
const rootElement = document.getElementById("root");
ReactDOM.render(

View File

@@ -1,7 +1,72 @@
const initialState = {};
import {
IS_SENDING_AUTH_REQUEST,
SET_AUTH_REQUEST_ERROR,
CLEAR_AUTH_REQUEST_ERROR,
SET_AUTH_REQUEST_SUCCESS,
CLEAR_AUTH_REQUEST_SUCCESS,
SET_SELF_USER,
SET_FORM_EMAIL,
SET_FORM_PASSWORD,
SET_FORM_PASSWORD_CONFIRMATION
} from "../constants/auth.constants";
const initialState = {
isSendingAuthRequest: false,
authRequestError: "",
authRequestSuccess: "",
currentUser: {},
email: "",
password: "",
passwordConfirmation: ""
};
function authReducer(state = initialState, action) {
switch (action.type) {
case IS_SENDING_AUTH_REQUEST:
return {
...state,
isSendingAuthRequest: action.data
};
case SET_AUTH_REQUEST_ERROR:
return {
...state,
authRequestError: action.data
};
case CLEAR_AUTH_REQUEST_ERROR:
return {
...state,
authRequestError: ""
};
case SET_AUTH_REQUEST_SUCCESS:
return {
...state,
authRequestSuccess: action.data
};
case CLEAR_AUTH_REQUEST_SUCCESS:
return {
...state,
authRequestSuccess: ""
};
case SET_SELF_USER:
return {
...state,
currentUser: action.data
};
case SET_FORM_EMAIL:
return {
...state,
email: action.data
};
case SET_FORM_PASSWORD:
return {
...state,
password: action.data
};
case SET_FORM_PASSWORD_CONFIRMATION:
return {
...state,
passwordConfirmation: action.data
};
default:
return state;
}

39
src/sagas/auth.sagas.js Normal file
View File

@@ -0,0 +1,39 @@
import { effects } from "redux-saga";
import {
isSendingAuthRequest,
setAuthRequestError,
setAuthRequestSuccess,
clearAuthRequestError,
setFormEmail,
setFormPassword,
setFormPasswordConfirmation
} from "../actions/auth/reducer.actions";
import { registerUser } from "../api/auth.api";
/**
* Saga for registering a new user.
* @param {*} postBody
*/
function* registerUserCall(postBody) {
yield effects.put(isSendingAuthRequest(true));
const { email, password1, password2 } = postBody;
try {
return yield effects.call(registerUser, email, password1, password2);
} catch (exception) {
yield effects.put(setAuthRequestError(exception));
return false;
} finally {
yield effects.put(isSendingAuthRequest(false));
}
}
export function* registerUserFlow(request) {
const wasSucessful = yield effects.call(registerUserCall, request.data);
if (wasSucessful) {
yield effects.put(setAuthRequestSuccess(wasSucessful));
yield effects.put(clearAuthRequestError());
yield effects.put(setFormEmail(""));
yield effects.put(setFormPassword(""));
yield effects.put(setFormPasswordConfirmation(""));
}
}

View File

@@ -1,4 +1,7 @@
import { takeLatest } from "redux-saga/effects";
import { SEND_REGISTER_REQUEST } from "../constants/auth.constants";
import { registerUserFlow } from "./auth.sagas";
export default function* rootSaga() {
yield takeLatest(SEND_REGISTER_REQUEST, registerUserFlow);
}

22
src/store/index.js Normal file
View File

@@ -0,0 +1,22 @@
import { createStore, applyMiddleware, compose } from "redux";
import createSagaMiddleware from "redux-saga";
import { createLogger } from "redux-logger";
import rootReducer from "../reducers";
import rootSaga from "../sagas";
export default function configureStore(initialState = {}) {
const sagaMiddleware = createSagaMiddleware();
const middlewares = [sagaMiddleware];
if (process.env.NODE_ENV === "development" && process.env.REACT_APP_REDUX_LOGGING) {
middlewares.push(createLogger());
}
const enhancers = [applyMiddleware(...middlewares)];
const store = createStore(rootReducer, initialState, compose(...enhancers));
// Extensions
store.asyncReducers = {}; // Async reducer registry
sagaMiddleware.run(rootSaga);
return store;
}