-
Notifications
You must be signed in to change notification settings - Fork 152
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(examples): add React Router (a.k.a. Remix) (#16)
* initial scaffold from `create-react-router` * remove docker things * remove default css & tailwind * delete default files from CRR template * implement auth * update README * delete default css * format code
- Loading branch information
1 parent
249c5cd
commit ff5d7b5
Showing
15 changed files
with
306 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
.DS_Store | ||
/node_modules/ | ||
|
||
# React Router | ||
/.react-router/ | ||
/build/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# OpenAuth React Router Client | ||
|
||
The files to note are | ||
- `app/auth.ts` - creates the client that is used to interact with the auth server, along with code that runs to verify access tokens, refresh them if out of date, and redirect the user to the auth server if they are not logged in | ||
- `app/routes/auth/callback.ts` - the callback endpoint that receives the auth code and exchanges it for an access/refresh token |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import { createClient } from "@openauthjs/openauth"; | ||
import { createCookie } from "react-router"; | ||
import { subjects } from "../../../subjects"; | ||
|
||
export { subjects } | ||
|
||
export const client = createClient({ | ||
clientID: "react-router", | ||
issuer: "http://localhost:3000", | ||
}); | ||
|
||
const refreshTokenCookie = createCookie("refresh_token", { | ||
httpOnly: true, | ||
sameSite: "strict", | ||
path: "/", | ||
maxAge: 34_560_000, | ||
}) | ||
|
||
const accessTokenCookie = createCookie("access_token", { | ||
httpOnly: true, | ||
sameSite: "strict", | ||
path: "/", | ||
maxAge: 34_560_000, | ||
}) | ||
|
||
export async function setTokens(access: string, refresh: string, headers?: Headers) { | ||
headers ??= new Headers() | ||
headers.append('Set-Cookie', await refreshTokenCookie.serialize(refresh)) | ||
headers.append('Set-Cookie', await accessTokenCookie.serialize(access)) | ||
return headers | ||
} | ||
|
||
export async function clearTokens(headers?: Headers) { | ||
headers ??= new Headers() | ||
headers.append('Set-Cookie', await refreshTokenCookie.serialize("", { maxAge: 0 })) | ||
headers.append('Set-Cookie', await accessTokenCookie.serialize("", { maxAge: 0 })) | ||
return headers | ||
} | ||
|
||
export async function login(request: Request) { | ||
const url = new URL(request.url); | ||
return Response.redirect( | ||
client.authorize(url.origin + "/callback", "code"), | ||
302 | ||
); | ||
} | ||
|
||
export async function logout() { | ||
const headers = await clearTokens() | ||
headers.set('Location', '/') | ||
return new Response("/", { | ||
status: 302, | ||
headers | ||
}) | ||
} | ||
|
||
/** | ||
* Checks if the user is authenticated. | ||
* If so, returns the subject along with updated tokens, otherwise `null`. | ||
*/ | ||
export async function tryAuth(request: Request) { | ||
const cookieHeader = request.headers.get("Cookie"); | ||
try { | ||
const accessToken = await accessTokenCookie.parse(cookieHeader); | ||
if (!accessToken) return null | ||
const refreshToken = await refreshTokenCookie.parse(cookieHeader); | ||
const verified = await client.verify(subjects, accessToken, { | ||
refresh: refreshToken, | ||
}); | ||
const headers = new Headers(); | ||
if (verified.tokens) { | ||
setTokens(verified.tokens.access, verified.tokens.refresh, headers); | ||
} | ||
return { | ||
headers, | ||
subject: verified.subject | ||
}; | ||
} catch { | ||
return null | ||
}; | ||
} | ||
|
||
/** | ||
* Requires the user to be authenticated. | ||
* If so, returns the subject along with updated tokens, otherwise throws a redirect to login. | ||
*/ | ||
export async function requireAuth(request: Request) { | ||
const auth = await tryAuth(request) | ||
if (!auth) { | ||
throw login(request) | ||
} | ||
return auth | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { | ||
isRouteErrorResponse, | ||
Links, | ||
Meta, | ||
Outlet, | ||
Scripts, | ||
ScrollRestoration, | ||
} from "react-router"; | ||
import type { Route } from "./+types/root"; | ||
|
||
export function Layout({ children }: { children: React.ReactNode }) { | ||
return ( | ||
<html lang="en"> | ||
<head> | ||
<meta charSet="utf-8" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
<Meta /> | ||
<Links /> | ||
</head> | ||
<body> | ||
{children} | ||
<ScrollRestoration /> | ||
<Scripts /> | ||
</body> | ||
</html> | ||
); | ||
} | ||
|
||
export default function App() { | ||
return <Outlet />; | ||
} | ||
|
||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { | ||
let message = "Oops!"; | ||
let details = "An unexpected error occurred."; | ||
let stack: string | undefined; | ||
|
||
if (isRouteErrorResponse(error)) { | ||
message = error.status === 404 ? "404" : "Error"; | ||
details = | ||
error.status === 404 | ||
? "The requested page could not be found." | ||
: error.statusText || details; | ||
} else if (import.meta.env.DEV && error && error instanceof Error) { | ||
details = error.message; | ||
stack = error.stack; | ||
} | ||
|
||
return ( | ||
<main> | ||
<h1>{message}</h1> | ||
<p>{details}</p> | ||
{stack && ( | ||
<pre> | ||
<code>{stack}</code> | ||
</pre> | ||
)} | ||
</main> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { type RouteConfig, index, route } from "@react-router/dev/routes"; | ||
|
||
export default [ | ||
index("routes/home.tsx"), | ||
route('callback', 'routes/auth/callback.ts'), | ||
route('login', 'routes/auth/login.ts'), | ||
route('logout', 'routes/auth/logout.ts') | ||
] satisfies RouteConfig; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { client, setTokens } from "../../auth"; | ||
import type { Route } from "./+types/callback"; | ||
|
||
export async function loader({ request }: Route.LoaderArgs) { | ||
const url = new URL(request.url) | ||
const code = url.searchParams.get("code") | ||
try { | ||
const tokens = await client.exchange(code!, url.origin + "/callback"); | ||
const headers = await setTokens(tokens.access, tokens.refresh); | ||
headers.append("Location", "/"); | ||
return new Response(null, { | ||
headers, | ||
status: 302, | ||
}) | ||
} catch (e) { | ||
return Response.json(e, { | ||
status: 400, | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { login } from "~/auth"; | ||
import type { Route } from "./+types/login"; | ||
|
||
export function loader({ request }: Route.LoaderArgs) { | ||
return login(request) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { logout } from "~/auth"; | ||
|
||
export function action() { | ||
return logout() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { tryAuth } from "~/auth"; | ||
import type { Route } from "./+types/home"; | ||
import { data, Form, Link } from "react-router"; | ||
|
||
export async function loader({ request }: Route.LoaderArgs) { | ||
const auth = await tryAuth(request) | ||
if (!auth) { | ||
return { user: null } | ||
} | ||
return data({ user: auth.subject }, { headers: auth.headers }) | ||
} | ||
|
||
export default function Page({ loaderData }: Route.ComponentProps) { | ||
if (!loaderData.user) { | ||
return ( | ||
<div> | ||
<p>You are not authorized.</p> | ||
<Link to="/login">Login</Link> | ||
</div> | ||
) | ||
} | ||
|
||
return ( | ||
<div> | ||
<p>Hello {loaderData.user.properties.email}</p> | ||
<Form method="POST" action="/logout"> | ||
<button type="submit">Logout</button> | ||
</Form> | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
{ | ||
"name": "@openauthjs/example-react-router", | ||
"private": true, | ||
"type": "module", | ||
"scripts": { | ||
"build": "cross-env NODE_ENV=production react-router build", | ||
"dev": "react-router dev", | ||
"start": "cross-env NODE_ENV=production react-router-serve ./build/server/index.js", | ||
"typecheck": "react-router typegen && tsc" | ||
}, | ||
"dependencies": { | ||
"@openauthjs/openauth": "workspace:*", | ||
"@react-router/node": "^7.0.2", | ||
"@react-router/serve": "^7.0.2", | ||
"isbot": "^5.1.17", | ||
"react": "^19.0.0", | ||
"react-dom": "^19.0.0", | ||
"react-router": "^7.0.2" | ||
}, | ||
"devDependencies": { | ||
"@react-router/dev": "^7.0.2", | ||
"@types/node": "^20", | ||
"@types/react": "^19.0.1", | ||
"@types/react-dom": "^19.0.1", | ||
"cross-env": "^7.0.3", | ||
"typescript": "^5.7.2", | ||
"vite": "^5.4.11", | ||
"vite-tsconfig-paths": "^5.1.4" | ||
} | ||
} |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import type { Config } from "@react-router/dev/config"; | ||
|
||
export default { | ||
// Config options... | ||
// Server-side render by default, to enable SPA mode set this to `false` | ||
ssr: true, | ||
} satisfies Config; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
{ | ||
"include": [ | ||
"**/*", | ||
"**/.server/**/*", | ||
"**/.client/**/*", | ||
".react-router/types/**/*" | ||
], | ||
"compilerOptions": { | ||
"lib": ["DOM", "DOM.Iterable", "ES2022"], | ||
"types": ["node", "vite/client"], | ||
"target": "ES2022", | ||
"module": "ES2022", | ||
"moduleResolution": "bundler", | ||
"jsx": "react-jsx", | ||
"rootDirs": [".", "./.react-router/types"], | ||
"baseUrl": ".", | ||
"paths": { | ||
"~/*": ["./app/*"] | ||
}, | ||
"esModuleInterop": true, | ||
"verbatimModuleSyntax": true, | ||
"noEmit": true, | ||
"resolveJsonModule": true, | ||
"skipLibCheck": true, | ||
"strict": true | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { reactRouter } from "@react-router/dev/vite"; | ||
import { defineConfig } from "vite"; | ||
import tsconfigPaths from "vite-tsconfig-paths"; | ||
|
||
export default defineConfig({ | ||
plugins: [reactRouter(), tsconfigPaths()], | ||
}); |