parent
aaad6ea3eb
commit
415aea74a3
25 changed files with 587 additions and 66 deletions
@ -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 |
||||
}; |
||||
} |
@ -0,0 +1,10 @@ |
||||
import { |
||||
SEND_REGISTER_REQUEST |
||||
} from "../../constants/auth.constants"; |
||||
|
||||
export function sendRegisterRequest(postbody) { |
||||
return { |
||||
type: SEND_REGISTER_REQUEST, |
||||
data: postbody |
||||
} |
||||
} |
@ -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 |
||||
} |
@ -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); |
||||
}); |
||||
} |
@ -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)); |
||||
} |
@ -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; |
@ -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); |
@ -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; |
@ -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; |
@ -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; |
@ -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"; |
@ -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("")); |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
|
@ -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; |
||||
} |
Loading…
Reference in new issue