From 90eaa013dc021289523e71669b55364f9e315cda Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Tue, 5 Nov 2024 12:10:32 +0530 Subject: [PATCH] [docs] Add `SignInPage` Vite + React Router example (#4335) --- .../components/sign-in-page/sign-in-page.md | 4 + .../toolpad/core/introduction/integration.md | 224 ++++++++++++++++++ .../components/ExamplesGrid/core-examples.ts | 7 + examples/core/auth-vite/.gitignore | 24 ++ examples/core/auth-vite/README.md | 8 + examples/core/auth-vite/index.html | 20 ++ examples/core/auth-vite/package.json | 27 +++ examples/core/auth-vite/public/vite.svg | 1 + examples/core/auth-vite/src/App.tsx | 56 +++++ examples/core/auth-vite/src/SessionContext.ts | 16 ++ examples/core/auth-vite/src/assets/.gitkeep | 0 .../core/auth-vite/src/layouts/dashboard.tsx | 25 ++ examples/core/auth-vite/src/main.tsx | 40 ++++ examples/core/auth-vite/src/pages/index.tsx | 6 + examples/core/auth-vite/src/pages/orders.tsx | 6 + examples/core/auth-vite/src/pages/signIn.tsx | 47 ++++ examples/core/auth-vite/src/vite-env.d.ts | 1 + examples/core/auth-vite/tsconfig.json | 20 ++ examples/core/auth-vite/vite.config.ts | 7 + pnpm-lock.yaml | 2 +- 20 files changed, 540 insertions(+), 1 deletion(-) create mode 100644 examples/core/auth-vite/.gitignore create mode 100644 examples/core/auth-vite/README.md create mode 100644 examples/core/auth-vite/index.html create mode 100644 examples/core/auth-vite/package.json create mode 100644 examples/core/auth-vite/public/vite.svg create mode 100644 examples/core/auth-vite/src/App.tsx create mode 100644 examples/core/auth-vite/src/SessionContext.ts create mode 100644 examples/core/auth-vite/src/assets/.gitkeep create mode 100644 examples/core/auth-vite/src/layouts/dashboard.tsx create mode 100644 examples/core/auth-vite/src/main.tsx create mode 100644 examples/core/auth-vite/src/pages/index.tsx create mode 100644 examples/core/auth-vite/src/pages/orders.tsx create mode 100644 examples/core/auth-vite/src/pages/signIn.tsx create mode 100644 examples/core/auth-vite/src/vite-env.d.ts create mode 100644 examples/core/auth-vite/tsconfig.json create mode 100644 examples/core/auth-vite/vite.config.ts diff --git a/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md b/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md index e7504d9b519..bc7097a9f28 100644 --- a/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md +++ b/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md @@ -213,6 +213,10 @@ If you're using the default [Next.js example](https://github.com/mui/toolpad/tre If you're not on the Next Auth v5 version yet, see the [example with Next Auth v4](https://github.com/mui/toolpad/tree/master/examples/core/auth-nextjs-pages-nextauth-4/) to get started. ::: +:::info +If you're using Vite with React Router, check out the [example with Vite and React Router](https://github.com/mui/toolpad/tree/master/examples/core/auth-vite/) that showcases how to use `SignInPage` along with any external authentication library of your choice. +::: + ## Customization ### Branding diff --git a/docs/data/toolpad/core/introduction/integration.md b/docs/data/toolpad/core/introduction/integration.md index 3123e14fec5..01569a2b7dd 100644 --- a/docs/data/toolpad/core/introduction/integration.md +++ b/docs/data/toolpad/core/introduction/integration.md @@ -836,3 +836,227 @@ That's it! You now have Toolpad Core integrated into your single-page app with R :::info For a full working example, see the [Toolpad Core Vite app with React Router example](https://github.com/mui/toolpad/tree/master/examples/core/vite/) ::: + +### 4. (Optional) Set up authentication + +You can use the `SignInPage` component to add authentication along with an external authentication provider of your choice. The following code demonstrates the code required to set up authentication with a mock provider. + +#### a. Define a `SessionContext` to act as the mock authentication provider + +```tsx title="src/SessionContext.ts" +import * as React from 'react'; +import type { Session } from '@toolpad/core'; + +export interface SessionContextValue { + session: Session | null; + setSession: (session: Session | null) => void; +} + +export const SessionContext = React.createContext({ + session: {}, + setSession: () => {}, +}); + +export function useSession() { + return React.useContext(SessionContext); +} +``` + +### b. Add the mock authentication and session data to the `AppProvider` + +```tsx title="src/App.tsx" +import * as React from 'react'; +import DashboardIcon from '@mui/icons-material/Dashboard'; +import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; +import { AppProvider } from '@toolpad/core/react-router-dom'; +import { Outlet, useNavigate } from 'react-router-dom'; +import type { Navigation, Session } from '@toolpad/core'; +import { SessionContext } from './SessionContext'; + +const NAVIGATION: Navigation = [ + { + kind: 'header', + title: 'Main items', + }, + { + title: 'Dashboard', + icon: , + }, + { + segment: 'orders', + title: 'Orders', + icon: , + }, +]; + +const BRANDING = { + title: 'My Toolpad Core App', +}; + +export default function App() { + const [session, setSession] = React.useState(null); + const navigate = useNavigate(); + + const signIn = React.useCallback(() => { + navigate('/sign-in'); + }, [navigate]); + + const signOut = React.useCallback(() => { + setSession(null); + navigate('/sign-in'); + }, [navigate]); + + const sessionContextValue = React.useMemo( + () => ({ session, setSession }), + [session, setSession], + ); + + return ( + + + + + + ); +} +``` + +#### c. Protect routes inside the dashboard layout + +```tsx title="src/layouts/dashboard.tsx" +import * as React from 'react'; +import { Outlet, Navigate, useLocation } from 'react-router-dom'; +import { DashboardLayout } from '@toolpad/core/DashboardLayout'; +import { PageContainer } from '@toolpad/core/PageContainer'; +import { useSession } from '../SessionContext'; + +export default function Layout() { + const { session } = useSession(); + const location = useLocation(); + + if (!session) { + // Add the `callbackUrl` search parameter + const redirectTo = `/sign-in?callbackUrl=${encodeURIComponent(location.pathname)}`; + + return ; + } + + return ( + + + + + + ); +} +``` + +You can protect any page or groups of pages through this mechanism. + +#### d. Use the `SignInPage` component to create a sign-in page + +```tsx title="src/pages/signIn.tsx" +'use client'; +import * as React from 'react'; +import { SignInPage } from '@toolpad/core/SignInPage'; +import type { Session } from '@toolpad/core/AppProvider'; +import { useNavigate } from 'react-router-dom'; +import { useSession } from '../SessionContext'; + +const fakeAsyncGetSession = async (formData: any): Promise => { + return new Promise((resolve, reject) => { + setTimeout(() => { + if (formData.get('password') === 'password') { + resolve({ + user: { + name: 'Bharat Kashyap', + email: formData.get('email') || '', + image: 'https://avatars.githubusercontent.com/u/19550456', + }, + }); + } + reject(new Error('Incorrect credentials.')); + }, 1000); + }); +}; + +export default function SignIn() { + const { setSession } = useSession(); + const navigate = useNavigate(); + return ( + { + // Demo session + try { + const session = await fakeAsyncGetSession(formData); + if (session) { + setSession(session); + navigate(callbackUrl || '/', { replace: true }); + return {}; + } + } catch (error) { + return { + error: error instanceof Error ? error.message : 'An error occurred', + }; + } + return {}; + }} + /> + ); +} +``` + +#### e. Add the sign in page to the router + +```tsx title="src/main.tsx" +import * as React from 'react'; +import * as ReactDOM from 'react-dom/client'; +import { createBrowserRouter, RouterProvider } from 'react-router-dom'; +import App from './App'; +import Layout from './layouts/dashboard'; +import DashboardPage from './pages'; +import OrdersPage from './pages/orders'; +import SignInPage from './pages/signIn'; + +const router = createBrowserRouter([ + { + Component: App, + children: [ + { + path: '/', + Component: Layout, + children: [ + { + path: '/', + Component: DashboardPage, + }, + { + path: '/orders', + Component: OrdersPage, + }, + ], + }, + { + path: '/sign-in', + Component: SignInPage, + }, + ], + }, +]); + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); +``` + +:::info +For a full working example, see the [Toolpad Core Vite app with React Router and authentication example](https://github.com/mui/toolpad/tree/master/examples/core/auth-vite/) +::: diff --git a/docs/src/modules/components/ExamplesGrid/core-examples.ts b/docs/src/modules/components/ExamplesGrid/core-examples.ts index 6385a2ec6e2..d855bca816d 100644 --- a/docs/src/modules/components/ExamplesGrid/core-examples.ts +++ b/docs/src/modules/components/ExamplesGrid/core-examples.ts @@ -78,5 +78,12 @@ export default function examples() { stackblitz: 'https://stackblitz.com/github/mui/toolpad/tree/master/examples/core/auth-nextjs-passkey', }, + { + title: 'Vite with React Router and authentication', + description: + 'This app shows you to how to get started using Toolpad Core with Vite, React Router and any external authentication provider', + src: '/static/toolpad/docs/core/vite-react-router.png', + source: 'https://github.com/mui/toolpad/tree/master/examples/core/auth-vite', + }, ]; } diff --git a/examples/core/auth-vite/.gitignore b/examples/core/auth-vite/.gitignore new file mode 100644 index 00000000000..a547bf36d8d --- /dev/null +++ b/examples/core/auth-vite/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/core/auth-vite/README.md b/examples/core/auth-vite/README.md new file mode 100644 index 00000000000..77643b462dd --- /dev/null +++ b/examples/core/auth-vite/README.md @@ -0,0 +1,8 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh diff --git a/examples/core/auth-vite/index.html b/examples/core/auth-vite/index.html new file mode 100644 index 00000000000..fc6eb70c9c8 --- /dev/null +++ b/examples/core/auth-vite/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + + Toolpad Core Vite with Auth + + +
+ + + diff --git a/examples/core/auth-vite/package.json b/examples/core/auth-vite/package.json new file mode 100644 index 00000000000..be9ab632f81 --- /dev/null +++ b/examples/core/auth-vite/package.json @@ -0,0 +1,27 @@ +{ + "name": "core-vite-auth", + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@emotion/react": "^11", + "@emotion/styled": "^11", + "@mui/icons-material": "^6", + "@mui/material": "^6", + "@toolpad/core": "workspace:*", + "react": "^18", + "react-dom": "^18", + "react-router-dom": "^6" + }, + "devDependencies": { + "@types/react": "^18", + "@types/react-dom": "^18", + "@vitejs/plugin-react": "^4.3.2", + "typescript": "^5", + "vite": "^5.4.8" + } +} diff --git a/examples/core/auth-vite/public/vite.svg b/examples/core/auth-vite/public/vite.svg new file mode 100644 index 00000000000..e7b8dfb1b2a --- /dev/null +++ b/examples/core/auth-vite/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/core/auth-vite/src/App.tsx b/examples/core/auth-vite/src/App.tsx new file mode 100644 index 00000000000..075aa3c62c1 --- /dev/null +++ b/examples/core/auth-vite/src/App.tsx @@ -0,0 +1,56 @@ +import * as React from 'react'; +import DashboardIcon from '@mui/icons-material/Dashboard'; +import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; +import { AppProvider } from '@toolpad/core/react-router-dom'; +import { Outlet, useNavigate } from 'react-router-dom'; +import type { Navigation, Session } from '@toolpad/core'; +import { SessionContext } from './SessionContext'; + +const NAVIGATION: Navigation = [ + { + kind: 'header', + title: 'Main items', + }, + { + title: 'Dashboard', + icon: , + }, + { + segment: 'orders', + title: 'Orders', + icon: , + }, +]; + +const BRANDING = { + title: 'My Toolpad Core App', +}; + +export default function App() { + const [session, setSession] = React.useState(null); + const navigate = useNavigate(); + + const signIn = React.useCallback(() => { + navigate('/sign-in'); + }, [navigate]); + + const signOut = React.useCallback(() => { + setSession(null); + navigate('/sign-in'); + }, [navigate]); + + const sessionContextValue = React.useMemo(() => ({ session, setSession }), [session, setSession]); + + return ( + + + + + + ); +} diff --git a/examples/core/auth-vite/src/SessionContext.ts b/examples/core/auth-vite/src/SessionContext.ts new file mode 100644 index 00000000000..6dff1dcae9a --- /dev/null +++ b/examples/core/auth-vite/src/SessionContext.ts @@ -0,0 +1,16 @@ +import * as React from 'react'; +import type { Session } from '@toolpad/core'; + +export interface SessionContextValue { + session: Session | null; + setSession: (session: Session | null) => void; +} + +export const SessionContext = React.createContext({ + session: {}, + setSession: () => {}, +}); + +export function useSession() { + return React.useContext(SessionContext); +} diff --git a/examples/core/auth-vite/src/assets/.gitkeep b/examples/core/auth-vite/src/assets/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/core/auth-vite/src/layouts/dashboard.tsx b/examples/core/auth-vite/src/layouts/dashboard.tsx new file mode 100644 index 00000000000..39b681178b6 --- /dev/null +++ b/examples/core/auth-vite/src/layouts/dashboard.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { Outlet, Navigate, useLocation } from 'react-router-dom'; +import { DashboardLayout } from '@toolpad/core/DashboardLayout'; +import { PageContainer } from '@toolpad/core/PageContainer'; +import { useSession } from '../SessionContext'; + +export default function Layout() { + const { session } = useSession(); + const location = useLocation(); + + if (!session) { + // Add the `callbackUrl` search parameter + const redirectTo = `/sign-in?callbackUrl=${encodeURIComponent(location.pathname)}`; + + return ; + } + + return ( + + + + + + ); +} diff --git a/examples/core/auth-vite/src/main.tsx b/examples/core/auth-vite/src/main.tsx new file mode 100644 index 00000000000..ceee289fe2c --- /dev/null +++ b/examples/core/auth-vite/src/main.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom/client'; +import { createBrowserRouter, RouterProvider } from 'react-router-dom'; +import App from './App'; +import Layout from './layouts/dashboard'; +import DashboardPage from './pages'; +import OrdersPage from './pages/orders'; +import SignInPage from './pages/signIn'; + +const router = createBrowserRouter([ + { + Component: App, + children: [ + { + path: '/', + Component: Layout, + children: [ + { + path: '/', + Component: DashboardPage, + }, + { + path: '/orders', + Component: OrdersPage, + }, + ], + }, + { + path: '/sign-in', + Component: SignInPage, + }, + ], + }, +]); + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/examples/core/auth-vite/src/pages/index.tsx b/examples/core/auth-vite/src/pages/index.tsx new file mode 100644 index 00000000000..e4581fc26bf --- /dev/null +++ b/examples/core/auth-vite/src/pages/index.tsx @@ -0,0 +1,6 @@ +import * as React from 'react'; +import Typography from '@mui/material/Typography'; + +export default function DashboardPage() { + return Welcome to Toolpad!; +} diff --git a/examples/core/auth-vite/src/pages/orders.tsx b/examples/core/auth-vite/src/pages/orders.tsx new file mode 100644 index 00000000000..de4948afd88 --- /dev/null +++ b/examples/core/auth-vite/src/pages/orders.tsx @@ -0,0 +1,6 @@ +import * as React from 'react'; +import Typography from '@mui/material/Typography'; + +export default function OrdersPage() { + return Welcome to the Toolpad orders!; +} diff --git a/examples/core/auth-vite/src/pages/signIn.tsx b/examples/core/auth-vite/src/pages/signIn.tsx new file mode 100644 index 00000000000..f01c9e9993a --- /dev/null +++ b/examples/core/auth-vite/src/pages/signIn.tsx @@ -0,0 +1,47 @@ +'use client'; +import * as React from 'react'; +import { SignInPage } from '@toolpad/core/SignInPage'; +import type { Session } from '@toolpad/core/AppProvider'; +import { useNavigate } from 'react-router-dom'; +import { useSession } from '../SessionContext'; + +const fakeAsyncGetSession = async (formData: any): Promise => { + return new Promise((resolve, reject) => { + setTimeout(() => { + if (formData.get('password') === 'password') { + resolve({ + user: { + name: 'Bharat Kashyap', + email: formData.get('email') || '', + image: 'https://avatars.githubusercontent.com/u/19550456', + }, + }); + } + reject(new Error('Incorrect credentials.')); + }, 1000); + }); +}; + +export default function SignIn() { + const { setSession } = useSession(); + const navigate = useNavigate(); + return ( + { + // Demo session + try { + const session = await fakeAsyncGetSession(formData); + if (session) { + setSession(session); + navigate(callbackUrl || '/', { replace: true }); + return {}; + } + } catch (error) { + return { error: error instanceof Error ? error.message : 'An error occurred' }; + } + return {}; + }} + /> + ); +} diff --git a/examples/core/auth-vite/src/vite-env.d.ts b/examples/core/auth-vite/src/vite-env.d.ts new file mode 100644 index 00000000000..11f02fe2a00 --- /dev/null +++ b/examples/core/auth-vite/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/core/auth-vite/tsconfig.json b/examples/core/auth-vite/tsconfig.json new file mode 100644 index 00000000000..251a83f8a97 --- /dev/null +++ b/examples/core/auth-vite/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"] +} diff --git a/examples/core/auth-vite/vite.config.ts b/examples/core/auth-vite/vite.config.ts new file mode 100644 index 00000000000..627a3196243 --- /dev/null +++ b/examples/core/auth-vite/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e16db6c868b..486ac09e6f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2060,7 +2060,7 @@ packages: resolution: {integrity: sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': ^7.25.8 '@babel/register@7.25.9': resolution: {integrity: sha512-8D43jXtGsYmEeDvm4MWHYUpWf8iiXgWYx3fW7E7Wb7Oe6FWqJPl5K6TuFW0dOwNZzEE5rjlaSJYH9JjrUKJszA==}