Skip to content

Commit 95318a7

Browse files
committed
initial commit
0 parents  commit 95318a7

20 files changed

+11175
-0
lines changed

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Available in your WorkOS dashboard
2+
WORKOS_CLIENT_ID=
3+
WORKOS_API_KEY=
4+
WORKOS_REDIRECT_URI=
5+
WORKOS_COOKIE_PASSWORD=

.eslintrc.cjs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* This is intended to be a basic starting point for linting in your app.
3+
* It relies on recommended configs out of the box for simplicity, but you can
4+
* and should modify this configuration to best suit your team's needs.
5+
*/
6+
7+
/** @type {import('eslint').Linter.Config} */
8+
module.exports = {
9+
root: true,
10+
parserOptions: {
11+
ecmaVersion: 'latest',
12+
sourceType: 'module',
13+
ecmaFeatures: {
14+
jsx: true,
15+
},
16+
},
17+
env: {
18+
browser: true,
19+
commonjs: true,
20+
es6: true,
21+
},
22+
23+
// Base config
24+
extends: ['eslint:recommended'],
25+
26+
overrides: [
27+
// React
28+
{
29+
files: ['**/*.{js,jsx,ts,tsx}'],
30+
plugins: ['react', 'jsx-a11y'],
31+
extends: [
32+
'plugin:react/recommended',
33+
'plugin:react/jsx-runtime',
34+
'plugin:react-hooks/recommended',
35+
'plugin:jsx-a11y/recommended',
36+
],
37+
settings: {
38+
'react': {
39+
version: 'detect',
40+
},
41+
'formComponents': ['Form'],
42+
'linkComponents': [
43+
{ name: 'Link', linkAttribute: 'to' },
44+
{ name: 'NavLink', linkAttribute: 'to' },
45+
],
46+
'import/resolver': {
47+
typescript: {},
48+
},
49+
},
50+
},
51+
52+
// Typescript
53+
{
54+
files: ['**/*.{ts,tsx}'],
55+
plugins: ['@typescript-eslint', 'import'],
56+
parser: '@typescript-eslint/parser',
57+
settings: {
58+
'import/internal-regex': '^~/',
59+
'import/resolver': {
60+
node: {
61+
extensions: ['.ts', '.tsx'],
62+
},
63+
typescript: {
64+
alwaysTryTypes: true,
65+
},
66+
},
67+
},
68+
extends: ['plugin:@typescript-eslint/recommended', 'plugin:import/recommended', 'plugin:import/typescript'],
69+
},
70+
71+
// Node
72+
{
73+
files: ['.eslintrc.cjs'],
74+
env: {
75+
node: true,
76+
},
77+
},
78+
],
79+
};

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
node_modules
2+
3+
/.cache
4+
/build
5+
/public/build
6+
.env
7+
.react-router/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 WorkOS
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.

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# React Router integration example using AuthKit
2+
3+
An example application demonstrating how to authenticate users with AuthKit and the authkit-react-router helper library.
4+
5+
> Refer to the [User Management](https://workos.com/docs/user-management) documentation for reference.
6+
7+
## Prerequisites
8+
9+
You will need a [WorkOS account](https://dashboard.workos.com/signup).
10+
11+
## Requirements
12+
13+
Node v18 or higher
14+
15+
## Running the example
16+
17+
Rename the `.env.example` file to `.env` and supply your Client ID and API key as environment variables. The client ID and API key can be found in the [WorkOS dashboard](https://dashboard.workos.com/), and the redirect URI can also be configured there.
18+
19+
```sh
20+
WORKOS_CLIENT_ID=client_... # retrieved from the WorkOS dashboard
21+
WORKOS_API_KEY=sk_test_... # retrieved from the WorkOS dashboard
22+
WORKOS_REDIRECT_URI=http://localhost:3000/callback # configured in the WorkOS dashboard
23+
WORKOS_COOKIE_PASSWORD=<your password> # generate a secure password here
24+
```
25+
26+
`WORKOS_COOKIE_PASSWORD` is the private key used to encrypt the session cookie. It has to be at least 32 characters long. You can use the [1Password generator](https://1password.com/password-generator/) or the `openssl` library to generate a strong password via the command line:
27+
28+
```
29+
openssl rand -base64 24
30+
```
31+
32+
Install the dependencies
33+
34+
```bash
35+
npm install
36+
```
37+
38+
Run the following command and navigate to [http://localhost:3000](http://localhost:3000).
39+
40+
```bash
41+
npm run dev
42+
```

app/components/footer.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Card, Grid, Heading, Text } from '@radix-ui/themes';
2+
3+
export default function Footer() {
4+
return (
5+
<Grid columns={{ initial: '1', sm: '3' }} gap={{ initial: '3', sm: '5' }}>
6+
<Card size="4" asChild variant="classic">
7+
<a href="https://workos.com/docs" rel="noreferrer" target="_blank">
8+
<Heading size="4" mb="1">
9+
Documentation
10+
</Heading>
11+
<Text color="gray">View integration guides and SDK documentation.</Text>
12+
</a>
13+
</Card>
14+
<Card size="4" asChild variant="classic">
15+
<a href="https://workos.com/docs/reference" rel="noreferrer" target="_blank">
16+
<Heading size="4" mb="1">
17+
API Reference
18+
</Heading>
19+
<Text color="gray">Every WorkOS API method and endpoint documented.</Text>
20+
</a>
21+
</Card>
22+
<Card size="4" asChild variant="classic">
23+
<a href="https://workos.com" rel="noreferrer" target="_blank">
24+
<Heading size="4" mb="1">
25+
WorkOS
26+
</Heading>
27+
<Text color="gray">Learn more about other WorkOS products.</Text>
28+
</a>
29+
</Card>
30+
</Grid>
31+
);
32+
}

app/components/sign-in-button.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Button, Flex } from '@radix-ui/themes';
2+
import { Form } from 'react-router';
3+
import { useRootLoaderData } from '~/root';
4+
5+
export default function SignInButton({ large }: { large?: boolean }) {
6+
const rootLoaderData = useRootLoaderData();
7+
const { user, signInUrl } = rootLoaderData || {};
8+
9+
if (user) {
10+
return (
11+
<Flex gap="3">
12+
<Form method="post">
13+
<Button type="submit" size={large ? '3' : '2'}>
14+
Sign Out
15+
</Button>
16+
</Form>
17+
</Flex>
18+
);
19+
}
20+
21+
return (
22+
<Button asChild size={large ? '3' : '2'}>
23+
<a href={signInUrl}>Sign In{large && ' with AuthKit'}</a>
24+
</Button>
25+
);
26+
}

app/entry.client.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* By default, React Router will handle hydrating your app on the client for you.
3+
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx react-router reveal` ✨
4+
* For more information, see https://reactrouter.com/explanation/special-files#entryclienttsx
5+
*/
6+
7+
import { HydratedRouter } from 'react-router/dom';
8+
import { startTransition, StrictMode } from 'react';
9+
import { hydrateRoot } from 'react-dom/client';
10+
11+
startTransition(() => {
12+
hydrateRoot(
13+
document,
14+
<StrictMode>
15+
<HydratedRouter />
16+
</StrictMode>,
17+
);
18+
});

app/entry.server.tsx

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/**
2+
* By default, React Router will handle generating the HTTP Response for you.
3+
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx react-router reveal` ✨
4+
* For more information, see https://reactrouter.com/explanation/special-files#entryservertsx
5+
*/
6+
7+
import { PassThrough } from 'node:stream';
8+
9+
import type { AppLoadContext, EntryContext } from 'react-router';
10+
import { createReadableStreamFromReadable } from '@react-router/node';
11+
import { ServerRouter } from 'react-router';
12+
import { isbot } from 'isbot';
13+
import { renderToPipeableStream } from 'react-dom/server';
14+
15+
const ABORT_DELAY = 5_000;
16+
17+
export default function handleRequest(
18+
request: Request,
19+
responseStatusCode: number,
20+
responseHeaders: Headers,
21+
reactRouterContext: EntryContext,
22+
// This is ignored so we can keep it in the template for visibility. Feel
23+
// free to delete this parameter in your app if you're not using it!
24+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
25+
loadContext: AppLoadContext,
26+
) {
27+
return isbot(request.headers.get('user-agent') || '')
28+
? handleBotRequest(request, responseStatusCode, responseHeaders, reactRouterContext)
29+
: handleBrowserRequest(request, responseStatusCode, responseHeaders, reactRouterContext);
30+
}
31+
32+
function handleBotRequest(
33+
request: Request,
34+
responseStatusCode: number,
35+
responseHeaders: Headers,
36+
reactRouterContext: EntryContext,
37+
) {
38+
return new Promise((resolve, reject) => {
39+
let shellRendered = false;
40+
const { pipe, abort } = renderToPipeableStream(<ServerRouter context={reactRouterContext} url={request.url} />, {
41+
onAllReady() {
42+
shellRendered = true;
43+
const body = new PassThrough();
44+
const stream = createReadableStreamFromReadable(body);
45+
46+
responseHeaders.set('Content-Type', 'text/html');
47+
48+
resolve(
49+
new Response(stream, {
50+
headers: responseHeaders,
51+
status: responseStatusCode,
52+
}),
53+
);
54+
55+
pipe(body);
56+
},
57+
onShellError(error: unknown) {
58+
reject(error);
59+
},
60+
onError(error: unknown) {
61+
responseStatusCode = 500;
62+
// Log streaming rendering errors from inside the shell. Don't log
63+
// errors encountered during initial shell rendering since they'll
64+
// reject and get logged in handleDocumentRequest.
65+
if (shellRendered) {
66+
console.error(error);
67+
}
68+
},
69+
});
70+
71+
setTimeout(abort, ABORT_DELAY);
72+
});
73+
}
74+
75+
function handleBrowserRequest(
76+
request: Request,
77+
responseStatusCode: number,
78+
responseHeaders: Headers,
79+
reactRouterContext: EntryContext,
80+
) {
81+
return new Promise((resolve, reject) => {
82+
let shellRendered = false;
83+
const { pipe, abort } = renderToPipeableStream(<ServerRouter context={reactRouterContext} url={request.url} />, {
84+
onShellReady() {
85+
shellRendered = true;
86+
const body = new PassThrough();
87+
const stream = createReadableStreamFromReadable(body);
88+
89+
responseHeaders.set('Content-Type', 'text/html');
90+
91+
resolve(
92+
new Response(stream, {
93+
headers: responseHeaders,
94+
status: responseStatusCode,
95+
}),
96+
);
97+
98+
pipe(body);
99+
},
100+
onShellError(error: unknown) {
101+
reject(error);
102+
},
103+
onError(error: unknown) {
104+
responseStatusCode = 500;
105+
// Log streaming rendering errors from inside the shell. Don't log
106+
// errors encountered during initial shell rendering since they'll
107+
// reject and get logged in handleDocumentRequest.
108+
if (shellRendered) {
109+
console.error(error);
110+
}
111+
},
112+
});
113+
114+
setTimeout(abort, ABORT_DELAY);
115+
});
116+
}

0 commit comments

Comments
 (0)