├── .env ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── react-redux-refresh-token-axios-jwt-flow.png ├── src ├── App.css ├── App.js ├── actions │ ├── auth.js │ ├── message.js │ └── types.js ├── common │ └── EventBus.js ├── components │ ├── board-admin.component.js │ ├── board-moderator.component.js │ ├── board-user.component.js │ ├── home.component.js │ ├── login.component.js │ ├── profile.component.js │ └── register.component.js ├── helpers │ └── history.js ├── index.css ├── index.js ├── reducers │ ├── auth.js │ ├── index.js │ └── message.js ├── reportWebVitals.js ├── services │ ├── api.js │ ├── auth.service.js │ ├── setupInterceptors.js │ ├── token.service.js │ └── user.service.js ├── setupTests.js └── store.js └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | PORT=8081 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## React Redux Refresh Token with Axios and JWT example 2 | 3 | ![react-redux-refresh-token-axios-jwt-flow](react-redux-refresh-token-axios-jwt-flow.png) 4 | 5 | For more detail, please visit: 6 | > [React Redux Refresh Token with Axios and JWT](https://www.bezkoder.com/redux-refresh-token-axios/) 7 | 8 | > [React Redux JWT Authentication & Authorization example](https://bezkoder.com/react-redux-jwt-auth/) 9 | 10 | > [React Hooks + Redux: JWT Authentication & Authorization example](https://bezkoder.com/react-hooks-redux-login-registration-example/) 11 | 12 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 13 | 14 | ### Set port 15 | .env 16 | ``` 17 | PORT=8081 18 | ``` 19 | 20 | ## Note: 21 | Open `src/services/setupInterceptors.js` and modify `config.headers` for appropriate back-end (found in the tutorial). 22 | 23 | ```js 24 | instance.interceptors.request.use( 25 | (config) => { 26 | const token = TokenService.getLocalAccessToken(); 27 | if (token) { 28 | // config.headers["Authorization"] = 'Bearer ' + token; // for Spring Boot back-end 29 | config.headers["x-access-token"] = token; // for Node.js Express back-end 30 | } 31 | return config; 32 | }, 33 | (error) => { 34 | return Promise.reject(error); 35 | } 36 | ); 37 | ``` 38 | 39 | ## Project setup 40 | 41 | In the project directory, you can run: 42 | 43 | ``` 44 | npm install 45 | # or 46 | yarn install 47 | ``` 48 | 49 | or 50 | 51 | ### Compiles and hot-reloads for development 52 | 53 | ``` 54 | npm start 55 | # or 56 | yarn start 57 | ``` 58 | 59 | Open [http://localhost:8081](http://localhost:8081) to view it in the browser. 60 | 61 | The page will reload if you make edits. 62 | 63 | ## Related Posts 64 | > [In-depth Introduction to JWT-JSON Web Token](https://bezkoder.com/jwt-json-web-token/) 65 | 66 | > [React.js CRUD example to consume Web API](https://bezkoder.com/react-crud-web-api/) 67 | 68 | > [React Redux CRUD App example with Rest API](https://bezkoder.com/react-redux-crud-example/) 69 | 70 | > [React Pagination example](https://bezkoder.com/react-pagination-material-ui/) 71 | 72 | > [React File Upload with Axios and Progress Bar to Rest API](https://bezkoder.com/react-file-upload-axios/) 73 | 74 | Fullstack (JWT Authentication & Authorization example): 75 | > [React + Spring Boot](https://bezkoder.com/spring-boot-react-jwt-auth/) 76 | 77 | > [React + Node.js Express](https://bezkoder.com/react-express-authentication-jwt/) 78 | 79 | Fullstack CRUD with Node.js Express: 80 | > [React.js + Node.js Express + MySQL](https://bezkoder.com/react-node-express-mysql/) 81 | 82 | > [React.js + Node.js Express + PostgreSQL](https://bezkoder.com/react-node-express-postgresql/) 83 | 84 | > [React.js + Node.js Express + MongoDB](https://bezkoder.com/react-node-express-mongodb-mern-stack/) 85 | 86 | Fullstack CRUD with Spring Boot: 87 | > [React.js + Spring Boot + MySQL](https://bezkoder.com/react-spring-boot-crud/) 88 | 89 | > [React.js + Spring Boot + PostgreSQL](https://bezkoder.com/spring-boot-react-postgresql/) 90 | 91 | > [React.js + Spring Boot + MongoDB](https://bezkoder.com/react-spring-boot-mongodb/) 92 | 93 | Fullstack CRUD with Django: 94 | > [React.js + Django Rest Framework](https://bezkoder.com/django-react-axios-rest-framework/) 95 | 96 | Integration (run back-end & front-end on same server/port) 97 | > [How to integrate React.js with Spring Boot](https://bezkoder.com/integrate-reactjs-spring-boot/) 98 | 99 | > [Integrate React with Node.js Express on same Server/Port](https://bezkoder.com/integrate-react-express-same-server-port/) 100 | 101 | Serverless: 102 | > [React Firebase CRUD App with Realtime Database](https://bezkoder.com/react-firebase-crud/) 103 | 104 | > [React Firestore CRUD App example | Firebase Cloud Firestore](https://bezkoder.com/react-firestore-crud/) 105 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-jwt-refresh-token", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.4", 7 | "@testing-library/react": "^11.1.0", 8 | "@testing-library/user-event": "^12.1.10", 9 | "axios": "^0.21.1", 10 | "bootstrap": "^4.6.0", 11 | "react": "^17.0.2", 12 | "react-dom": "^17.0.2", 13 | "react-redux": "^7.2.4", 14 | "react-router-dom": "^5.2.0", 15 | "react-scripts": "4.0.3", 16 | "react-validation": "^3.0.7", 17 | "redux": "^4.1.0", 18 | "redux-thunk": "^2.3.0", 19 | "validator": "^13.6.0", 20 | "web-vitals": "^1.0.1" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject" 27 | }, 28 | "eslintConfig": { 29 | "extends": [ 30 | "react-app", 31 | "react-app/jest" 32 | ] 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "devDependencies": { 47 | "redux-devtools-extension": "^2.13.9" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bezkoder/redux-refresh-token-axios/0a7da8223e340891f5955f626519194a5bdbed7e/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bezkoder/redux-refresh-token-axios/0a7da8223e340891f5955f626519194a5bdbed7e/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bezkoder/redux-refresh-token-axios/0a7da8223e340891f5955f626519194a5bdbed7e/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /react-redux-refresh-token-axios-jwt-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bezkoder/redux-refresh-token-axios/0a7da8223e340891f5955f626519194a5bdbed7e/react-redux-refresh-token-axios-jwt-flow.png -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | label { 2 | display: block; 3 | margin-top: 10px; 4 | } 5 | 6 | .card-container.card { 7 | max-width: 350px !important; 8 | padding: 40px 40px; 9 | } 10 | 11 | .card { 12 | background-color: #f7f7f7; 13 | padding: 20px 25px 30px; 14 | margin: 0 auto 25px; 15 | margin-top: 50px; 16 | -moz-border-radius: 2px; 17 | -webkit-border-radius: 2px; 18 | border-radius: 2px; 19 | -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); 20 | -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); 21 | box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); 22 | } 23 | 24 | .profile-img-card { 25 | width: 96px; 26 | height: 96px; 27 | margin: 0 auto 10px; 28 | display: block; 29 | -moz-border-radius: 50%; 30 | -webkit-border-radius: 50%; 31 | border-radius: 50%; 32 | } -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import { Router, Switch, Route, Link } from "react-router-dom"; 4 | 5 | import "bootstrap/dist/css/bootstrap.min.css"; 6 | import "./App.css"; 7 | 8 | import Login from "./components/login.component"; 9 | import Register from "./components/register.component"; 10 | import Home from "./components/home.component"; 11 | import Profile from "./components/profile.component"; 12 | import BoardUser from "./components/board-user.component"; 13 | import BoardModerator from "./components/board-moderator.component"; 14 | import BoardAdmin from "./components/board-admin.component"; 15 | 16 | import { logout } from "./actions/auth"; 17 | import { clearMessage } from "./actions/message"; 18 | 19 | import { history } from './helpers/history'; 20 | 21 | import EventBus from "./common/EventBus"; 22 | 23 | class App extends Component { 24 | constructor(props) { 25 | super(props); 26 | this.logOut = this.logOut.bind(this); 27 | 28 | this.state = { 29 | showModeratorBoard: false, 30 | showAdminBoard: false, 31 | currentUser: undefined, 32 | }; 33 | 34 | history.listen((location) => { 35 | props.dispatch(clearMessage()); // clear message when changing location 36 | }); 37 | } 38 | 39 | componentDidMount() { 40 | const user = this.props.user; 41 | 42 | if (user) { 43 | this.setState({ 44 | currentUser: user, 45 | showModeratorBoard: user.roles.includes("ROLE_MODERATOR"), 46 | showAdminBoard: user.roles.includes("ROLE_ADMIN"), 47 | }); 48 | } 49 | 50 | EventBus.on("logout", () => { 51 | this.logOut(); 52 | }); 53 | } 54 | 55 | componentWillUnmount() { 56 | EventBus.remove("logout"); 57 | } 58 | 59 | logOut() { 60 | this.props.dispatch(logout()); 61 | this.setState({ 62 | showModeratorBoard: false, 63 | showAdminBoard: false, 64 | currentUser: undefined, 65 | }); 66 | } 67 | 68 | render() { 69 | const { currentUser, showModeratorBoard, showAdminBoard } = this.state; 70 | 71 | return ( 72 | 73 |
74 | 139 | 140 |
141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 |
151 |
152 |
153 | ); 154 | } 155 | } 156 | 157 | function mapStateToProps(state) { 158 | const { user } = state.auth; 159 | return { 160 | user, 161 | }; 162 | } 163 | 164 | export default connect(mapStateToProps)(App); 165 | -------------------------------------------------------------------------------- /src/actions/auth.js: -------------------------------------------------------------------------------- 1 | import { 2 | REGISTER_SUCCESS, 3 | REGISTER_FAIL, 4 | LOGIN_SUCCESS, 5 | LOGIN_FAIL, 6 | LOGOUT, 7 | SET_MESSAGE, 8 | REFRESH_TOKEN 9 | } from "./types"; 10 | 11 | import AuthService from "../services/auth.service"; 12 | 13 | export const register = (username, email, password) => (dispatch) => { 14 | return AuthService.register(username, email, password).then( 15 | (response) => { 16 | dispatch({ 17 | type: REGISTER_SUCCESS, 18 | }); 19 | 20 | dispatch({ 21 | type: SET_MESSAGE, 22 | payload: response.data.message, 23 | }); 24 | 25 | return Promise.resolve(); 26 | }, 27 | (error) => { 28 | const message = 29 | (error.response && 30 | error.response.data && 31 | error.response.data.message) || 32 | error.message || 33 | error.toString(); 34 | 35 | dispatch({ 36 | type: REGISTER_FAIL, 37 | }); 38 | 39 | dispatch({ 40 | type: SET_MESSAGE, 41 | payload: message, 42 | }); 43 | 44 | return Promise.reject(); 45 | } 46 | ); 47 | }; 48 | 49 | export const login = (username, password) => (dispatch) => { 50 | return AuthService.login(username, password).then( 51 | (data) => { 52 | dispatch({ 53 | type: LOGIN_SUCCESS, 54 | payload: { user: data }, 55 | }); 56 | 57 | return Promise.resolve(); 58 | }, 59 | (error) => { 60 | const message = 61 | (error.response && 62 | error.response.data && 63 | error.response.data.message) || 64 | error.message || 65 | error.toString(); 66 | 67 | dispatch({ 68 | type: LOGIN_FAIL, 69 | }); 70 | 71 | dispatch({ 72 | type: SET_MESSAGE, 73 | payload: message, 74 | }); 75 | 76 | return Promise.reject(); 77 | } 78 | ); 79 | }; 80 | 81 | export const logout = () => (dispatch) => { 82 | AuthService.logout(); 83 | 84 | dispatch({ 85 | type: LOGOUT, 86 | }); 87 | }; 88 | 89 | export const refreshToken = (accessToken) => (dispatch) => { 90 | dispatch({ 91 | type: REFRESH_TOKEN, 92 | payload: accessToken, 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /src/actions/message.js: -------------------------------------------------------------------------------- 1 | import { SET_MESSAGE, CLEAR_MESSAGE } from "./types"; 2 | 3 | export const setMessage = (message) => ({ 4 | type: SET_MESSAGE, 5 | payload: message, 6 | }); 7 | 8 | export const clearMessage = () => ({ 9 | type: CLEAR_MESSAGE, 10 | }); 11 | -------------------------------------------------------------------------------- /src/actions/types.js: -------------------------------------------------------------------------------- 1 | export const REGISTER_SUCCESS = "REGISTER_SUCCESS"; 2 | export const REGISTER_FAIL = "REGISTER_FAIL"; 3 | export const LOGIN_SUCCESS = "LOGIN_SUCCESS"; 4 | export const LOGIN_FAIL = "LOGIN_FAIL"; 5 | export const LOGOUT = "LOGOUT"; 6 | export const REFRESH_TOKEN = "REFRESH_TOKEN"; 7 | 8 | export const SET_MESSAGE = "SET_MESSAGE"; 9 | export const CLEAR_MESSAGE = "CLEAR_MESSAGE"; 10 | -------------------------------------------------------------------------------- /src/common/EventBus.js: -------------------------------------------------------------------------------- 1 | const eventBus = { 2 | on(event, callback) { 3 | document.addEventListener(event, (e) => callback(e.detail)); 4 | }, 5 | dispatch(event, data) { 6 | document.dispatchEvent(new CustomEvent(event, { detail: data })); 7 | }, 8 | remove(event, callback) { 9 | document.removeEventListener(event, callback); 10 | }, 11 | }; 12 | 13 | export default eventBus; 14 | -------------------------------------------------------------------------------- /src/components/board-admin.component.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | import UserService from "../services/user.service"; 4 | import EventBus from "../common/EventBus"; 5 | 6 | export default class BoardAdmin extends Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | content: "" 12 | }; 13 | } 14 | 15 | componentDidMount() { 16 | UserService.getAdminBoard().then( 17 | response => { 18 | this.setState({ 19 | content: response.data 20 | }); 21 | }, 22 | error => { 23 | this.setState({ 24 | content: 25 | (error.response && 26 | error.response.data && 27 | error.response.data.message) || 28 | error.message || 29 | error.toString() 30 | }); 31 | 32 | if (error.response && error.response.status === 403) { 33 | EventBus.dispatch("logout"); 34 | } 35 | } 36 | ); 37 | } 38 | 39 | render() { 40 | return ( 41 |
42 |
43 |

{this.state.content}

44 |
45 |
46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/components/board-moderator.component.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | import UserService from "../services/user.service"; 4 | import EventBus from "../common/EventBus"; 5 | 6 | export default class BoardModerator extends Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | content: "" 12 | }; 13 | } 14 | 15 | componentDidMount() { 16 | UserService.getModeratorBoard().then( 17 | response => { 18 | this.setState({ 19 | content: response.data 20 | }); 21 | }, 22 | error => { 23 | this.setState({ 24 | content: 25 | (error.response && 26 | error.response.data && 27 | error.response.data.message) || 28 | error.message || 29 | error.toString() 30 | }); 31 | 32 | if (error.response && error.response.status === 403) { 33 | EventBus.dispatch("logout"); 34 | } 35 | } 36 | ); 37 | } 38 | 39 | render() { 40 | return ( 41 |
42 |
43 |

{this.state.content}

44 |
45 |
46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/components/board-user.component.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | import UserService from "../services/user.service"; 4 | import EventBus from "../common/EventBus"; 5 | 6 | export default class BoardUser extends Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | content: "" 12 | }; 13 | } 14 | 15 | componentDidMount() { 16 | UserService.getUserBoard().then( 17 | response => { 18 | this.setState({ 19 | content: response.data 20 | }); 21 | }, 22 | error => { 23 | this.setState({ 24 | content: 25 | (error.response && 26 | error.response.data && 27 | error.response.data.message) || 28 | error.message || 29 | error.toString() 30 | }); 31 | 32 | if (error.response && error.response.status === 403) { 33 | EventBus.dispatch("logout"); 34 | } 35 | } 36 | ); 37 | } 38 | 39 | render() { 40 | return ( 41 |
42 |
43 |

{this.state.content}

44 |
45 |
46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/components/home.component.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | import UserService from "../services/user.service"; 4 | 5 | export default class Home extends Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { 10 | content: "" 11 | }; 12 | } 13 | 14 | componentDidMount() { 15 | UserService.getPublicContent().then( 16 | response => { 17 | this.setState({ 18 | content: response.data 19 | }); 20 | }, 21 | error => { 22 | this.setState({ 23 | content: 24 | (error.response && error.response.data) || 25 | error.message || 26 | error.toString() 27 | }); 28 | } 29 | ); 30 | } 31 | 32 | render() { 33 | return ( 34 |
35 |
36 |

{this.state.content}

37 |
38 |
39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/login.component.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Redirect } from 'react-router-dom'; 3 | 4 | import Form from "react-validation/build/form"; 5 | import Input from "react-validation/build/input"; 6 | import CheckButton from "react-validation/build/button"; 7 | 8 | import { connect } from "react-redux"; 9 | import { login } from "../actions/auth"; 10 | 11 | const required = (value) => { 12 | if (!value) { 13 | return ( 14 |
15 | This field is required! 16 |
17 | ); 18 | } 19 | }; 20 | 21 | class Login extends Component { 22 | constructor(props) { 23 | super(props); 24 | this.handleLogin = this.handleLogin.bind(this); 25 | this.onChangeUsername = this.onChangeUsername.bind(this); 26 | this.onChangePassword = this.onChangePassword.bind(this); 27 | 28 | this.state = { 29 | username: "", 30 | password: "", 31 | loading: false, 32 | }; 33 | } 34 | 35 | onChangeUsername(e) { 36 | this.setState({ 37 | username: e.target.value, 38 | }); 39 | } 40 | 41 | onChangePassword(e) { 42 | this.setState({ 43 | password: e.target.value, 44 | }); 45 | } 46 | 47 | handleLogin(e) { 48 | e.preventDefault(); 49 | 50 | this.setState({ 51 | loading: true, 52 | }); 53 | 54 | this.form.validateAll(); 55 | 56 | const { dispatch, history } = this.props; 57 | 58 | if (this.checkBtn.context._errors.length === 0) { 59 | dispatch(login(this.state.username, this.state.password)) 60 | .then(() => { 61 | history.push("/profile"); 62 | window.location.reload(); 63 | }) 64 | .catch(() => { 65 | this.setState({ 66 | loading: false 67 | }); 68 | }); 69 | } else { 70 | this.setState({ 71 | loading: false, 72 | }); 73 | } 74 | } 75 | 76 | render() { 77 | const { isLoggedIn, message } = this.props; 78 | 79 | if (isLoggedIn) { 80 | return ; 81 | } 82 | 83 | return ( 84 |
85 |
86 | profile-img 91 | 92 |
{ 95 | this.form = c; 96 | }} 97 | > 98 |
99 | 100 | 108 |
109 | 110 |
111 | 112 | 120 |
121 | 122 |
123 | 132 |
133 | 134 | {message && ( 135 |
136 |
137 | {message} 138 |
139 |
140 | )} 141 | { 144 | this.checkBtn = c; 145 | }} 146 | /> 147 | 148 |
149 |
150 | ); 151 | } 152 | } 153 | 154 | function mapStateToProps(state) { 155 | const { isLoggedIn } = state.auth; 156 | const { message } = state.message; 157 | return { 158 | isLoggedIn, 159 | message 160 | }; 161 | } 162 | 163 | export default connect(mapStateToProps)(Login); 164 | -------------------------------------------------------------------------------- /src/components/profile.component.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Redirect } from 'react-router-dom'; 3 | import { connect } from "react-redux"; 4 | 5 | class Profile extends Component { 6 | 7 | render() { 8 | const { user: currentUser } = this.props; 9 | 10 | if (!currentUser) { 11 | return ; 12 | } 13 | 14 | return ( 15 |
16 |
17 |

18 | {currentUser.username} Profile 19 |

20 |
21 |

22 | Token: {currentUser.accessToken.substring(0, 20)} ...{" "} 23 | {currentUser.accessToken.substr(currentUser.accessToken.length - 20)} 24 |

25 |

26 | Id: {currentUser.id} 27 |

28 |

29 | Email: {currentUser.email} 30 |

31 | Authorities: 32 |
    33 | {currentUser.roles && 34 | currentUser.roles.map((role, index) =>
  • {role}
  • )} 35 |
36 |
37 | ); 38 | } 39 | } 40 | 41 | function mapStateToProps(state) { 42 | const { user } = state.auth; 43 | return { 44 | user, 45 | }; 46 | } 47 | 48 | export default connect(mapStateToProps)(Profile); 49 | -------------------------------------------------------------------------------- /src/components/register.component.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import Form from "react-validation/build/form"; 3 | import Input from "react-validation/build/input"; 4 | import CheckButton from "react-validation/build/button"; 5 | import { isEmail } from "validator"; 6 | 7 | import { connect } from "react-redux"; 8 | import { register } from "../actions/auth"; 9 | 10 | const required = (value) => { 11 | if (!value) { 12 | return ( 13 |
14 | This field is required! 15 |
16 | ); 17 | } 18 | }; 19 | 20 | const email = (value) => { 21 | if (!isEmail(value)) { 22 | return ( 23 |
24 | This is not a valid email. 25 |
26 | ); 27 | } 28 | }; 29 | 30 | const vusername = (value) => { 31 | if (value.length < 3 || value.length > 20) { 32 | return ( 33 |
34 | The username must be between 3 and 20 characters. 35 |
36 | ); 37 | } 38 | }; 39 | 40 | const vpassword = (value) => { 41 | if (value.length < 6 || value.length > 40) { 42 | return ( 43 |
44 | The password must be between 6 and 40 characters. 45 |
46 | ); 47 | } 48 | }; 49 | 50 | class Register extends Component { 51 | constructor(props) { 52 | super(props); 53 | this.handleRegister = this.handleRegister.bind(this); 54 | this.onChangeUsername = this.onChangeUsername.bind(this); 55 | this.onChangeEmail = this.onChangeEmail.bind(this); 56 | this.onChangePassword = this.onChangePassword.bind(this); 57 | 58 | this.state = { 59 | username: "", 60 | email: "", 61 | password: "", 62 | successful: false, 63 | }; 64 | } 65 | 66 | onChangeUsername(e) { 67 | this.setState({ 68 | username: e.target.value, 69 | }); 70 | } 71 | 72 | onChangeEmail(e) { 73 | this.setState({ 74 | email: e.target.value, 75 | }); 76 | } 77 | 78 | onChangePassword(e) { 79 | this.setState({ 80 | password: e.target.value, 81 | }); 82 | } 83 | 84 | handleRegister(e) { 85 | e.preventDefault(); 86 | 87 | this.setState({ 88 | successful: false, 89 | }); 90 | 91 | this.form.validateAll(); 92 | 93 | if (this.checkBtn.context._errors.length === 0) { 94 | this.props 95 | .dispatch( 96 | register(this.state.username, this.state.email, this.state.password) 97 | ) 98 | .then(() => { 99 | this.setState({ 100 | successful: true, 101 | }); 102 | }) 103 | .catch(() => { 104 | this.setState({ 105 | successful: false, 106 | }); 107 | }); 108 | } 109 | } 110 | 111 | render() { 112 | const { message } = this.props; 113 | 114 | return ( 115 |
116 |
117 | profile-img 122 | 123 |
{ 126 | this.form = c; 127 | }} 128 | > 129 | {!this.state.successful && ( 130 |
131 |
132 | 133 | 141 |
142 | 143 |
144 | 145 | 153 |
154 | 155 |
156 | 157 | 165 |
166 | 167 |
168 | 169 |
170 |
171 | )} 172 | 173 | {message && ( 174 |
175 |
176 | {message} 177 |
178 |
179 | )} 180 | { 183 | this.checkBtn = c; 184 | }} 185 | /> 186 | 187 |
188 |
189 | ); 190 | } 191 | } 192 | 193 | function mapStateToProps(state) { 194 | const { message } = state.message; 195 | return { 196 | message, 197 | }; 198 | } 199 | 200 | export default connect(mapStateToProps)(Register); 201 | -------------------------------------------------------------------------------- /src/helpers/history.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from "history"; 2 | 3 | export const history = createBrowserHistory(); 4 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { Provider } from "react-redux"; 4 | import store from "./store"; 5 | import "./index.css"; 6 | import App from "./App"; 7 | import reportWebVitals from "./reportWebVitals"; 8 | 9 | import setupInterceptors from "./services/setupInterceptors"; 10 | 11 | ReactDOM.render( 12 | 13 | 14 | , 15 | document.getElementById("root") 16 | ); 17 | 18 | setupInterceptors(store); 19 | 20 | // If you want to start measuring performance in your app, pass a function 21 | // to log results (for example: reportWebVitals(console.log)) 22 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 23 | reportWebVitals(); 24 | -------------------------------------------------------------------------------- /src/reducers/auth.js: -------------------------------------------------------------------------------- 1 | import { 2 | REGISTER_SUCCESS, 3 | REGISTER_FAIL, 4 | LOGIN_SUCCESS, 5 | LOGIN_FAIL, 6 | LOGOUT, 7 | REFRESH_TOKEN 8 | } from "../actions/types"; 9 | 10 | const user = JSON.parse(localStorage.getItem("user")); 11 | 12 | const initialState = user 13 | ? { isLoggedIn: true, user } 14 | : { isLoggedIn: false, user: null }; 15 | 16 | export default function (state = initialState, action) { 17 | const { type, payload } = action; 18 | 19 | switch (type) { 20 | case REGISTER_SUCCESS: 21 | return { 22 | ...state, 23 | isLoggedIn: false, 24 | }; 25 | case REGISTER_FAIL: 26 | return { 27 | ...state, 28 | isLoggedIn: false, 29 | }; 30 | case LOGIN_SUCCESS: 31 | return { 32 | ...state, 33 | isLoggedIn: true, 34 | user: payload.user, 35 | }; 36 | case LOGIN_FAIL: 37 | return { 38 | ...state, 39 | isLoggedIn: false, 40 | user: null, 41 | }; 42 | case LOGOUT: 43 | return { 44 | ...state, 45 | isLoggedIn: false, 46 | user: null, 47 | }; 48 | case REFRESH_TOKEN: 49 | return { 50 | ...state, 51 | user: { ...user, accessToken: payload }, 52 | }; 53 | default: 54 | return state; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import auth from "./auth"; 3 | import message from "./message"; 4 | 5 | export default combineReducers({ 6 | auth, 7 | message, 8 | }); 9 | -------------------------------------------------------------------------------- /src/reducers/message.js: -------------------------------------------------------------------------------- 1 | import { SET_MESSAGE, CLEAR_MESSAGE } from "../actions/types"; 2 | 3 | const initialState = {}; 4 | 5 | export default function (state = initialState, action) { 6 | const { type, payload } = action; 7 | 8 | switch (type) { 9 | case SET_MESSAGE: 10 | return { message: payload }; 11 | 12 | case CLEAR_MESSAGE: 13 | return { message: "" }; 14 | 15 | default: 16 | return state; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/services/api.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const instance = axios.create({ 4 | baseURL: "http://localhost:8080/api", 5 | headers: { 6 | "Content-Type": "application/json", 7 | }, 8 | }); 9 | 10 | export default instance; 11 | -------------------------------------------------------------------------------- /src/services/auth.service.js: -------------------------------------------------------------------------------- 1 | import api from "./api"; 2 | import TokenService from "./token.service"; 3 | 4 | class AuthService { 5 | login(username, password) { 6 | return api 7 | .post("/auth/signin", { 8 | username, 9 | password 10 | }) 11 | .then(response => { 12 | if (response.data.accessToken) { 13 | TokenService.setUser(response.data); 14 | } 15 | 16 | return response.data; 17 | }); 18 | } 19 | 20 | logout() { 21 | TokenService.removeUser(); 22 | } 23 | 24 | register(username, email, password) { 25 | return api.post("/auth/signup", { 26 | username, 27 | email, 28 | password 29 | }); 30 | } 31 | } 32 | 33 | export default new AuthService(); 34 | -------------------------------------------------------------------------------- /src/services/setupInterceptors.js: -------------------------------------------------------------------------------- 1 | import axiosInstance from "./api"; 2 | import TokenService from "./token.service"; 3 | import { refreshToken } from "../actions/auth"; 4 | 5 | const setup = (store) => { 6 | axiosInstance.interceptors.request.use( 7 | (config) => { 8 | const token = TokenService.getLocalAccessToken(); 9 | if (token) { 10 | // config.headers["Authorization"] = 'Bearer ' + token; // for Spring Boot back-end 11 | config.headers["x-access-token"] = token; // for Node.js Express back-end 12 | } 13 | return config; 14 | }, 15 | (error) => { 16 | return Promise.reject(error); 17 | } 18 | ); 19 | 20 | const { dispatch } = store; 21 | axiosInstance.interceptors.response.use( 22 | (res) => { 23 | return res; 24 | }, 25 | async (err) => { 26 | const originalConfig = err.config; 27 | 28 | if (originalConfig.url !== "/auth/signin" && err.response) { 29 | // Access Token was expired 30 | if (err.response.status === 401 && !originalConfig._retry) { 31 | originalConfig._retry = true; 32 | 33 | try { 34 | const rs = await axiosInstance.post("/auth/refreshtoken", { 35 | refreshToken: TokenService.getLocalRefreshToken(), 36 | }); 37 | 38 | const { accessToken } = rs.data; 39 | 40 | dispatch(refreshToken(accessToken)); 41 | TokenService.updateLocalAccessToken(accessToken); 42 | 43 | return axiosInstance(originalConfig); 44 | } catch (_error) { 45 | return Promise.reject(_error); 46 | } 47 | } 48 | } 49 | 50 | return Promise.reject(err); 51 | } 52 | ); 53 | }; 54 | 55 | export default setup; 56 | -------------------------------------------------------------------------------- /src/services/token.service.js: -------------------------------------------------------------------------------- 1 | class TokenService { 2 | getLocalRefreshToken() { 3 | const user = JSON.parse(localStorage.getItem("user")); 4 | return user?.refreshToken; 5 | } 6 | 7 | getLocalAccessToken() { 8 | const user = JSON.parse(localStorage.getItem("user")); 9 | return user?.accessToken; 10 | } 11 | 12 | updateLocalAccessToken(token) { 13 | let user = JSON.parse(localStorage.getItem("user")); 14 | user.accessToken = token; 15 | localStorage.setItem("user", JSON.stringify(user)); 16 | } 17 | 18 | getUser() { 19 | return JSON.parse(localStorage.getItem("user")); 20 | } 21 | 22 | setUser(user) { 23 | console.log(JSON.stringify(user)); 24 | localStorage.setItem("user", JSON.stringify(user)); 25 | } 26 | 27 | removeUser() { 28 | localStorage.removeItem("user"); 29 | } 30 | } 31 | 32 | export default new TokenService(); 33 | -------------------------------------------------------------------------------- /src/services/user.service.js: -------------------------------------------------------------------------------- 1 | import api from './api'; 2 | 3 | class UserService { 4 | getPublicContent() { 5 | return api.get('/test/all'); 6 | } 7 | 8 | getUserBoard() { 9 | return api.get('/test/user'); 10 | } 11 | 12 | getModeratorBoard() { 13 | return api.get('/test/mod'); 14 | } 15 | 16 | getAdminBoard() { 17 | return api.get('/test/admin'); 18 | } 19 | } 20 | 21 | export default new UserService(); 22 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from "redux"; 2 | import { composeWithDevTools } from "redux-devtools-extension"; 3 | import thunk from "redux-thunk"; 4 | import rootReducer from "./reducers"; 5 | 6 | const middleware = [thunk]; 7 | 8 | const store = createStore( 9 | rootReducer, 10 | composeWithDevTools(applyMiddleware(...middleware)) 11 | ); 12 | 13 | export default store; 14 | --------------------------------------------------------------------------------