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() { |
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