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

Self PR #1

Open
wants to merge 37 commits into
base: self-pr
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
163014d
Starting redux
rodriguezmanu Aug 30, 2018
22be0c9
Starting Login and signup
rodriguezmanu Aug 30, 2018
efe8bab
Adding server api, user actions, user api, improvements validation an…
rodriguezmanu Aug 31, 2018
06f757d
Adding private routes, me, decode jwt and roles
rodriguezmanu Aug 31, 2018
b2fa1ec
Update README.md
rodriguezmanu Aug 31, 2018
65c4285
Adding changes server, signup and roles router hoc
rodriguezmanu Sep 2, 2018
8e338bd
Adding styling eslint and prettier
rodriguezmanu Sep 2, 2018
157a248
Adding api middleware
rodriguezmanu Sep 4, 2018
32bb3ba
Adding delete user and users redux
rodriguezmanu Sep 5, 2018
452ce05
Adding filter users
rodriguezmanu Sep 5, 2018
feff414
Adding improvements routers, edit user and auth
rodriguezmanu Sep 6, 2018
6fbdf24
Adding custom validation form
rodriguezmanu Sep 6, 2018
f9c46fd
Adding external validator
rodriguezmanu Sep 6, 2018
b81c23d
Adding validation on middleware
rodriguezmanu Sep 10, 2018
55bf82e
Adding edit user redux
rodriguezmanu Sep 10, 2018
dde1b34
Edition user feature
rodriguezmanu Sep 10, 2018
572b45b
Improvements role auth, some cleanups and eslint fix
rodriguezmanu Sep 11, 2018
af2aaef
General improvements
rodriguezmanu Sep 11, 2018
b55eae1
Adding pagination
rodriguezmanu Sep 11, 2018
87f1f3a
Adding scroll top and edit readme
rodriguezmanu Sep 11, 2018
28ebbf8
Update README.md
rodriguezmanu Sep 11, 2018
3d8aa6c
Adding script to deploy
rodriguezmanu Sep 11, 2018
68eec9c
Set new port nodeserver
rodriguezmanu Sep 11, 2018
752fc8d
Adding server to heroku
rodriguezmanu Sep 11, 2018
1eb3b76
Adding heroku file
rodriguezmanu Sep 11, 2018
4fdc277
Adding heroku post install script
rodriguezmanu Sep 11, 2018
7edc9f2
Revert github pages deploy
rodriguezmanu Sep 11, 2018
946aa09
Update yarn
rodriguezmanu Sep 11, 2018
ca95a9e
cleanups
rodriguezmanu Sep 11, 2018
8650936
Addign new script
rodriguezmanu Sep 11, 2018
36b69f1
Update README.md
rodriguezmanu Sep 11, 2018
5c276c6
Adding style paginate and improvements
rodriguezmanu Sep 12, 2018
1e4f587
Adding clean errors
rodriguezmanu Sep 12, 2018
1e01f94
Change name user redux
rodriguezmanu Sep 12, 2018
80e49de
Adding routes component
rodriguezmanu Sep 12, 2018
998af3f
Adding router redux
rodriguezmanu Sep 12, 2018
8d7887b
Updated Yarn lockfile
rodriguezmanu Sep 12, 2018
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
18 changes: 18 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# http://editorconfig.org
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding editorconfig with personal configuration in order to get better coding in editor.

root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
max_line_length = 120
trim_trailing_whitespace = true

[*.md]
max_line_length = 0
trim_trailing_whitespace = false

[COMMIT_EDITMSG]
max_line_length = 0
40 changes: 40 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some external extensions for eslint and also personal lint rules to improve the coding standards.

"extends": [
"airbnb",
"prettier",
"prettier/react",
"react-app"
],
"rules": {
"react/jsx-one-expression-per-line": [
0,
{
"allow": "none"
}
],
"react/jsx-filename-extension": [
1,
{
"extensions": [
".js",
".jsx"
]
}
],
"react/prefer-stateless-function": [0],
"jsx-a11y/label-has-associated-control": [0],
"jsx-a11y/label-has-for": [0],
"no-shadow": [0],
"prettier/prettier": [
"error",
{
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100
}
]
},
"plugins": [
"prettier"
]
}
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: node server-heroku.js
2,495 changes: 15 additions & 2,480 deletions README.md

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions db.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

database for json serve

"users": [{
"id": 1,
"name": "Manuel Admin",
"email": "[email protected]",
"role": "admin",
"password": "Qwerty1234@"
},
{
"id": 2,
"name": "Manuel User",
"email": "[email protected]",
"role": "user",
"password": "Qwerty1234@1"
}
]
}
24 changes: 23 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -4,10 +4,16 @@
"private": true,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tried to used less external dependencies as its posible

"dependencies": {
"bootstrap": "^4.1.3",
"connected-react-router": "^4.4.1",
"history": "^4.7.2",
"jwt-decode": "^2.2.0",
"prop-types": "^15.6.2",
"react": "^16.4.2",
"react-bootstrap": "^0.32.3",
"react-dom": "^16.4.2",
"react-paginate": "^5.2.4",
"react-redux": "^5.0.7",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1",
"react-scripts": "1.1.5",
"redux": "^4.0.0",
@@ -19,6 +25,22 @@
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
"eject": "react-scripts eject",
"start-api": "node server.js",
"postinstall": "react-scripts build"
},
"devDependencies": {
"eslint": "^4.19.1",
"eslint-config-airbnb": "^17.0.0",
"eslint-config-prettier": "^3.0.1",
"eslint-loader": "^2.1.0",
"eslint-plugin-import": "^2.13.0",
"eslint-plugin-jsx-a11y": "^6.1.1",
"eslint-plugin-prettier": "^2.6.2",
"eslint-plugin-react": "^7.10.0",
"express": "^4.16.3",
"json-server": "^0.14.0",
"jsonwebtoken": "^8.3.0",
"prettier": "^1.14.2"
}
}
34 changes: 34 additions & 0 deletions server-heroku.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const express = require('express');
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nodejs with express server to deploy in heroku

const path = require('path');
const http = require('http');
const bodyParser = require('body-parser');

const app = express();

// Parsers for POST data
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

// Point static path to build
app.use(express.static(path.join(__dirname, 'build')));

// Catch all other routes and return the index file
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'build', 'index.html'));
});

/**
* Get port from environment and store in Express.
*/
const port = process.env.PORT || '3000';
app.set('port', port);

/**
* Create HTTP server.
*/
const server = http.createServer(app);

/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port, () => console.log(`API running on localhost:${port}`));
91 changes: 91 additions & 0 deletions server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
const fs = require('fs');
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

server json with jwt auth

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const bodyParser = require('body-parser');
const jsonServer = require('json-server');
const jwt = require('jsonwebtoken');

const server = jsonServer.create();
const router = jsonServer.router('./db.json');
const port = process.env.PORT || 3004;

server.use(bodyParser.urlencoded({ extended: true }));
server.use(bodyParser.json());
server.use(jsonServer.defaults());

const SECRET_KEY = '123456789';
const expiresIn = '24h';

/**
* Create a token from a payload
* @param {Object} payload
*/
function createToken(payload) {
return jwt.sign(payload, SECRET_KEY, { expiresIn });
}

/**
* Verify the token
* @param {String} token
*/
function verifyToken(token) {
return jwt.verify(token, SECRET_KEY, (err, decode) => decode || err);
}

/**
* Check if the user exists in database
* @param {String} email
* @param {String} password
* @returns {Boolean}
*/
function isAuthenticated({ email, password }) {
const userdb = JSON.parse(fs.readFileSync('./db.json', 'UTF-8'));
return userdb.users.find(user => user.email === email && user.password === password);
}

server.post('/auth/login', (req, res) => {
const { email, password } = req.body;
const user = isAuthenticated({ email, password });

if (user) {
const userClone = Object.assign({}, user);
delete userClone.password;

const access_token = createToken(userClone);
res.status(200).json({ access_token, ...userClone });
return;
}
const status = 401;
const message = 'Incorrect email or password';

res.status(status).json({ status, message });
});

server.use(/^(?!\/auth).*$/, (req, res, next) => {
if (!req.headers.authorization || req.headers.authorization.split(' ')[0] !== 'Bearer') {
const status = 401;
const message = 'Error in authorization format';

res.status(status).json({ status, message });
return;
}
try {
verifyToken(req.headers.authorization.split(' ')[1]);
next();
} catch (err) {
const status = 401;
const message = 'Error access_token is revoked';

res.status(status).json({ status, message });
}
});

server.use(
jsonServer.rewriter({
'/auth/signup': '/users',
})
);

server.use(router);

server.listen(port, () => {
console.log('Run API Server');
});
28 changes: 0 additions & 28 deletions src/App.css

This file was deleted.

21 changes: 0 additions & 21 deletions src/App.js

This file was deleted.

9 changes: 0 additions & 9 deletions src/App.test.js

This file was deleted.

77 changes: 77 additions & 0 deletions src/actions/auth.actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import jwtDecode from 'jwt-decode';
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action only Auth user, create separate file in order to gain maintainability and readability

import {
LOGIN_SUCCESS,
LOGIN_REQUEST,
LOGIN_FAILURE,
SIGNUP_SUCCESS,
SIGNUP_REQUEST,
SIGNUP_FAILURE,
LOGOUT_SUCCESS,
ME_REQUEST,
ME_SUCCESS,
ME_FAILURE,
CLEAN_ERRORS,
} from '../constants/actionTypes';
import { API } from '../constants/endpoints';
import { CALL_API } from '../constants/variables';

/**
* Signup API handler
* @param {String} name
* @param {String} email
* @param {String} password
*/
export const signup = (name, email, password, role) => ({
[CALL_API]: {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to api middleware with object of params in order to create a common structure for it

payload: { name, email, password, role },
method: 'post',
types: [SIGNUP_REQUEST, SIGNUP_SUCCESS, SIGNUP_FAILURE, { type: 'login', action: login }],
endpoint: API.URL + API.USERS.AUTH.SIGNUP,
validate: true,
},
});

/**
* Login API handler
* @param {String} email
* @param {String} password
*/
export const login = (email, password) => ({
[CALL_API]: {
payload: { email, password },
method: 'post',
types: [LOGIN_REQUEST, LOGIN_SUCCESS, LOGIN_FAILURE],
endpoint: API.URL + API.USERS.AUTH.LOGIN,
validate: true,
},
});

/**
* Logout handler
*/
export const logout = () => dispatch => {
dispatch({ type: LOGOUT_SUCCESS });
};

/**
* Me handler
*/
export const me = () => dispatch => {
dispatch({ type: ME_REQUEST });

try {
const token = localStorage.getItem('token');
const user = jwtDecode(token);

dispatch({ type: ME_SUCCESS, user });
} catch (err) {
dispatch({ type: ME_FAILURE });
}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try catch in order to avoid errors

};

/**
* Clean Errors
*/
export const cleanErrors = () => dispatch => {
dispatch({ type: CLEAN_ERRORS });
};
114 changes: 114 additions & 0 deletions src/actions/users.actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { push } from 'connected-react-router';
import {
GET_USERS_REQUEST,
GET_USERS_SUCCESS,
GET_USERS_FAILURE,
DELETE_USER_REQUEST,
DELETE_USER_SUCCESS,
DELETE_USER_FAILURE,
FILTER_USER_SUCCESS,
GET_USER_REQUEST,
GET_USER_SUCCESS,
GET_USER_FAILURE,
UPDATE_USER_REQUEST,
UPDATE_USER_SUCCESS,
UPDATE_USER_FAILURE,
GET_USERS_COUNT_REQUEST,
GET_USERS_COUNT_SUCCESS,
GET_USERS_COUNT_FAILURE,
} from '../constants/actionTypes';
import { API } from '../constants/endpoints';
import { CALL_API } from '../constants/variables';
import { logout } from './auth.actions';

/**
* GetUsers API handler
*
* @param {Number} page
*/
export const getUsers = (page = 1) => ({
[CALL_API]: {
method: 'get',
types: [GET_USERS_REQUEST, GET_USERS_SUCCESS, GET_USERS_FAILURE],
endpoint: `${API.URL + API.USERS.GET}?_page=${page}`,
},
});

/**
* GetUsers count API handler for pagination
*
* @param {Number} page
*/
export const getUsersCount = () => ({
[CALL_API]: {
method: 'get',
types: [GET_USERS_COUNT_REQUEST, GET_USERS_COUNT_SUCCESS, GET_USERS_COUNT_FAILURE],
endpoint: API.URL + API.USERS.GET,
},
});

/**
* Get single API handler
* @param {Number} id
*/
export const getUser = id => ({
[CALL_API]: {
method: 'get',
types: [GET_USER_REQUEST, GET_USER_SUCCESS, GET_USER_FAILURE],
endpoint: API.URL + API.USERS.GET + id,
},
});

/**
* GetUsers API handler
* @params {Number} id
* @params {Boolean} isCurrent
*/
export const deleteUser = (id, isCurrent) => {
const action = isCurrent
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it the current user have to be logout action

? { type: 'logout', action: logout }
: { type: 'getUsers', action: getUsers };

return {
[CALL_API]: {
method: 'delete',
types: [DELETE_USER_REQUEST, DELETE_USER_SUCCESS, DELETE_USER_FAILURE, action],
endpoint: API.URL + API.USERS.DELETE + id,
},
};
};

/**
* Filter User handler handler
* @param {String} name
*/
export const filterUser = name => dispatch => {
dispatch({ type: FILTER_USER_SUCCESS, name });
};

/**
* Update User API handler
* @param {Number} id
* @param {String} name
* @param {String} email
* @param {String} password
* @param {Boolean} isCurrentUserRoleChange
*/
export const updateUser = (id, name, email, password, role, isCurrentUserRoleChange) => {
const actions = [UPDATE_USER_REQUEST, UPDATE_USER_SUCCESS, UPDATE_USER_FAILURE];
if (isCurrentUserRoleChange) {
actions.push({ type: 'logout', action: logout });
} else {
actions.push({ type: 'redirect', action: push('/users') });
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the current user change the role, have to logout , otherwise redirect to user listing page

}

return {
[CALL_API]: {
payload: { id, name, email, password, role },
method: 'put',
types: actions,
endpoint: API.URL + API.USERS.UPDATE + id,
validate: true,
},
};
};
13 changes: 13 additions & 0 deletions src/components/home/Home.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dummy home page to work with different roles


export class Home extends React.PureComponent {
render() {
return (
<div>
<h1>Home Role Users</h1>
</div>
);
}
}

export default Home;
54 changes: 54 additions & 0 deletions src/components/input/Input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from 'react';
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created composition model, In order to reuse code between components, better to maintain.

import PropTypes from 'prop-types';

class Input extends React.PureComponent {
static propTypes = {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

work with proptypes help to control props and create a stable and solid component.

label: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
placeholder: PropTypes.string.isRequired,
value: PropTypes.string,
};

static defaultProps = {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use default props helps also to create solid components

value: '',
};

componentWillMount() {
const { name, value } = this.props;

this.setState({
[name]: value,
});
}

/**
* Handler change form
*/
handleChange = event => {
const { name, value } = event.target;

this.setState({ [name]: value });
};

render() {
const { label, type, placeholder, name } = this.props;

return (
<React.Fragment>
<div className="form-group">
<label>{label}</label>
<input
className="form-control"
name={name}
type={type}
placeholder={placeholder}
value={this.state[name]}
onChange={this.handleChange}
/>
</div>
</React.Fragment>
);
}
}
export default Input;
53 changes: 53 additions & 0 deletions src/components/select/Select.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created composition model, In order to reuse code between components, better to maintain.

import PropTypes from 'prop-types';

class Select extends React.PureComponent {
static propTypes = {
label: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string,
};

static defaultProps = {
value: '',
};

componentWillMount() {
const { name, value } = this.props;

this.setState({
[name]: value,
});
}

/**
* Handler change form
*/
handleChange = event => {
const { name, value } = event.target;

this.setState({ [name]: value });
};

render() {
const { label, name } = this.props;

return (
<React.Fragment>
<div className="form-group">
<label>{label}</label>
<select
value={this.state[name]}
name="role"
className="form-control"
onChange={this.handleChange}
>
<option value="admin">Admin</option>
<option value="user">User</option>
</select>
</div>
</React.Fragment>
);
}
}
export default Select;
41 changes: 41 additions & 0 deletions src/constants/actionTypes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export const SIGNUP_REQUEST = 'SIGNUP_REQUEST';
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created a separated file with const actions, better to read, maintain.

export const SIGNUP_SUCCESS = 'SIGNUP_SUCCESS';
export const SIGNUP_FAILURE = 'SIGNUP_FAILURE';

export const LOGIN_REQUEST = 'LOGIN_REQUEST';
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
export const LOGIN_FAILURE = 'LOGIN_FAILURE';

export const LOGOUT_REQUEST = 'LOGOUT_REQUEST';
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
export const LOGOUT_FAILURE = 'LOGOUT_FAILURE';

export const ME_REQUEST = 'ME_REQUEST';
export const ME_SUCCESS = 'ME_SUCCESS';
export const ME_FAILURE = 'ME_FAILURE';

export const GET_USERS_REQUEST = 'GET_USERS_REQUEST';
export const GET_USERS_SUCCESS = 'GET_USERS_SUCCESS';
export const GET_USERS_FAILURE = 'GET_USERS_FAILURE';

export const GET_USERS_COUNT_REQUEST = 'GET_USERS_COUNT_REQUEST';
export const GET_USERS_COUNT_SUCCESS = 'GET_USERS_COUNT_SUCCESS';
export const GET_USERS_COUNT_FAILURE = 'GET_USERS_COUNT_FAILURE';

export const GET_USER_REQUEST = 'GET_USER_REQUEST';
export const GET_USER_SUCCESS = 'GET_USER_SUCCESS';
export const GET_USER_FAILURE = 'GET_USER_FAILURE';

export const UPDATE_USER_REQUEST = 'UPDATE_USER_REQUEST';
export const UPDATE_USER_SUCCESS = 'UPDATE_USER_SUCCESS';
export const UPDATE_USER_FAILURE = 'GET_USER_FAILURE';

export const DELETE_USER_REQUEST = 'DELETE_USER_REQUEST';
export const DELETE_USER_SUCCESS = 'DELETE_USER_SUCCESS';
export const DELETE_USER_FAILURE = 'DELETE_USERS_FAILURE';

export const FILTER_USER_REQUEST = 'FILTER_USER_REQUEST';
export const FILTER_USER_SUCCESS = 'FILTER_USER_SUCCESS';
export const FILTER_USER_FAILURE = 'FILTER_USER_FAILURE';

export const CLEAN_ERRORS = 'CLEAN_ERRORS';
15 changes: 15 additions & 0 deletions src/constants/endpoints.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const API = {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created a separated file with const with all relative to api, better to read, maintain.

URL: process.env.NODE_ENV === 'production' ? 'https://crud-users.herokuapp.com' : 'http://localhost:3004',
USERS: {
AUTH: {
LOGIN: '/auth/login',
SIGNUP: '/auth/signup',
},
CREATE: '/users',
DELETE: '/users/',
UPDATE: '/users/',
GET: '/users/',
},
};

export default API;
9 changes: 9 additions & 0 deletions src/constants/variables.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const emailPattern = /^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$/;
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created a separated file with common const, better to read, maintain.

export const passwordPattern = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{6,}$/;
export const namePattern = /^[a-zA-z ,.'-]+$/;
export const adminRole = 'admin';
export const userRole = 'user';
export const userRoles = ['user'];
export const adminRoles = ['admin'];
export const CALL_API = 'Call API';
export const limitUsers = 10;
51 changes: 51 additions & 0 deletions src/containers/errorFormMessage/ErrorFormMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created composition model, In order to reuse code between components, better to maintain.

import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { cleanErrors } from '../../actions/auth.actions';

class ErrorFormMessage extends React.PureComponent {
static propTypes = {
errors: PropTypes.arrayOf(PropTypes.string).isRequired,
cleanErrors: PropTypes.func.isRequired,
};

componentWillUnmount() {
const { cleanErrors } = this.props;

cleanErrors();
}

render() {
const { errors } = this.props;

return (
<div className="formErrors">
<div>
<ul>
{errors.map(item => (
<div key={item}>
<li>
<p>{item}</p>
</li>
</div>
))}
</ul>
<div className="alert alert-danger">Form invalid, please check again</div>
</div>
</div>
);
}
}

const mapStateToProps = state => ({
user: state.auth,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not used anymore, should be deleted

});

const mapDispatchToProps = {
cleanErrors,
};

export default connect(
mapStateToProps,
mapDispatchToProps
)(ErrorFormMessage);
61 changes: 61 additions & 0 deletions src/containers/login/Login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from 'react';
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Login component using other small component and redux to validate form and login action

import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { login } from '../../actions/auth.actions';
import Input from '../../components/input/Input';
import ErrorFormMessage from '../errorFormMessage/ErrorFormMessage';

export class Login extends React.PureComponent {
static propTypes = {
user: PropTypes.shape({}).isRequired,
login: PropTypes.func.isRequired,
};

/**
* Submit handler
*/
handleSubmit = e => {
e.preventDefault();

const {
email: { value: email },
password: { value: password },
} = e.target;
const { login } = this.props;

login(email, password);
};

render() {
const { user } = this.props;

return (
<div className="container">
<h1 className="text-center m-2">Login</h1>
<form name="form" onSubmit={this.handleSubmit}>
<Input label="Email" type="email" placeholder="Email" name="email" />
<Input label="Password" type="password" placeholder="Password" name="password" />
<div className="form-group text-center">
<button type="submit" className="btn btn-primary">
Submit
</button>
</div>
{user.errors && <ErrorFormMessage errors={user.errors} />}
</form>
</div>
);
}
}

const mapStateToProps = state => ({
user: state.auth,
});

const mapDispatchToProps = {
login,
};

export default connect(
mapStateToProps,
mapDispatchToProps
)(Login);
113 changes: 113 additions & 0 deletions src/containers/main/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { ConnectedRouter } from 'connected-react-router';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { logout, me } from '../../actions/auth.actions';
import routes from '../../routes';
import history from '../../history';

class App extends React.PureComponent {
static propTypes = {
user: PropTypes.shape({}).isRequired,
logout: PropTypes.func.isRequired,
me: PropTypes.func.isRequired,
};

componentWillMount() {
this.handlerMe();
}

/**
* Handler user data from token saved on localStorage
*/
handlerMe = () => {
const { me } = this.props;
me();
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getting form api the user data from jwt token

};

/**
* Logout Handler
*/
logoutHandler = () => {
const { logout } = this.props;
logout();
};

render() {
const { user } = this.props;

return (
<React.Fragment>
{!user.isFetching && (
<ConnectedRouter history={history}>
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

used connected router for redux

<div>
<nav className="navbar navbar-expand-lg navbar-light bg-light">
<div className="navbar-brand">APP</div>
<button
className="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span className="navbar-toggler-icon" />
</button>
<div className="collapse navbar-collapse" id="navbarNav">
{user.isAuth ? (
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

menu options for auth user and no auth user

<ul className="navbar-nav">
<li>
<div className="nav-item nav-link">{user.data.name}</div>
</li>
<li className="nav-item">
<div className="nav-item">
<button
className="btn btn-primary"
type="button"
onClick={this.logoutHandler}
>
Logout
</button>
</div>
</li>
</ul>
) : (
<ul className="navbar-nav">
<li className="nav-item">
<div className="nav-item nav-link">
<Link to="/login">Login</Link>
</div>
</li>
<li className="nav-item">
<div className="nav-item nav-link">
<Link to="/signup">Signup</Link>
</div>
</li>
</ul>
)}
</div>
</nav>
{routes}
</div>
</ConnectedRouter>
)}
</React.Fragment>
);
}
}

const mapStateToProps = state => ({
user: state.auth,
});

const mapDispatchToProps = {
logout,
me,
};

export default connect(
mapStateToProps,
mapDispatchToProps
)(App);
39 changes: 39 additions & 0 deletions src/containers/privateRoute/PrivateRoute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

worked with private routes for auth user and specific role in order to prevent not allowed redirection

import { Route, Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';

export class PrivateRoute extends React.PureComponent {
static propTypes = {
user: PropTypes.shape({}).isRequired,
component: PropTypes.func.isRequired,
allowedRoles: PropTypes.arrayOf(PropTypes.string).isRequired,
};

render() {
const { user, component: Component, allowedRoles, ...rest } = this.props;
return (
<Route
{...rest}
render={props =>
user.isAuth && allowedRoles.includes(user.data.role) ? (
<Component {...props} />
) : (
<Redirect to={{ pathname: '/login', state: { from: props.location } }} />
)
}
/>
);
}
}

const mapStateToProps = state => ({
user: state.auth,
});

const mapDispatchToProps = {};

export default connect(
mapStateToProps,
mapDispatchToProps
)(PrivateRoute);
51 changes: 51 additions & 0 deletions src/containers/publicRoute/PublicRoute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public routes in order to prevent auth user redirect to login and signup page

import { Route, Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { userRole, adminRole } from '../../constants/variables';

export class PublicRoute extends React.PureComponent {
static propTypes = {
user: PropTypes.shape({}).isRequired,
component: PropTypes.func.isRequired,
};

render() {
const { user, component: Component } = this.props;

return (
<Route
render={props => {
let toPath;

if (user.isAuth && user.data.role === adminRole) {
toPath = 'users';
}
if (user.isAuth && user.data.role === userRole) {
toPath = 'home';
}

if (toPath) {
return (
<Route>
<Redirect to={toPath} />
</Route>
);
}
return <Component {...props} />;
}}
/>
);
}
}

const mapStateToProps = state => ({
user: state.auth,
});

const mapDispatchToProps = {};

export default connect(
mapStateToProps,
mapDispatchToProps
)(PublicRoute);
84 changes: 84 additions & 0 deletions src/containers/signup/Signup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { signup } from '../../actions/auth.actions';
import Input from '../../components/input/Input';
import ErrorFormMessage from '../errorFormMessage/ErrorFormMessage';

export class Signup extends React.PureComponent {
state = {
role: 'admin',
};

static propTypes = {
user: PropTypes.shape({}).isRequired,
signup: PropTypes.func.isRequired,
};

/**
* Handler change form
*/
handleChange = event => {
const { name, value } = event.target;

this.setState({ [name]: value });
};

/**
* Submit handler
*/
handleSubmit = e => {
e.preventDefault();

const {
name: { value: name },
email: { value: email },
password: { value: password },
role: { value: role },
} = e.target;
const { signup } = this.props;

signup(name, email, password, role);
};

render() {
const { role } = this.state;
const { user } = this.props;

return (
<div className="container">
<h1 className="text-center m-2">SignUp</h1>
<form name="form" onSubmit={this.handleSubmit}>
<div className="form-group">
<select value={role} name="role" className="form-control" onChange={this.handleChange}>
<option value="admin">Admin</option>
<option value="user">User</option>
</select>
</div>
<Input label="Name" type="text" placeholder="Name" name="name" />
<Input label="Email" type="email" placeholder="Email" name="email" />
<Input label="Password" type="password" placeholder="Password" name="password" />
<div className="form-group text-center">
<button type="submit" className="btn btn-primary">
Submit
</button>
</div>
{user.errors && <ErrorFormMessage errors={user.errors} />}
</form>
</div>
);
}
}

const mapStateToProps = state => ({
user: state.auth,
});

const mapDispatchToProps = {
signup,
};

export default connect(
mapStateToProps,
mapDispatchToProps
)(Signup);
139 changes: 139 additions & 0 deletions src/containers/users/EditUser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { NavLink } from 'react-router-dom';
import { matchPath } from 'react-router';
import Input from '../../components/input/Input';
import Select from '../../components/select/Select';
import ErrorFormMessage from '../errorFormMessage/ErrorFormMessage';
import { getUser, updateUser } from '../../actions/users.actions';

export class EditUser extends React.PureComponent {
static propTypes = {
user: PropTypes.shape({}).isRequired,
getUser: PropTypes.func.isRequired,
updateUser: PropTypes.func.isRequired,
history: PropTypes.shape({}).isRequired,
};

componentDidMount() {
this.getUserHandler();
}

/**
* Submit handler
*/
handleSubmit = e => {
e.preventDefault();

const { updateUser, user } = this.props;
const {
name: { value: name },
email: { value: email },
password: { value: password },
role: { value: role },
} = e.target;
const match = this.getUrlParams();
const checkRoleChange = user.data.id === Number(match.params.id) && user.data.role !== role;
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check if the current user change it role, in order to redirect to proper action


updateUser(match.params.id, name, email, password, role, checkRoleChange);
};

/**
* Get Url Params
*
* @return {Object}
*/
getUrlParams = () => {
const { history } = this.props;
const match = matchPath(history.location.pathname, {
path: '/users/:id',
});

return match;
};

/**
* Handler change form
*/
handleChange = event => {
const { name, value } = event.target;

this.setState({ [name]: value });
};

/**
* Get User Handler
*/
getUserHandler = () => {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get user information to edit from url query params

const { getUser } = this.props;
const match = this.getUrlParams();

getUser(match.params.id);
};

/**
* Handler change form
*/
handleChange = event => {
const { name, value } = event.target;

this.setState({ [name]: value });
};

render() {
const { user } = this.props;

return (
<div className="container">
<div>
<h1 className="text-center m-2">EditUser</h1>
<NavLink className="btn btn-primary text-center m-2" to="/users/">
Back to Users
</NavLink>
</div>
{user.data &&
!user.isFetching && (
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

waiting for redux and api return data

<form name="form" onSubmit={this.handleSubmit}>
<Select label="Role" name="role" value={user.data.role} />
<Input
label="Name"
type="text"
placeholder="Name"
name="name"
value={user.data.name}
/>
<Input
label="Email"
type="email"
placeholder="Email"
name="email"
value={user.data.email}
/>
<Input label="Password" type="password" placeholder="Password" name="password" />
<div className="form-group text-center">
<button type="submit" className="btn btn-primary">
Submit
</button>
</div>
</form>
)}
{user.errors && <ErrorFormMessage errors={user.errors} />}
</div>
);
}
}

const mapStateToProps = state => ({
user: state.users,
});

const mapDispatchToProps = {
getUser,
updateUser,
};

export default connect(
mapStateToProps,
mapDispatchToProps
)(EditUser);
109 changes: 109 additions & 0 deletions src/containers/users/Users.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
.pagination {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some styling for pagination

display: inline-block;
padding-left: 0;
margin: 22px 0;
border-radius: 4px;
}

.pagination>li {
display: inline;
}

.pagination>li>a,
.pagination>li>span {
position: relative;
float: left;
padding: 6px 12px;
line-height: 1.42857143;
text-decoration: none;
color: #2c689c;
background-color: #fff;
border: 1px solid #ddd;
margin-left: -1px;
}

.pagination>li:first-child>a,
.pagination>li:first-child>span {
margin-left: 0;
border-bottom-left-radius: 4px;
border-top-left-radius: 4px;
}

.pagination>li:last-child>a,
.pagination>li:last-child>span {
border-bottom-right-radius: 4px;
border-top-right-radius: 4px;
}

.pagination>li>a:hover,
.pagination>li>span:hover,
.pagination>li>a:focus,
.pagination>li>span:focus {
z-index: 2;
color: #1b4060;
background-color: #eeeeee;
border-color: #ddd;
}

.pagination>.active>a,
.pagination>.active>span,
.pagination>.active>a:hover,
.pagination>.active>span:hover,
.pagination>.active>a:focus,
.pagination>.active>span:focus {
z-index: 3;
color: #fff;
background-color: #2c689c;
border-color: #2c689c;
cursor: default;
}

.pagination>.disabled>span,
.pagination>.disabled>span:hover,
.pagination>.disabled>span:focus,
.pagination>.disabled>a,
.pagination>.disabled>a:hover,
.pagination>.disabled>a:focus {
color: #777777;
background-color: #fff;
border-color: #ddd;
cursor: not-allowed;
}

.pagination-lg>li>a,
.pagination-lg>li>span {
padding: 10px 16px;
font-size: 20px;
line-height: 1.3333333;
}

.pagination-lg>li:first-child>a,
.pagination-lg>li:first-child>span {
border-bottom-left-radius: 6px;
border-top-left-radius: 6px;
}

.pagination-lg>li:last-child>a,
.pagination-lg>li:last-child>span {
border-bottom-right-radius: 6px;
border-top-right-radius: 6px;
}

.pagination-sm>li>a,
.pagination-sm>li>span {
padding: 5px 10px;
font-size: 14px;
line-height: 1.5;
}

.pagination-sm>li:first-child>a,
.pagination-sm>li:first-child>span {
border-bottom-left-radius: 3px;
border-top-left-radius: 3px;
}

.pagination-sm>li:last-child>a,
.pagination-sm>li:last-child>span {
border-bottom-right-radius: 3px;
border-top-right-radius: 3px;
}
168 changes: 168 additions & 0 deletions src/containers/users/Users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { NavLink } from 'react-router-dom';
import ReactPaginate from 'react-paginate';
import { getUsers, deleteUser, filterUser, getUsersCount } from '../../actions/users.actions';
import './Users.css';
import { limitUsers } from '../../constants/variables';

export class Users extends React.PureComponent {
static propTypes = {
users: PropTypes.shape([]).isRequired,
user: PropTypes.shape({}).isRequired,
getUsers: PropTypes.func.isRequired,
deleteUser: PropTypes.func.isRequired,
filterUser: PropTypes.func.isRequired,
getUsersCount: PropTypes.func.isRequired,
};

state = {
filtered: false,
};

componentWillMount() {
const { getUsers, getUsersCount } = this.props;

getUsers();
getUsersCount();
}

/**
* Delete user
* @params {Number} id
*/
deleteUserHandler = id => {
const { user, deleteUser } = this.props;

deleteUser(id, user.data.id === id);
};

/**
* Filter user list action
*/
filterUserList = event => {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filter user and handle with pagination

const { filterUser } = this.props;

if (event.target.value === '') {
this.setState({
filtered: false,
});
} else {
filterUser(event.target.value);
this.setState({
filtered: true,
});
}
};

/**
* Handler click pagination
*/
handlePageClick = data => {
const { getUsers } = this.props;

getUsers(data.selected + 1);
window.scrollTo(0, 0);
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

every time go to next page in pagination scroll to up

};

/**
* Render paginate component
*/
renderPaginate = () => {
const { users } = this.props;
const { filtered } = this.state;
const pageCount = filtered ? users.countFiltered : users.count;

if (users.count <= limitUsers) {
return (
<div className="m-2">
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pagination external component

<ReactPaginate
previousLabel="prev"
nextLabel="next"
pageCount={pageCount}
marginPagesDisplayed={2}
pageRangeDisplayed={5}
onPageChange={this.handlePageClick}
containerClassName="pagination"
subContainerClassName="pages pagination"
activeClassName="active"
/>
</div>
);
}
return null;
};

render() {
const { users } = this.props;

return (
<div className="m-2 text-center">
<div className="m-2 text-center">
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

user filter

<input type="text" placeholder="Search" onChange={this.filterUserList} />
<hr />
</div>
<div>
{users.filtered && (
<div>
<ul className="list-unstyled">
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

user list

{users.filtered.map(item => (
<div key={item.id}>
<li>
<p>
<b>Name: </b>
{item.name}
</p>
<p>
<b>Email: </b>
{item.email}
</p>
<p>
<b>Role: </b>
{item.role.toUpperCase()}
</p>
</li>
<div>
<button
className="btn btn-primary m-2"
type="button"
onClick={e => {
this.deleteUserHandler(item.id);
}}
>
Delete User
</button>
<NavLink className="btn btn-primary" to={`/users/${item.id}`}>
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redirect to edit user with query param id

Edit
</NavLink>
</div>
<hr />
</div>
))}
</ul>
</div>
)}
{this.renderPaginate()}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

separate render pagination for better understanding

</div>
</div>
);
}
}

const mapStateToProps = state => ({
users: state.users,
user: state.auth,
});

const mapDispatchToProps = {
getUsers,
deleteUser,
filterUser,
getUsersCount,
};

export default connect(
mapStateToProps,
mapDispatchToProps
)(Users);
3 changes: 3 additions & 0 deletions src/history.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createBrowserHistory } from 'history';
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

history in order to work with connect router redux redirect methods


export default createBrowserHistory();
5 changes: 0 additions & 5 deletions src/index.css

This file was deleted.

17 changes: 13 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import 'bootstrap/dist/css/bootstrap.min.css';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import registerServiceWorker from './registerServiceWorker';
import App from './containers/main/App';
import configureStore from './store/configureStore';

ReactDOM.render(<App />, document.getElementById('root'));
const store = configureStore();

render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
registerServiceWorker();
7 changes: 0 additions & 7 deletions src/logo.svg

This file was deleted.

148 changes: 148 additions & 0 deletions src/middleware/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { emailPattern, passwordPattern, namePattern, CALL_API } from '../constants/variables';

/**
* Handle communication with web service
*
* @param {Object} callAPI
* @param {Object} next
* @param {Object} actionWith
* @param {Object} store
*/
export const api = async (callAPI, next, actionWith, store) => {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

used async - await feature, more readably

const [requestType, successType, failureType, nextType] = callAPI.types;

try {
const res = await fetch(callAPI.endpoint, {
method: callAPI.method,
headers: getHeaders(),
body: JSON.stringify(callAPI.payload),
});
const data = await res.json();
if (res.ok) {
next(
actionWith({
data,
type: successType,
})
);
if (nextType) {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in order to dispatch next action defined in action file

if (nextType.type === 'login') {
store.dispatch(nextType.action(data.email, data.password));
} else if (nextType.type === 'redirect') {
store.dispatch(nextType.action);
} else {
store.dispatch(nextType.action());
}
}
} else {
next(
actionWith({
type: failureType,
errors: [data.message || 'Error with API, please try again'],
})
);
}
} catch (e) {
next(
actionWith({
type: failureType,
errors: [e.message || 'Error with API, please try again'],
})
);
}
};
/**
* Get headers
* @return {Object} headers
*/
const getHeaders = () => {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get headers method in order to apply auth jwt token for auth routes

const token = localStorage.getItem('token');
const headers = {
'Content-Type': 'application/json',
};

if (token) {
const tokenParsed = JSON.parse(token);
headers.Authorization = `Bearer ${tokenParsed}`;
}

return headers;
};

// Default middleware
export default store => next => action => {
const callAPI = action[CALL_API];

if (callAPI) {
const { types } = callAPI;
const [requestType, successType, failureType, nextType] = types;
const actionWith = data => {
const finalAction = Object.assign({}, action, data);
delete finalAction[CALL_API];
return finalAction;
};

next(actionWith({ type: requestType }));

if (callAPI.validate) {
if (!validate(callAPI.payload)) {
return next(actionWith({ type: failureType, errors: callAPI.payload.errors }));
}
}

if (callAPI.payload && callAPI.payload.errors) {
delete callAPI.payload.errors;
}

api(callAPI, next, actionWith, store);
} else {
return next(action);
}
};

/**
* Validate form data
*
* @param {Object} payload
* @return {Boolean}
*/
const validate = payload => {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validation form by field, using regex and adding a complete array of error to work with errormessage component

let res;
let regex;
payload.errors = [];

for (const key in payload) {
if (Object.prototype.hasOwnProperty.call(payload, key)) {
const element = payload[key];
switch (key) {
case 'password':
regex = RegExp(passwordPattern);
res = regex.test(element);
if (!res) {
payload.errors.push(
'Minimum eight characters, at least one letter, one number and one special'
);
}
break;
case 'email':
regex = RegExp(emailPattern);
res = regex.test(element);
if (!res) {
payload.errors.push('Must be a valid Email');
}
break;
case 'name':
regex = RegExp(namePattern);
res = regex.test(element);
if (!res) {
payload.errors.push('Must be a valid Name');
}
break;

default:
break;
}
}
}
return payload.errors.length === 0;
};
89 changes: 89 additions & 0 deletions src/reducers/auth.reducer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {
SIGNUP_REQUEST,
SIGNUP_SUCCESS,
SIGNUP_FAILURE,
LOGIN_REQUEST,
LOGIN_SUCCESS,
LOGIN_FAILURE,
LOGOUT_SUCCESS,
ME_REQUEST,
ME_SUCCESS,
ME_FAILURE,
CLEAN_ERRORS,
} from '../constants/actionTypes';

const initialState = {
isFetching: false,
isAuth: false,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by default the user is not auth

};

const auth = (state = initialState, action) => {
switch (action.type) {
case SIGNUP_REQUEST:
return {
...state,
isFetching: true,
};
case SIGNUP_SUCCESS:
return {
...state,
isFetching: true,
};
case SIGNUP_FAILURE:
return {
...state,
errors: action.errors,
isFetching: false,
};
case LOGIN_REQUEST:
return {
...state,
isFetching: true,
};
case LOGIN_SUCCESS:
localStorage.setItem('token', JSON.stringify(action.data.access_token));
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

login set a new jwt token

return {
...state,
data: action.data,
isAuth: true,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isauth works to control private routes and menus

isFetching: false,
};
case LOGIN_FAILURE:
return {
...state,
isAuth: false,
errors: action.errors,
isFetching: false,
};
case LOGOUT_SUCCESS:
localStorage.removeItem('token');
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logout remove the token and return an empty user

return {};
case ME_REQUEST:
return {
...state,
isFetching: true,
};
case ME_SUCCESS:
return {
...state,
data: action.user,
isAuth: true,
isFetching: false,
};
case ME_FAILURE:
return {
...state,
isAuth: false,
isFetching: false,
};
case CLEAN_ERRORS:
return {
...state,
errors: null,
};
default:
return state;
}
};

export default auth;
11 changes: 11 additions & 0 deletions src/reducers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { combineReducers } from 'redux';

import auth from './auth.reducer';
import users from './users.reducer';

const rootReducer = combineReducers({
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used separated reducer and mix them

auth,
users,
});

export default rootReducer;
110 changes: 110 additions & 0 deletions src/reducers/users.reducer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {
GET_USERS_REQUEST,
GET_USERS_SUCCESS,
GET_USERS_FAILURE,
GET_USER_REQUEST,
GET_USER_SUCCESS,
GET_USER_FAILURE,
UPDATE_USER_REQUEST,
UPDATE_USER_SUCCESS,
UPDATE_USER_FAILURE,
FILTER_USER_SUCCESS,
GET_USERS_COUNT_REQUEST,
GET_USERS_COUNT_SUCCESS,
GET_USERS_COUNT_FAILURE,
} from '../constants/actionTypes';
import { limitUsers } from '../constants/variables';

const initialState = {
isFetching: false,
};
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

created initial state


const users = (state = initialState, action) => {
let filtered;
switch (action.type) {
case GET_USERS_REQUEST:
return {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

every action return the state, action and isfetching in order to handle render method data

...state,
isFetching: true,
};
case GET_USERS_SUCCESS:
return {
...state,
data: action.data,
filtered: action.data,
isFetching: false,
};
case GET_USERS_FAILURE:
return {
...state,
errors: action.errors,
isFetching: false,
};
case GET_USER_REQUEST:
return {
...state,
isFetching: true,
};
case GET_USER_SUCCESS:
return {
...state,
data: action.data,
isFetching: false,
};
case GET_USER_FAILURE:
return {
...state,
errors: action.errors,
isFetching: false,
};
case UPDATE_USER_REQUEST:
return {
...state,
isFetching: true,
};
case UPDATE_USER_SUCCESS:
return {
...state,
data: action.data,
isFetching: false,
errors: null,
};
case UPDATE_USER_FAILURE:
return {
...state,
errors: action.errors,
isFetching: false,
};
case FILTER_USER_SUCCESS:
filtered = state.data.filter(val =>
val.name.toLowerCase().includes(action.name.toLowerCase())
);

return {
...state,
filtered,
countFiltered: Math.ceil(filtered.length / limitUsers),
isFetching: false,
};
case GET_USERS_COUNT_REQUEST:
return {
...state,
isFetching: true,
};
case GET_USERS_COUNT_SUCCESS:
return {
...state,
count: Math.ceil(action.data.length / limitUsers),
isFetching: false,
};
case GET_USERS_COUNT_FAILURE:
return {
...state,
isFetching: false,
};
default:
return state;
}
};

export default users;
25 changes: 25 additions & 0 deletions src/routes/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separated routes file

import { Switch, Redirect } from 'react-router';
import Login from '../containers/login/Login';
import Signup from '../containers/signup/Signup';
import Users from '../containers/users/Users';
import EditUser from '../containers/users/EditUser';
import Home from '../components/home/Home';
import PrivateRoute from '../containers/privateRoute/PrivateRoute';
import PublicRoute from '../containers/publicRoute/PublicRoute';
import { adminRoles, userRoles } from '../constants/variables';

const routes = (
<div className="page-container">
<Switch>
<PublicRoute exact path="/login" component={Login} />
<PublicRoute path="/signup" component={Signup} />
<PrivateRoute path="/users/:id" component={EditUser} allowedRoles={adminRoles} />
<PrivateRoute path="/users" component={Users} allowedRoles={adminRoles} />
<PrivateRoute path="/home" component={Home} allowedRoles={userRoles} />
<Redirect to="/login" />
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any other path goes to login page

</Switch>
</div>
);

export default routes;
28 changes: 28 additions & 0 deletions src/store/configureStore.dev.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';
import logger from 'redux-logger';
import { createBrowserHistory } from 'history';
import { connectRouter, routerMiddleware } from 'connected-react-router';
import rootReducer from '../reducers';
import api from '../middleware/api';
import history from '../history';

const configureStore = preloadedState => {
const store = createStore(
connectRouter(history)(rootReducer),
preloadedState,
composeWithDevTools(applyMiddleware(routerMiddleware(history), thunk, api, logger))
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for dev env, used logger

);

if (module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('../reducers', () => {
store.replaceReducer(rootReducer);
});
}

return store;
};

export default configureStore;
5 changes: 5 additions & 0 deletions src/store/configureStore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
if (process.env.NODE_ENV === 'production') {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

used 2 env in order to change different options between them

module.exports = require('./configureStore.prod');
} else {
module.exports = require('./configureStore.dev');
}
18 changes: 18 additions & 0 deletions src/store/configureStore.prod.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import { connectRouter, routerMiddleware } from 'connected-react-router';
import history from '../history';
import rootReducer from '../reducers';
import api from '../middleware/api';

const configureStore = preloadedState => {
const store = createStore(
connectRouter(history)(rootReducer),
preloadedState,
compose(applyMiddleware(routerMiddleware(history), thunk, api))
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logger is not used to in prod env

);

return store;
};

export default configureStore;
807 changes: 715 additions & 92 deletions yarn.lock

Large diffs are not rendered by default.