├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── manifest.json └── robots.txt ├── src ├── App.jsx ├── components │ ├── ErrorPage.jsx │ └── Spinner.jsx ├── features │ └── auth │ │ ├── authService.js │ │ └── authSlice.js ├── index.css ├── index.js ├── routes │ ├── AuthPage.jsx │ ├── ProfilePage.jsx │ ├── ProtectedRoute.jsx │ ├── PublicPage.jsx │ └── Redirect.jsx ├── store.js └── utils │ └── config.js ├── tailwind.config.js └── yarn.lock /.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 | # PocketBase oAuth with React 2 | ### PocketBase is an Open Source backend comprises of only one single file, written in **Go**, for more info visit: [https://pocketbase.io/](https://pocketbase.io/) 3 | 4 | #### For beginner friendly Todo app using React+PocketBase visit: [blog](https://dev.to/rajesh6161/realtime-todo-app-using-react-and-pocketbase-3mf), [repo](https://github.com/rajesh6161/pocketbaseTodo) is now part of the official PocketBase [show-and-tell](https://github.com/pocketbase/pocketbase/discussions/categories/show-and-tell) and [awesome-pocketbase](https://github.com/benallfree/awesome-pocketbase/) 5 | 6 | #### PocketBase+React Realtime Blog: [Source](https://github.com/rajesh6161/pbRealtimeBlog) || [Live](https://pbrealtimeblog.vercel.app/) 7 | 8 | ## Setup for GitHub oAuth 9 | - Go to your GitHub settings -> oAuth Apps -> New oAuth App 10 | ![image](https://user-images.githubusercontent.com/40054161/197316727-67123a3c-688e-4042-abef-c7ed4a2685e9.png) 11 | 12 | - Generate a new client secret 13 | - In your PocketBase Admin Dashboard you can setup your Auth Provider, here I am using GitHub and paste in your clientID and secret: 14 | 15 | ![image](https://user-images.githubusercontent.com/40054161/197316990-f9c67d56-36e6-41e6-a93f-ab743609d4c8.png) 16 | 17 | #### Some screenshots of the application: 18 | 19 | - Public Page 20 | 21 | ![image](https://user-images.githubusercontent.com/40054161/197317018-753dbadd-d4da-412d-b179-0118271cbc48.png) 22 | 23 | - Login Page 24 | 25 | ![image](https://user-images.githubusercontent.com/40054161/197317146-1a1b2505-0bdb-46fb-b18f-d2af082b0c44.png) 26 | 27 | - Profile Page (Protected) 28 | 29 | ![image](https://user-images.githubusercontent.com/40054161/197317034-831c93cc-c0f0-468a-b7ce-08551be82684.png) 30 | 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "realtime", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@reduxjs/toolkit": "^1.8.6", 7 | "@tailwindcss/forms": "^0.5.3", 8 | "@testing-library/jest-dom": "^5.14.1", 9 | "@testing-library/react": "^13.0.0", 10 | "@testing-library/user-event": "^13.2.1", 11 | "moment": "^2.29.4", 12 | "pocketbase": "^0.7.1", 13 | "react": "^18.2.0", 14 | "react-debounce-input": "^3.3.0", 15 | "react-dom": "^18.2.0", 16 | "react-redux": "^8.0.4", 17 | "react-router-dom": "^6.4.2", 18 | "react-scripts": "5.0.1", 19 | "react-toastify": "^9.0.8", 20 | "redux-persist": "^6.0.0", 21 | "web-vitals": "^2.1.0" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "devDependencies": { 48 | "autoprefixer": "^10.4.12", 49 | "postcss": "^8.4.17", 50 | "tailwindcss": "^3.1.8" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rajesh6161/pocketbase-oauth-demo/655ff8186326f6d3e3613c583a110fa3a18d842a/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | PocketBase oAuth2.0 Demo 17 | 18 | 19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rajesh6161/pocketbase-oauth-demo/655ff8186326f6d3e3613c583a110fa3a18d842a/public/logo192.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 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { ToastContainer } from 'react-toastify'; 2 | import 'react-toastify/dist/ReactToastify.css'; 3 | import { client } from './utils/config'; 4 | 5 | import PublicPage from './routes/PublicPage'; 6 | import { Route, Routes } from 'react-router-dom'; 7 | import AuthPage from './routes/AuthPage'; 8 | import ProtectedRoute from './routes/ProtectedRoute'; 9 | import ProfilePage from './routes/ProfilePage'; 10 | import Redirect from './routes/Redirect'; 11 | import { useDispatch, useSelector } from 'react-redux'; 12 | import { useEffect } from 'react'; 13 | import { setUserLoggedIn } from './features/auth/authSlice'; 14 | 15 | function App() { 16 | const dispatch = useDispatch(); 17 | const { loading } = useSelector((state) => state.auth); 18 | // useEffect(() => { 19 | // client.realtime.subscribe('', function (e) {}); 20 | // return () => { 21 | // client.realtime.unsubscribe(); 22 | // }; 23 | // }); 24 | 25 | useEffect(() => { 26 | dispatch(setUserLoggedIn()); 27 | }, [loading]); 28 | 29 | return ( 30 |
31 | 32 | } /> 33 | } /> 34 | 38 | 39 | 40 | } 41 | /> 42 | } /> 43 | Not Found
} /> 44 | 45 | 46 | ); 47 | } 48 | 49 | export default App; 50 | -------------------------------------------------------------------------------- /src/components/ErrorPage.jsx: -------------------------------------------------------------------------------- 1 | import { useRouteError } from 'react-router-dom'; 2 | 3 | export default function ErrorPage() { 4 | const error = useRouteError(); 5 | console.error(error); 6 | 7 | return ( 8 |
9 |

Oops!

10 |

Sorry, an unexpected error has occurred.

11 |

12 | {error.statusText || error.message} 13 |

14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Spinner.jsx: -------------------------------------------------------------------------------- 1 | // create a spinner in react using tailwind animation and fontawesome 2 | 3 | import React from 'react'; 4 | 5 | const Spinner = () => { 6 | return ( 7 |
8 |
9 |
10 | ); 11 | }; 12 | 13 | export default Spinner; 14 | -------------------------------------------------------------------------------- /src/features/auth/authService.js: -------------------------------------------------------------------------------- 1 | import { client } from '../../utils/config'; 2 | 3 | const logout = () => { 4 | client.authStore.clear(); 5 | }; 6 | 7 | const oAuthMethods = async () => { 8 | const authMethods = await client.users.listAuthMethods(); 9 | const listItems = []; 10 | for (const provider of authMethods.authProviders) { 11 | listItems.push(provider); 12 | } 13 | return listItems; 14 | }; 15 | 16 | const oAuthLogin = async (provider) => { 17 | try { 18 | const res = await client.users.authViaOAuth2( 19 | provider.name, 20 | provider.code, 21 | provider.codeVerifier, 22 | provider.redirectUrl 23 | ); 24 | 25 | return res; 26 | } catch (err) { 27 | return err; 28 | } 29 | }; 30 | 31 | const authService = { 32 | logout, 33 | oAuthMethods, 34 | oAuthLogin, 35 | }; 36 | 37 | export default authService; 38 | -------------------------------------------------------------------------------- /src/features/auth/authSlice.js: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; 2 | import authService from './authService'; 3 | import { toast } from 'react-toastify'; 4 | 5 | const initialState = { 6 | user: null, 7 | loading: false, 8 | error: null, 9 | loggedIn: false, 10 | authProviders: [], 11 | }; 12 | 13 | export const getoAuthProviders = createAsyncThunk( 14 | 'auth/oAuthProviders', 15 | async (_, { rejectWithValue }) => { 16 | try { 17 | const authProviders = await authService.oAuthMethods(); 18 | return authProviders; 19 | } catch (err) { 20 | console.log(err); 21 | return rejectWithValue(err); 22 | } 23 | } 24 | ); 25 | 26 | const authSlice = createSlice({ 27 | name: 'auth', 28 | initialState, 29 | reducers: { 30 | logout: (state) => { 31 | authService.logout(); 32 | localStorage.removeItem('userData'); 33 | state.user = null; 34 | state.loggedIn = false; 35 | }, 36 | setUserLoggedIn: (state) => { 37 | let pocketbase_auth = localStorage.getItem('pocketbase_auth'); 38 | pocketbase_auth = JSON.parse(pocketbase_auth); 39 | if (pocketbase_auth?.token?.length > 0) { 40 | state.loggedIn = true; 41 | } 42 | let user = 43 | localStorage.getItem('userData') !== null 44 | ? JSON.parse(localStorage.getItem('userData')) 45 | : null; 46 | state.user = user; 47 | }, 48 | }, 49 | extraReducers: { 50 | [getoAuthProviders.pending]: (state) => { 51 | state.loading = true; 52 | }, 53 | [getoAuthProviders.fulfilled]: (state, action) => { 54 | state.loading = false; 55 | state.authProviders = action.payload; 56 | }, 57 | [getoAuthProviders.rejected]: (state, action) => { 58 | state.loading = false; 59 | state.error = action.payload; 60 | }, 61 | }, 62 | }); 63 | 64 | export const { logout, setUserLoggedIn } = authSlice.actions; 65 | 66 | export default authSlice.reducer; 67 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import { Provider } from 'react-redux'; 6 | import { store } from './store'; 7 | import { BrowserRouter } from 'react-router-dom'; 8 | import ErrorPage from './components/ErrorPage'; 9 | import AuthPage from './routes/AuthPage'; 10 | import ProfilePage from './routes/ProfilePage'; 11 | 12 | // const router = createBrowserRouter([ 13 | // { 14 | // path: '/', 15 | // element: , 16 | // errorElement: , 17 | // }, 18 | // { 19 | // path: '/login', 20 | // element: , 21 | // }, 22 | // { 23 | // path: '/profile', 24 | // element: , 25 | // }, 26 | // ]); 27 | 28 | const root = ReactDOM.createRoot(document.getElementById('root')); 29 | root.render( 30 | 31 | 32 | {/* */} 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | -------------------------------------------------------------------------------- /src/routes/AuthPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { getoAuthProviders, setUserLoggedIn } from '../features/auth/authSlice'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import Spinner from '../components/Spinner'; 5 | import { Link, Navigate } from 'react-router-dom'; 6 | 7 | const AuthPage = () => { 8 | const dispatch = useDispatch(); 9 | const { loggedIn, authProviders, loading } = useSelector( 10 | (state) => state.auth 11 | ); 12 | 13 | useEffect(() => { 14 | dispatch(getoAuthProviders()); 15 | }, []); 16 | 17 | useEffect(() => { 18 | dispatch(setUserLoggedIn()); 19 | }, [loading]); 20 | 21 | if (loggedIn) { 22 | return ; 23 | } 24 | 25 | const oAuthHandler = (provider) => { 26 | localStorage.setItem('provider', JSON.stringify(provider)); 27 | }; 28 | 29 | const redirectUrl = 'http://localhost:3000/redirect'; 30 | return ( 31 |
32 | {loading ? ( 33 |

Authenticating...

34 | ) : ( 35 |
36 | <> 37 | Logo 42 |

43 | Login to your account 44 |

45 | 46 |

Back to root "/"

47 | 48 | 49 |
50 |

Available Providers:

51 | {authProviders?.length > 0 && 52 | authProviders.map((provider) => ( 53 | oAuthHandler(provider)} 58 | > 59 | Login via {provider.name}{' '} 60 | 61 | ))} 62 | 63 | {authProviders?.length === 0 && ( 64 |

No login methods available

65 | )} 66 |
67 |
68 | )} 69 |
70 | ); 71 | }; 72 | 73 | export default AuthPage; 74 | -------------------------------------------------------------------------------- /src/routes/ProfilePage.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { Link } from 'react-router-dom'; 4 | import { logout } from '../features/auth/authSlice'; 5 | 6 | const ProfilePage = () => { 7 | const dispatch = useDispatch(); 8 | const { user } = useSelector((state) => state.auth); 9 | 10 | return ( 11 |
12 | Avatar 17 |
18 |

19 | Welcome, {user?.meta?.name} 20 |

21 |

Email: {user?.meta?.email}

22 | 23 |

Back to root "/"

24 | 25 | 31 |
32 |
33 | ); 34 | }; 35 | 36 | export default ProfilePage; 37 | -------------------------------------------------------------------------------- /src/routes/ProtectedRoute.jsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | import { Navigate } from 'react-router-dom'; 3 | 4 | const ProtectedRoute = ({ children }) => { 5 | const { loggedIn } = useSelector((state) => state.auth); 6 | 7 | if (!loggedIn) { 8 | return ; 9 | } 10 | 11 | return children; 12 | }; 13 | 14 | export default ProtectedRoute; 15 | -------------------------------------------------------------------------------- /src/routes/PublicPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | const PublicPage = () => { 6 | const { loggedIn } = useSelector((state) => state.auth); 7 | 8 | return ( 9 |
10 |

PocketBase oAuth Demo

11 |

12 | Currently:{' '} 13 | 18 | {loggedIn ? 'Logged In 🎉' : 'Not logged in 😔'} 19 | 20 |

21 | 22 | currently you are on "/" viz., a public page and if you try to access 23 | profile page you will be redirected to login page if you aren't logged 24 | in, as it is a protected route. 25 | 26 | 27 | 28 | Go to Profile "/profile" 29 | 30 |
31 | 49 |
50 | ); 51 | }; 52 | 53 | export default PublicPage; 54 | -------------------------------------------------------------------------------- /src/routes/Redirect.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { useNavigate } from 'react-router-dom'; 4 | 5 | import authService from '../features/auth/authService'; 6 | import { oAuthLogin, setUserLoggedIn } from '../features/auth/authSlice'; 7 | 8 | const Redirect = () => { 9 | let navigate = useNavigate(); 10 | window.addEventListener('load', function () { 11 | const provider = JSON.parse(localStorage.getItem('provider')); 12 | const url = new URL(window.location.href); 13 | const code = url.searchParams.get('code'); 14 | const state = url.searchParams.get('state'); 15 | 16 | if (code && state && state === provider.state) { 17 | document.getElementById('content').innerText = 18 | 'Authentication successful! You can close this window now.'; 19 | } else { 20 | document.getElementById('content').innerText = 21 | 'Authentication failed! You can close this window now.'; 22 | } 23 | }); 24 | const redirectUrl = 'http://localhost:3000/redirect'; 25 | const params = new URL(window.location).searchParams; 26 | const provider = JSON.parse(localStorage.getItem('provider')); 27 | if (provider.state !== params.get('state')) { 28 | throw "State parameters don't match."; 29 | } 30 | provider.redirectUrl = redirectUrl; 31 | provider.code = params.get('code'); 32 | 33 | const dispatch = useDispatch(); 34 | useEffect(() => { 35 | authService 36 | .oAuthLogin(provider) 37 | .then((data) => { 38 | if (data) { 39 | localStorage.setItem('userData', JSON.stringify(data)); 40 | return navigate('/profile'); 41 | } 42 | }) 43 | .catch((err) => { 44 | dispatch(setUserLoggedIn(err)); 45 | console.log(err); 46 | }); 47 | }, []); 48 | 49 | return
Authenticating...
; 50 | }; 51 | 52 | export default Redirect; 53 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import authReducer from './features/auth/authSlice'; 3 | 4 | export const store = configureStore({ 5 | middleware: (getDefaultMiddleware) => { 6 | return getDefaultMiddleware({ 7 | serializableCheck: false, 8 | }); 9 | }, 10 | reducer: { 11 | auth: authReducer, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/utils/config.js: -------------------------------------------------------------------------------- 1 | import Pocketbase from 'pocketbase'; 2 | 3 | const url = ' http://127.0.0.1:8090'; 4 | const client = new Pocketbase(url); 5 | 6 | export { client }; 7 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./src/**/*.{js,jsx,ts,tsx}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [require('@tailwindcss/forms')], 8 | }; 9 | --------------------------------------------------------------------------------