Skip to content

Commit b886326

Browse files
Merge pull request #6 from rphlmr/feat/add-example
2 parents 39c37a8 + bcf41ec commit b886326

37 files changed

+20421
-99
lines changed

README.md

Lines changed: 109 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -32,66 +32,65 @@ It Supports the following:
3232
To allow for more freedom and support some of the different authentication types the verify no longer just sends the form,
3333
but it now sends the entire request. See [Setup authenticator & strategy](#setup-authenticator-&-strategy)
3434

35-
### Setup sessionStorage
35+
### Setup sessionStorage, strategy & authenticator
3636
```js
37-
// app/session.server.ts
37+
// app/auth.server.ts
3838
import { createCookieSessionStorage } from 'remix'
39+
import { Authenticator, AuthorizationError } from 'remix-auth'
40+
import { SupabaseStrategy } from '@afaik/remix-auth-supabase-strategy'
41+
import { supabaseClient } from '~/supabase'
42+
import type { Session } from '~/supabase'
3943

4044
export const sessionStorage = createCookieSessionStorage({
41-
cookie: {
42-
name: 'sb',
43-
sameSite: 'lax',
44-
path: '/',
45-
httpOnly: true,
46-
secrets: [SESSION_SECRET],
47-
// 1 hour resembles access_token expiration
48-
// set it to 1 hour if you want to user to re-log every hour
49-
// 24 hours refreshes_token if the user visits every 24 hours
50-
maxAge: 60 * 60 * 24,
51-
secure: process.env.NODE_ENV === 'production',
52-
},
45+
cookie: {
46+
name: 'sb',
47+
httpOnly: true,
48+
path: '/',
49+
sameSite: 'lax',
50+
secrets: ['s3cr3t'], // This should be an env variable
51+
secure: process.env.NODE_ENV === 'production',
52+
},
5353
})
54-
```
55-
56-
### Setup authenticator & strategy
57-
```js
58-
// app/auth.server.ts
59-
import type { Session } from '@supabase/supabase-js'
60-
import { Authenticator } from 'remix-auth'
61-
import { SupabaseStrategy } from 'remix-auth-supabase'
62-
import { supabase } from '~/utils/supabase'
63-
import { sessionStorage } from '~/session.server'
6454

6555
export const supabaseStrategy = new SupabaseStrategy(
66-
{
67-
supabaseClient: supabase,
68-
sessionStorage,
69-
sessionKey: 'sb:session', // if not set, default is sb:session
70-
sessionErrorKey: 'sb:error', // if not set, default is sb:error
71-
},
72-
async ({ req, supabaseClient }) => {
56+
{
57+
supabaseClient,
58+
sessionStorage,
59+
sessionKey: 'sb:session', // if not set, default is sb:session
60+
sessionErrorKey: 'sb:error', // if not set, default is sb:error
61+
},
7362
// simple verify example for email/password auth
74-
const form = await req.formData()
75-
const email = form?.get('email')
76-
const password = form?.get('password')
77-
if (!email || typeof email !== 'string' || !password || typeof password !== 'string')
78-
throw new Error('Need a valid email and/or password')
79-
80-
return supabaseClient.auth.api.signInWithEmail(email, password).then((res) => {
81-
if (res?.error || !res.data)
82-
throw new Error(res?.error?.message ?? 'No user found')
83-
84-
return res?.data
85-
})
86-
},
63+
async({ req, supabaseClient }) => {
64+
const form = await req.formData()
65+
const email = form?.get('email')
66+
const password = form?.get('password')
67+
68+
if (!email) throw new AuthorizationError('Email is required')
69+
if (typeof email !== 'string')
70+
throw new AuthorizationError('Email must be a string')
71+
72+
if (!password) throw new AuthorizationError('Password is required')
73+
if (typeof password !== 'string')
74+
throw new AuthorizationError('Password must be a string')
75+
76+
return supabaseClient.auth.api
77+
.signInWithEmail(email, password)
78+
.then(({ data, error }): Session => {
79+
if (error || !data) {
80+
throw new AuthorizationError(
81+
error?.message ?? 'No user session found',
82+
)
83+
}
84+
85+
return data
86+
})
87+
},
8788
)
8889

89-
export const authenticator = new Authenticator<Session>(
90-
sessionStorage,
91-
{
90+
export const authenticator = new Authenticator<Session>(sessionStorage, {
9291
sessionKey: supabaseStrategy.sessionKey, // keep in sync
9392
sessionErrorKey: supabaseStrategy.sessionErrorKey, // keep in sync
94-
})
93+
})
9594

9695
authenticator.use(supabaseStrategy)
9796
```
@@ -101,14 +100,14 @@ authenticator.use(supabaseStrategy)
101100
102101
```js
103102
// app/routes/login.ts
104-
export const loader: LoaderFunction = async({ request }) =>
105-
supabaseStrategy.checkSession(request, {
106-
successRedirect: '/profile'
107-
})
103+
export const loader: LoaderFunction = async({ request }) =>
104+
supabaseStrategy.checkSession(request, {
105+
successRedirect: '/private'
106+
})
108107

109108
export const action: ActionFunction = async({ request }) =>
110109
authenticator.authenticate('sb', request, {
111-
successRedirect: '/profile',
110+
successRedirect: '/private',
112111
failureRedirect: '/login',
113112
})
114113

@@ -124,7 +123,7 @@ export default function LoginPage() {
124123
```
125124
126125
```js
127-
// app/routes/profile.ts
126+
// app/routes/private.ts
128127
export const loader: LoaderFunction = async({ request }) => {
129128
// If token refresh and successRedirect not set, reload the current route
130129
const session = await supabaseStrategy.checkSession(request);
@@ -151,9 +150,9 @@ await supabaseStrategy.checkSession(request, {
151150
152151
##### Redirect if authenticated
153152
```js
154-
// If the user is authenticated, redirect to /profile
153+
// If the user is authenticated, redirect to /private
155154
await supabaseStrategy.checkSession(request, {
156-
successRedirect: "/profile",
155+
successRedirect: "/private",
157156
});
158157
```
159158
@@ -175,68 +174,80 @@ if (session) {
175174
```js
176175
// app/routes/login.ts
177176
export const loader: LoaderFunction = async({ request }) => {
178-
// Beware, never set failureRedirect equals to the current route
179-
const session = supabaseStrategy.checkSession(request, {
180-
successRedirect: '/profile',
181-
failureRedirect: "/login", // ❌ DONT'T : infinite loop
182-
});
183-
184-
// In this example, session is always null otherwise it would have been redirected
177+
// Beware, never set failureRedirect equals to the current route
178+
const session = supabaseStrategy.checkSession(request, {
179+
successRedirect: '/private',
180+
failureRedirect: "/login", // ❌ DONT'T : infinite loop
181+
});
182+
183+
// In this example, session is always null otherwise it would have been redirected
185184
}
186185
```
187186
188187
#### Redirect to
188+
[Example]("https://github.com/mitchelvanbever/remix-auth-supabase-strategy/tree/main/examples/with-redirect-to")
189189
> With Remix.run it's easy to add super UX
190+
190191
```js
191-
// app/routes/profile.ts
192+
// app/routes/private.profile.ts
192193
export const loader: LoaderFunction = async({ request }) =>
193-
// If checkSession fails, redirect to login and go back here when authenticated
194-
supabaseStrategy.checkSession(request, {
195-
failureRedirect: `/login?redirectTo=/profile`
196-
});
194+
// If checkSession fails, redirect to login and go back here when authenticated
195+
supabaseStrategy.checkSession(request, {
196+
failureRedirect: `/login?redirectTo=/private/profile`
197+
});
198+
```
199+
```js
200+
// app/routes/private.ts
201+
export const loader: LoaderFunction = async({ request }) =>
202+
// If checkSession fails, redirect to login and go back here when authenticated
203+
supabaseStrategy.checkSession(request, {
204+
failureRedirect: `/login`
205+
});
197206
```
198207
```js
199208
// app/routes/login.ts
200209
export const loader = async ({ request }) => {
201-
const redirectTo = new URL(request.url).searchParams.get("redirectTo") ?? "/dashboard";
210+
const redirectTo = new URL(request.url).searchParams.get("redirectTo") ?? "/profile";
202211

203212
return supabaseStrategy.checkSession(request, {
204213
successRedirect: redirectTo,
205214
});
206215
};
207216

208217
export const action: ActionFunction = async({ request }) =>{
209-
// Always clone request when access formData() in action/loader with authenticator
210-
// 💡 request.formData() can't be called twice
211-
const data = await request.clone().formData();
212-
// If authenticate success, redirectTo what found in searchParams
213-
// Or where you want
214-
const redirectTo = data.get("redirectTo") ?? "/dashboard";
215-
216-
return authenticator.authenticate('sb', request, {
217-
successRedirect: redirectTo,
218-
failureRedirect: '/login',
219-
})
218+
// Always clone request when access formData() in action/loader with authenticator
219+
// 💡 request.formData() can't be called twice
220+
const data = await request.clone().formData();
221+
// If authenticate success, redirectTo what found in searchParams
222+
// Or where you want
223+
const redirectTo = data.get("redirectTo") ?? "/profile";
224+
225+
return authenticator.authenticate('sb', request, {
226+
successRedirect: redirectTo,
227+
failureRedirect: '/login',
228+
})
220229
}
221230

222231
export default function LoginPage() {
223-
const [searchParams] = useSearchParams();
224-
225-
return (
226-
<Form method="post">
227-
<input
228-
name="redirectTo"
229-
value={searchParams.get("redirectTo") ?? undefined}
230-
hidden
231-
readOnly
232-
/>
233-
<input type="email" name="email" required />
234-
<input
235-
type="password"
236-
name="password"
237-
/>
238-
<button>Sign In</button>
239-
</Form>
240-
);
232+
const [searchParams] = useSearchParams();
233+
234+
return (
235+
<Form method="post">
236+
<input
237+
name="redirectTo"
238+
value={searchParams.get("redirectTo") ?? undefined}
239+
hidden
240+
readOnly
241+
/>
242+
<input type="email" name="email" />
243+
<input type="password" name="password" />
244+
<button>Sign In</button>
245+
</Form>
246+
);
241247
}
242248
```
249+
250+
## 📖 Examples
251+
- [Email / password]("https://github.com/mitchelvanbever/remix-auth-supabase-strategy/tree/main/examples/email-password")
252+
- [With redirect to]("https://github.com/mitchelvanbever/remix-auth-supabase-strategy/tree/main/examples/with-redirect-to")
253+
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
SUPABASE_SERVICE_KEY="{SERVICE_KEY}"
2+
SUPABASE_URL="https://{YOUR_INSTANCE_NAME}.supabase.co"

examples/email-password/.gitignore

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

examples/email-password/README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Remix Auth - Supabase Strategy with email and password
2+
3+
Authentication using `signInWithEmail`.
4+
5+
## Preview
6+
7+
Open this example on [CodeSandbox](https://codesandbox.com):
8+
9+
[![Open in CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/mitchelvanbever/remix-auth-supabase/tree/main/examples/email-password)
10+
11+
## Setup
12+
13+
1. Copy `.env.example` to create a new file `.env`:
14+
15+
```sh
16+
cp .env.example .env
17+
```
18+
2. Go to https://app.supabase.io/project/{PROJECT}/api?page=auth to find your secrets
19+
3. Add your `SUPABASE_URL` and `SUPABASE_SERVICE_ROLE` in `.env`
20+
```env
21+
SUPABASE_SERVICE_KEY="{SERVICE_KEY}"
22+
SUPABASE_URL="https://{YOUR_INSTANCE_NAME}.supabase.co"
23+
```
24+
25+
## Using the Remix Auth & SupabaseStrategy 🚀
26+
27+
SupabaseStrategy provides `checkSession` working like Remix Auth `isAuthenticated` but handles token refresh
28+
29+
You must use `checkSession` instead of `isAuthenticated`
30+
31+
32+
## Example
33+
34+
This is using Remix Auth, `remix-auth-supabase` and `supabase-js` packages.
35+
36+
> Thanks to Remix, we can securely use server only authentication with `supabase.auth.api.signInWithEmail`
37+
>
38+
> This function should only be called on a server (`loader` or `action` functions).
39+
>
40+
> **⚠️ Never expose your `service_role` key in the browser**
41+
42+
43+
The `/login` route renders a form with a email and password input. After a submit it runs some validations and store `user` object, `access_token` and `refresh_token` in the session.
44+
45+
The `/private` routes redirects the user to `/login` if it's not logged-in, or shows the user email and a logout form if it's logged-in.
46+
47+
**Handle refreshing of tokens** (if expired) or redirects to `/login` if it fails
48+
49+
More use cases can be found on [Supabase Strategy - Use cases](https://github.com/mitchelvanbever/remix-auth-supabase#using-the-authenticator--strategy-)
50+
51+
## Related Links
52+
53+
- [Remix Auth](https://github.com/sergiodxa/remix-auth)
54+
- [Supabase Strategy](https://github.com/mitchelvanbever/remix-auth-supabase)
55+
- [supabase-js](https://github.com/supabase/supabase-js)

0 commit comments

Comments
 (0)