├── .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 | 
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 | 
16 |
17 | #### Some screenshots of the application:
18 |
19 | - Public Page
20 |
21 | 
22 |
23 | - Login Page
24 |
25 | 
26 |
27 | - Profile Page (Protected)
28 |
29 | 
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 | You need to enable JavaScript to run this app.
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 |
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 |
42 |
43 | Login to your account
44 |
45 |
46 |
Back to root "/"
47 |
48 | >
49 |
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 |
17 |
18 |
19 | Welcome, {user?.meta?.name}
20 |
21 |
Email: {user?.meta?.email}
22 |
23 |
Back to root "/"
24 |
25 |
dispatch(logout())}
28 | >
29 | Logout
30 |
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 |
--------------------------------------------------------------------------------