├── .env
├── .gitignore
├── LICENSE
├── README.md
├── jsconfig.json
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
└── robots.txt
└── src
├── App.jsx
├── _actions
├── index.js
└── user.actions.js
├── _components
├── Nav.jsx
├── PrivateRoute.jsx
└── index.js
├── _helpers
├── fake-backend.js
├── fetch-wrapper.js
├── history.js
└── index.js
├── _state
├── auth.js
├── index.js
└── users.js
├── home
├── Home.jsx
└── index.js
├── index.css
├── index.js
└── login
├── Login.jsx
└── index.js
/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_API_URL=http://localhost:4000
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2021 Jason Watmore
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-recoil-basic-authentication-example
2 |
3 | React + Recoil - Basic HTTP Authentication Example & Tutorial
4 |
5 | Full documentation and demo available at https://jasonwatmore.com/post/2021/09/11/react-recoil-basic-http-authentication-tutorial-example!
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src"
4 | },
5 | "include": ["src"]
6 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-recoil-basic-authentication-example",
3 | "version": "0.1.0",
4 | "dependencies": {
5 | "@hookform/resolvers": "^2.8.0",
6 | "history": "^4.10.1",
7 | "react": "^17.0.2",
8 | "react-dom": "^17.0.2",
9 | "react-hook-form": "^7.15.0",
10 | "react-router-dom": "^5.3.0",
11 | "react-scripts": "4.0.3",
12 | "recoil": "^0.4.1",
13 | "yup": "^0.32.9"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test",
19 | "eject": "react-scripts eject"
20 | },
21 | "eslintConfig": {
22 | "extends": "react-app"
23 | },
24 | "browserslist": {
25 | "production": [
26 | ">0.2%",
27 | "not dead",
28 | "not op_mini all"
29 | ],
30 | "development": [
31 | "last 1 chrome version",
32 | "last 1 firefox version",
33 | "last 1 safari version"
34 | ]
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cornflourblue/react-recoil-basic-authentication-example/9dd065c64049029595c4aec3bf192d5a8fb347ef/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | React + Recoil - Basic HTTP Authentication Example
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
25 |
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { Router, Route, Switch, Redirect } from 'react-router-dom';
2 |
3 | import { Nav, PrivateRoute } from '_components';
4 | import { history } from '_helpers';
5 | import { Home } from 'home';
6 | import { Login } from 'login';
7 |
8 | export { App };
9 |
10 | function App() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/_actions/index.js:
--------------------------------------------------------------------------------
1 | export * from './user.actions'
--------------------------------------------------------------------------------
/src/_actions/user.actions.js:
--------------------------------------------------------------------------------
1 | import { useSetRecoilState } from 'recoil';
2 |
3 | import { history, useFetchWrapper } from '_helpers';
4 | import { authAtom, usersAtom } from '_state';
5 |
6 | export { useUserActions };
7 |
8 | function useUserActions () {
9 | const baseUrl = `${process.env.REACT_APP_API_URL}/users`;
10 | const fetchWrapper = useFetchWrapper();
11 | const setAuth = useSetRecoilState(authAtom);
12 | const setUsers = useSetRecoilState(usersAtom);
13 |
14 | return {
15 | login,
16 | logout,
17 | getAll
18 | }
19 |
20 | function login(username, password) {
21 | return fetchWrapper.post(`${baseUrl}/authenticate`, { username, password })
22 | .then(user => {
23 | // update recoil state with user object + basic auth data and
24 | // store in local storage to stay logged in between page refreshes
25 | user.authdata = window.btoa(username + ':' + password);
26 | setAuth(user);
27 | localStorage.setItem('user', JSON.stringify(user));
28 |
29 | // get return url from location state or default to home page
30 | const { from } = history.location.state || { from: { pathname: '/' } };
31 | history.push(from);
32 | });
33 | }
34 |
35 | function logout() {
36 | // remove user from local storage, set auth state to null and redirect to login page
37 | localStorage.removeItem('user');
38 | setAuth(null);
39 | history.push('/login');
40 | }
41 |
42 | function getAll() {
43 | return fetchWrapper.get(baseUrl).then(setUsers);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/_components/Nav.jsx:
--------------------------------------------------------------------------------
1 | import { NavLink } from 'react-router-dom';
2 | import { useRecoilValue } from 'recoil';
3 |
4 | import { authAtom } from '_state';
5 | import { useUserActions } from '_actions';
6 |
7 | function Nav() {
8 | const auth = useRecoilValue(authAtom);
9 | const userActions = useUserActions();
10 |
11 | // only show nav when logged in
12 | if (!auth) return null;
13 |
14 | return (
15 |
21 | );
22 | }
23 |
24 | export { Nav };
--------------------------------------------------------------------------------
/src/_components/PrivateRoute.jsx:
--------------------------------------------------------------------------------
1 | import { Route, Redirect } from 'react-router-dom';
2 | import { useRecoilValue } from 'recoil';
3 |
4 | import { authAtom } from '_state';
5 |
6 | export { PrivateRoute };
7 |
8 | function PrivateRoute({ component: Component, ...rest }) {
9 | const auth = useRecoilValue(authAtom);
10 | return (
11 | {
12 | if (!auth) {
13 | // not logged in so redirect to login page with the return url
14 | return
15 | }
16 |
17 | // authorized so return component
18 | return
19 | }} />
20 | );
21 | }
--------------------------------------------------------------------------------
/src/_components/index.js:
--------------------------------------------------------------------------------
1 | export * from './Nav';
2 | export * from './PrivateRoute';
3 |
--------------------------------------------------------------------------------
/src/_helpers/fake-backend.js:
--------------------------------------------------------------------------------
1 | export { fakeBackend };
2 |
3 | function fakeBackend() {
4 | let users = [{ id: 1, username: 'test', password: 'test', firstName: 'Test', lastName: 'User' }];
5 | let realFetch = window.fetch;
6 | window.fetch = function (url, opts) {
7 | return new Promise((resolve, reject) => {
8 | // wrap in timeout to simulate server api call
9 | setTimeout(handleRoute, 500);
10 |
11 | function handleRoute() {
12 | switch (true) {
13 | case url.endsWith('/users/authenticate') && opts.method === 'POST':
14 | return authenticate();
15 | case url.endsWith('/users') && opts.method === 'GET':
16 | return getUsers();
17 | default:
18 | // pass through any requests not handled above
19 | return realFetch(url, opts)
20 | .then(response => resolve(response))
21 | .catch(error => reject(error));
22 | }
23 | }
24 |
25 | // route functions
26 |
27 | function authenticate() {
28 | const { username, password } = body();
29 | const user = users.find(x => x.username === username && x.password === password);
30 |
31 | if (!user) return error('Username or password is incorrect');
32 |
33 | // return basic user details on success
34 | return ok({
35 | id: user.id,
36 | username: user.username,
37 | firstName: user.firstName,
38 | lastName: user.lastName
39 | });
40 | }
41 |
42 | function getUsers() {
43 | if (!isAuthenticated()) return unauthorized();
44 | return ok(users);
45 | }
46 |
47 | // helper functions
48 |
49 | function ok(body) {
50 | resolve({ ok: true, text: () => Promise.resolve(JSON.stringify(body)) });
51 | }
52 |
53 | function unauthorized() {
54 | resolve({ status: 401, text: () => Promise.resolve(JSON.stringify({ message: 'Unauthorized' })) });
55 | }
56 |
57 | function error(message) {
58 | resolve({ status: 400, text: () => Promise.resolve(JSON.stringify({ message })) });
59 | }
60 |
61 | function isAuthenticated() {
62 | return opts.headers['Authorization'] === `Basic ${window.btoa('test:test')}`;
63 | }
64 |
65 | function body() {
66 | return opts.body && JSON.parse(opts.body);
67 | }
68 | });
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/_helpers/fetch-wrapper.js:
--------------------------------------------------------------------------------
1 | import { useRecoilState } from 'recoil';
2 |
3 | import { history } from '_helpers';
4 | import { authAtom } from '_state';
5 |
6 | export { useFetchWrapper };
7 |
8 | function useFetchWrapper() {
9 | const [auth, setAuth] = useRecoilState(authAtom);
10 |
11 | return {
12 | get: request('GET'),
13 | post: request('POST'),
14 | put: request('PUT'),
15 | delete: request('DELETE')
16 | };
17 |
18 | function request(method) {
19 | return (url, body) => {
20 | const requestOptions = {
21 | method,
22 | headers: authHeader(url)
23 | };
24 | if (body) {
25 | requestOptions.headers['Content-Type'] = 'application/json';
26 | requestOptions.body = JSON.stringify(body);
27 | }
28 | return fetch(url, requestOptions).then(handleResponse);
29 | }
30 | }
31 |
32 | // helper functions
33 |
34 | function authHeader(url) {
35 | // return auth header with basic auth credentials if user is logged in and request is to the api url
36 | const isLoggedIn = !!auth?.authdata;
37 | const isApiUrl = url.startsWith(process.env.REACT_APP_API_URL);
38 | if (isLoggedIn && isApiUrl) {
39 | return { Authorization: `Basic ${auth?.authdata}` };
40 | } else {
41 | return {};
42 | }
43 | }
44 |
45 | function handleResponse(response) {
46 | return response.text().then(text => {
47 | const data = text && JSON.parse(text);
48 |
49 | if (!response.ok) {
50 | if ([401, 403].includes(response.status) && auth?.authdata) {
51 | // auto logout if 401 Unauthorized or 403 Forbidden response returned from api
52 | localStorage.removeItem('user');
53 | setAuth(null);
54 | history.push('/login');
55 | }
56 |
57 | const error = (data && data.message) || response.statusText;
58 | return Promise.reject(error);
59 | }
60 |
61 | return data;
62 | });
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/_helpers/history.js:
--------------------------------------------------------------------------------
1 | import { createBrowserHistory } from 'history';
2 |
3 | const history = createBrowserHistory();
4 |
5 | export { history };
--------------------------------------------------------------------------------
/src/_helpers/index.js:
--------------------------------------------------------------------------------
1 | export * from './fake-backend';
2 | export * from './fetch-wrapper';
3 | export * from './history';
4 |
--------------------------------------------------------------------------------
/src/_state/auth.js:
--------------------------------------------------------------------------------
1 | import { atom } from 'recoil';
2 |
3 | const authAtom = atom({
4 | key: 'auth',
5 | // get initial state from local storage to enable user to stay logged in
6 | default: JSON.parse(localStorage.getItem('user'))
7 | });
8 |
9 | export { authAtom };
--------------------------------------------------------------------------------
/src/_state/index.js:
--------------------------------------------------------------------------------
1 | export * from './auth';
2 | export * from './users';
--------------------------------------------------------------------------------
/src/_state/users.js:
--------------------------------------------------------------------------------
1 | import { atom } from 'recoil';
2 |
3 | const usersAtom = atom({
4 | key: 'users',
5 | default: null
6 | });
7 |
8 | export { usersAtom };
--------------------------------------------------------------------------------
/src/home/Home.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useRecoilValue } from 'recoil';
3 |
4 | import { authAtom, usersAtom } from '_state';
5 | import { useUserActions } from '_actions';
6 |
7 | export { Home };
8 |
9 | function Home() {
10 | const auth = useRecoilValue(authAtom);
11 | const users = useRecoilValue(usersAtom);
12 | const userActions = useUserActions();
13 |
14 | useEffect(() => {
15 | userActions.getAll();
16 |
17 | // eslint-disable-next-line react-hooks/exhaustive-deps
18 | }, []);
19 |
20 | return (
21 |
22 |
Hi {auth?.firstName}!
23 |
You're logged in with React + Recoil and HTTP Basic Authentication!!
24 |
Users from secure api end point:
25 | {users &&
26 |
27 | {users.map(user =>
28 | - {user.firstName} {user.lastName}
29 | )}
30 |
31 | }
32 | {!users &&
}
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/src/home/index.js:
--------------------------------------------------------------------------------
1 | export * from './Home';
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | a { cursor: pointer; }
2 |
3 | .app-container {
4 | min-height: 350px;
5 | }
6 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { RecoilRoot } from 'recoil';
4 |
5 | import './index.css';
6 | import { App } from './App';
7 |
8 | // setup fake backend
9 | import { fakeBackend } from './_helpers';
10 | fakeBackend();
11 |
12 | ReactDOM.render(
13 |
14 |
15 |
16 |
17 | ,
18 | document.getElementById('app')
19 | );
20 |
--------------------------------------------------------------------------------
/src/login/Login.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useForm } from "react-hook-form";
3 | import { yupResolver } from '@hookform/resolvers/yup';
4 | import * as Yup from 'yup';
5 | import { useRecoilValue } from 'recoil';
6 |
7 | import { authAtom } from '_state';
8 | import { useUserActions } from '_actions';
9 |
10 | export { Login };
11 |
12 | function Login({ history }) {
13 | const auth = useRecoilValue(authAtom);
14 | const userActions = useUserActions();
15 |
16 | useEffect(() => {
17 | // redirect to home if already logged in
18 | if (auth) history.push('/');
19 |
20 | // eslint-disable-next-line react-hooks/exhaustive-deps
21 | }, []);
22 |
23 | // form validation rules
24 | const validationSchema = Yup.object().shape({
25 | username: Yup.string().required('Username is required'),
26 | password: Yup.string().required('Password is required')
27 | });
28 | const formOptions = { resolver: yupResolver(validationSchema) };
29 |
30 | // get functions to build form with useForm() hook
31 | const { register, handleSubmit, setError, formState } = useForm(formOptions);
32 | const { errors, isSubmitting } = formState;
33 |
34 | function onSubmit({ username, password }) {
35 | return userActions.login(username, password)
36 | .catch(error => {
37 | setError('apiError', { message: error });
38 | });
39 | }
40 |
41 | return (
42 |
43 |
44 | Username: test
45 | Password: test
46 |
47 |
71 |
72 | )
73 | }
74 |
--------------------------------------------------------------------------------
/src/login/index.js:
--------------------------------------------------------------------------------
1 | export * from './Login';
--------------------------------------------------------------------------------