├── .env ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── react-refresh-token-jwt-axios-interceptors-flow.png ├── src ├── App.css ├── App.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 ├── index.css ├── index.js ├── logo.svg ├── reportWebVitals.js ├── services │ ├── api.js │ ├── auth.service.js │ ├── token.service.js │ └── user.service.js └── setupTests.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 Refresh Token with JWT and Axios Interceptors example 2 | 3 | ![react-refresh-token-jwt-axios-interceptors-flow](react-refresh-token-jwt-axios-interceptors-flow.png) 4 | 5 | For more detail, please visit: 6 | > [React Refresh Token with JWT and Axios Interceptors](https://www.bezkoder.com/react-refresh-token/) 7 | 8 | > [React JWT Authentication & Authorization example](https://bezkoder.com/react-jwt-auth/) 9 | 10 | Fullstack (JWT Authentication & Authorization example): 11 | > [React + Spring Boot](https://bezkoder.com/spring-boot-react-jwt-auth/) 12 | 13 | > [React + Node.js Express](https://bezkoder.com/react-express-authentication-jwt/) 14 | 15 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 16 | 17 | ### Set port 18 | .env 19 | ``` 20 | PORT=8081 21 | ``` 22 | 23 | ## Note: 24 | Open `src/services/api.js` and modify `config.headers` for appropriate back-end (found in the tutorial). 25 | 26 | ```js 27 | instance.interceptors.request.use( 28 | (config) => { 29 | const token = TokenService.getLocalAccessToken(); 30 | if (token) { 31 | // config.headers["Authorization"] = 'Bearer ' + token; // for Spring Boot back-end 32 | config.headers["x-access-token"] = token; // for Node.js Express back-end 33 | } 34 | return config; 35 | }, 36 | (error) => { 37 | return Promise.reject(error); 38 | } 39 | ); 40 | ``` 41 | 42 | ## Project setup 43 | 44 | In the project directory, you can run: 45 | 46 | ``` 47 | npm install 48 | # or 49 | yarn install 50 | ``` 51 | 52 | or 53 | 54 | ### Compiles and hot-reloads for development 55 | 56 | ``` 57 | npm start 58 | # or 59 | yarn start 60 | ``` 61 | 62 | Open [http://localhost:8081](http://localhost:8081) to view it in the browser. 63 | 64 | The page will reload if you make edits. 65 | 66 | ## Related Posts 67 | > [In-depth Introduction to JWT-JSON Web Token](https://bezkoder.com/jwt-json-web-token/) 68 | 69 | > [React.js CRUD example to consume Web API](https://bezkoder.com/react-crud-web-api/) 70 | 71 | > [React Pagination example](https://bezkoder.com/react-pagination-material-ui/) 72 | 73 | > [React File Upload with Axios and Progress Bar to Rest API](https://bezkoder.com/react-file-upload-axios/) 74 | 75 | Fullstack (JWT Authentication & Authorization example): 76 | > [React + Spring Boot](https://bezkoder.com/spring-boot-react-jwt-auth/) 77 | 78 | > [React + Node.js Express](https://bezkoder.com/react-express-authentication-jwt/) 79 | 80 | CRUD with Node.js Express: 81 | > [React.js + Node.js Express + MySQL](https://bezkoder.com/react-node-express-mysql/) 82 | 83 | > [React.js + Node.js Express + PostgreSQL](https://bezkoder.com/react-node-express-postgresql/) 84 | 85 | > [React.js + Node.js Express + MongoDB](https://bezkoder.com/react-node-express-mongodb-mern-stack/) 86 | 87 | CRUD with Spring Boot: 88 | > [React.js + Spring Boot + MySQL](https://bezkoder.com/react-spring-boot-crud/) 89 | 90 | > [React.js + Spring Boot + PostgreSQL](https://bezkoder.com/spring-boot-react-postgresql/) 91 | 92 | > [React.js + Spring Boot + MongoDB](https://bezkoder.com/react-spring-boot-mongodb/) 93 | 94 | CRUD with Django: 95 | > [React.js + Django Rest Framework](https://bezkoder.com/django-react-axios-rest-framework/) 96 | 97 | Serverless: 98 | > [React Firebase CRUD App with Realtime Database](https://bezkoder.com/react-firebase-crud/) 99 | 100 | > [React Firestore CRUD App example | Firebase Cloud Firestore](https://bezkoder.com/react-firestore-crud/) 101 | 102 | Integration (run back-end & front-end on same server/port) 103 | > [Integrate React with Spring Boot](https://bezkoder.com/integrate-reactjs-spring-boot/) 104 | 105 | > [Integrate React with Node.js Express](https://bezkoder.com/integrate-react-express-same-server-port/) 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-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-router-dom": "^5.2.0", 14 | "react-scripts": "4.0.3", 15 | "react-validation": "^3.0.7", 16 | "validator": "^13.6.0", 17 | "web-vitals": "^1.0.1" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": [ 27 | "react-app", 28 | "react-app/jest" 29 | ] 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bezkoder/react-jwt-refresh-token/6c1f319f59668d5bae6ad006b4e0fa66ee2a4161/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/react-jwt-refresh-token/6c1f319f59668d5bae6ad006b4e0fa66ee2a4161/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bezkoder/react-jwt-refresh-token/6c1f319f59668d5bae6ad006b4e0fa66ee2a4161/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-refresh-token-jwt-axios-interceptors-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bezkoder/react-jwt-refresh-token/6c1f319f59668d5bae6ad006b4e0fa66ee2a4161/react-refresh-token-jwt-axios-interceptors-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 { Switch, Route, Link } from "react-router-dom"; 3 | import "bootstrap/dist/css/bootstrap.min.css"; 4 | import "./App.css"; 5 | 6 | import AuthService from "./services/auth.service"; 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 EventBus from "./common/EventBus"; 17 | 18 | class App extends Component { 19 | constructor(props) { 20 | super(props); 21 | this.logOut = this.logOut.bind(this); 22 | 23 | this.state = { 24 | showModeratorBoard: false, 25 | showAdminBoard: false, 26 | currentUser: undefined, 27 | }; 28 | } 29 | 30 | componentDidMount() { 31 | const user = AuthService.getCurrentUser(); 32 | 33 | if (user) { 34 | this.setState({ 35 | currentUser: user, 36 | showModeratorBoard: user.roles.includes("ROLE_MODERATOR"), 37 | showAdminBoard: user.roles.includes("ROLE_ADMIN"), 38 | }); 39 | } 40 | 41 | EventBus.on("logout", () => { 42 | this.logOut(); 43 | }); 44 | } 45 | 46 | componentWillUnmount() { 47 | EventBus.remove("logout"); 48 | } 49 | 50 | logOut() { 51 | AuthService.logout(); 52 | this.setState({ 53 | showModeratorBoard: false, 54 | showAdminBoard: false, 55 | currentUser: undefined, 56 | }); 57 | } 58 | 59 | render() { 60 | const { currentUser, showModeratorBoard, showAdminBoard } = this.state; 61 | 62 | return ( 63 |
64 | 129 | 130 |
131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 |
141 |
142 | ); 143 | } 144 | } 145 | 146 | export default App; 147 | -------------------------------------------------------------------------------- /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 Form from "react-validation/build/form"; 3 | import Input from "react-validation/build/input"; 4 | import CheckButton from "react-validation/build/button"; 5 | 6 | import AuthService from "../services/auth.service"; 7 | 8 | const required = value => { 9 | if (!value) { 10 | return ( 11 |
12 | This field is required! 13 |
14 | ); 15 | } 16 | }; 17 | 18 | export default class Login extends Component { 19 | constructor(props) { 20 | super(props); 21 | this.handleLogin = this.handleLogin.bind(this); 22 | this.onChangeUsername = this.onChangeUsername.bind(this); 23 | this.onChangePassword = this.onChangePassword.bind(this); 24 | 25 | this.state = { 26 | username: "", 27 | password: "", 28 | loading: false, 29 | message: "" 30 | }; 31 | } 32 | 33 | onChangeUsername(e) { 34 | this.setState({ 35 | username: e.target.value 36 | }); 37 | } 38 | 39 | onChangePassword(e) { 40 | this.setState({ 41 | password: e.target.value 42 | }); 43 | } 44 | 45 | handleLogin(e) { 46 | e.preventDefault(); 47 | 48 | this.setState({ 49 | message: "", 50 | loading: true 51 | }); 52 | 53 | this.form.validateAll(); 54 | 55 | if (this.checkBtn.context._errors.length === 0) { 56 | AuthService.login(this.state.username, this.state.password).then( 57 | () => { 58 | this.props.history.push("/profile"); 59 | window.location.reload(); 60 | }, 61 | error => { 62 | const resMessage = 63 | (error.response && 64 | error.response.data && 65 | error.response.data.message) || 66 | error.message || 67 | error.toString(); 68 | 69 | this.setState({ 70 | loading: false, 71 | message: resMessage 72 | }); 73 | } 74 | ); 75 | } else { 76 | this.setState({ 77 | loading: false 78 | }); 79 | } 80 | } 81 | 82 | render() { 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 | {this.state.message && ( 135 |
136 |
137 | {this.state.message} 138 |
139 |
140 | )} 141 | { 144 | this.checkBtn = c; 145 | }} 146 | /> 147 | 148 |
149 |
150 | ); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/components/profile.component.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Redirect } from "react-router-dom"; 3 | import AuthService from "../services/auth.service"; 4 | 5 | export default class Profile extends Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { 10 | redirect: null, 11 | userReady: false, 12 | currentUser: { username: "" } 13 | }; 14 | } 15 | 16 | componentDidMount() { 17 | const currentUser = AuthService.getCurrentUser(); 18 | 19 | if (!currentUser) this.setState({ redirect: "/home" }); 20 | this.setState({ currentUser: currentUser, userReady: true }) 21 | } 22 | 23 | render() { 24 | if (this.state.redirect) { 25 | return 26 | } 27 | 28 | const { currentUser } = this.state; 29 | 30 | return ( 31 |
32 | {(this.state.userReady) ? 33 |
34 |
35 |

36 | {currentUser.username} Profile 37 |

38 |
39 |

40 | Token:{" "} 41 | {currentUser.accessToken.substring(0, 20)} ...{" "} 42 | {currentUser.accessToken.substr(currentUser.accessToken.length - 20)} 43 |

44 |

45 | Id:{" "} 46 | {currentUser.id} 47 |

48 |

49 | Email:{" "} 50 | {currentUser.email} 51 |

52 | Authorities: 53 |
    54 | {currentUser.roles && 55 | currentUser.roles.map((role, index) =>
  • {role}
  • )} 56 |
57 |
: null} 58 |
59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /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 AuthService from "../services/auth.service"; 8 | 9 | const required = value => { 10 | if (!value) { 11 | return ( 12 |
13 | This field is required! 14 |
15 | ); 16 | } 17 | }; 18 | 19 | const email = value => { 20 | if (!isEmail(value)) { 21 | return ( 22 |
23 | This is not a valid email. 24 |
25 | ); 26 | } 27 | }; 28 | 29 | const vusername = value => { 30 | if (value.length < 3 || value.length > 20) { 31 | return ( 32 |
33 | The username must be between 3 and 20 characters. 34 |
35 | ); 36 | } 37 | }; 38 | 39 | const vpassword = value => { 40 | if (value.length < 6 || value.length > 40) { 41 | return ( 42 |
43 | The password must be between 6 and 40 characters. 44 |
45 | ); 46 | } 47 | }; 48 | 49 | export default class Register extends Component { 50 | constructor(props) { 51 | super(props); 52 | this.handleRegister = this.handleRegister.bind(this); 53 | this.onChangeUsername = this.onChangeUsername.bind(this); 54 | this.onChangeEmail = this.onChangeEmail.bind(this); 55 | this.onChangePassword = this.onChangePassword.bind(this); 56 | 57 | this.state = { 58 | username: "", 59 | email: "", 60 | password: "", 61 | successful: false, 62 | message: "" 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 | message: "", 89 | successful: false 90 | }); 91 | 92 | this.form.validateAll(); 93 | 94 | if (this.checkBtn.context._errors.length === 0) { 95 | AuthService.register( 96 | this.state.username, 97 | this.state.email, 98 | this.state.password 99 | ).then( 100 | response => { 101 | this.setState({ 102 | message: response.data.message, 103 | successful: true 104 | }); 105 | }, 106 | error => { 107 | const resMessage = 108 | (error.response && 109 | error.response.data && 110 | error.response.data.message) || 111 | error.message || 112 | error.toString(); 113 | 114 | this.setState({ 115 | successful: false, 116 | message: resMessage 117 | }); 118 | } 119 | ); 120 | } 121 | } 122 | 123 | render() { 124 | return ( 125 |
126 |
127 | profile-img 132 | 133 |
{ 136 | this.form = c; 137 | }} 138 | > 139 | {!this.state.successful && ( 140 |
141 |
142 | 143 | 151 |
152 | 153 |
154 | 155 | 163 |
164 | 165 |
166 | 167 | 175 |
176 | 177 |
178 | 179 |
180 |
181 | )} 182 | 183 | {this.state.message && ( 184 |
185 |
193 | {this.state.message} 194 |
195 |
196 | )} 197 | { 200 | this.checkBtn = c; 201 | }} 202 | /> 203 | 204 |
205 |
206 | ); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /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 { BrowserRouter } from "react-router-dom"; 4 | import './index.css'; 5 | import App from './App'; 6 | import reportWebVitals from './reportWebVitals'; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | 15 | // If you want to start measuring performance in your app, pass a function 16 | // to log results (for example: reportWebVitals(console.log)) 17 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 18 | reportWebVitals(); 19 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | import TokenService from "./token.service"; 3 | 4 | const instance = axios.create({ 5 | baseURL: "http://localhost:8080/api", 6 | headers: { 7 | "Content-Type": "application/json", 8 | }, 9 | }); 10 | 11 | instance.interceptors.request.use( 12 | (config) => { 13 | const token = TokenService.getLocalAccessToken(); 14 | if (token) { 15 | // config.headers["Authorization"] = 'Bearer ' + token; // for Spring Boot back-end 16 | config.headers["x-access-token"] = token; // for Node.js Express back-end 17 | } 18 | return config; 19 | }, 20 | (error) => { 21 | return Promise.reject(error); 22 | } 23 | ); 24 | 25 | instance.interceptors.response.use( 26 | (res) => { 27 | return res; 28 | }, 29 | async (err) => { 30 | const originalConfig = err.config; 31 | 32 | if (originalConfig.url !== "/auth/signin" && err.response) { 33 | // Access Token was expired 34 | if (err.response.status === 401 && !originalConfig._retry) { 35 | originalConfig._retry = true; 36 | 37 | try { 38 | const rs = await instance.post("/auth/refreshtoken", { 39 | refreshToken: TokenService.getLocalRefreshToken(), 40 | }); 41 | 42 | const { accessToken } = rs.data; 43 | TokenService.updateLocalAccessToken(accessToken); 44 | 45 | return instance(originalConfig); 46 | } catch (_error) { 47 | return Promise.reject(_error); 48 | } 49 | } 50 | } 51 | 52 | return Promise.reject(err); 53 | } 54 | ); 55 | 56 | export default instance; 57 | -------------------------------------------------------------------------------- /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 | getCurrentUser() { 33 | return TokenService.getUser(); 34 | } 35 | } 36 | 37 | export default new AuthService(); 38 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------