Skip to content

Commit e861a02

Browse files
committed
add samples
1 parent 630032a commit e861a02

File tree

125 files changed

+4440
-4
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

125 files changed

+4440
-4
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,10 @@ async foo({ auth }: HttpContext) {
5656

5757
A list of examples is available [here](samples/) to show the possible implementations/modifications of the classes generated by the package. They also show how to test the application with a supabase mock
5858

59-
1. **classic:** unmodified version of the package with tests
60-
2. **with-custom-details:** modified version of the package with user details, more information [here](https://supabase.com/docs/guides/auth/auth-hooks)
61-
3. **with-internal-profile:** modified version of the package with an internal profile (stored in a sqlite database)
62-
4. **with-both-and-role-based:** version combining the above 2 samples, adding a decorator to build a role access based application
59+
1. **classic:** unmodified version of the package with tests - [README](samples/classic/README.md)
60+
2. **with-custom-details:** modified version of the package with user details, more information [here](https://supabase.com/docs/guides/auth/auth-hooks) - [README](samples/with-custom-details/README.md)
61+
3. **with-internal-profile:** modified version of the package with an internal profile (stored in a sqlite database) - [README](samples/with-internal-profile/README.md)
62+
4. **with-both-and-role-based:** version combining the above 2 samples, adding a decorator to build a role access based application - [README](samples/with-both-and-role-based/README.md)
6363

6464
## License
6565
This project is Open Source software released under the [MIT license.](LICENSE.md)

samples/classic/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Classic version
2+
3+
This version doesn't modify the code provided by the package. It just add an `auth.spec.ts` test that shows how to test routes that are **secure with supabase**.

samples/classic/ace.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
|--------------------------------------------------------------------------
3+
| JavaScript entrypoint for running ace commands
4+
|--------------------------------------------------------------------------
5+
|
6+
| DO NOT MODIFY THIS FILE AS IT WILL BE OVERRIDDEN DURING THE BUILD
7+
| PROCESS.
8+
|
9+
| See docs.adonisjs.com/guides/typescript-build-process#creating-production-build
10+
|
11+
| Since, we cannot run TypeScript source code using "node" binary, we need
12+
| a JavaScript entrypoint to run ace commands.
13+
|
14+
| This file registers the "ts-node/esm" hook with the Node.js module system
15+
| and then imports the "bin/console.ts" file.
16+
|
17+
*/
18+
19+
/**
20+
* Register hook to process TypeScript files using ts-node
21+
*/
22+
import 'ts-node-maintained/register/esm'
23+
24+
/**
25+
* Import ace console entrypoint
26+
*/
27+
await import('./bin/console.js')

samples/classic/adonisrc.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { defineConfig } from '@adonisjs/core/app'
2+
3+
export default defineConfig({
4+
/*
5+
|--------------------------------------------------------------------------
6+
| Commands
7+
|--------------------------------------------------------------------------
8+
|
9+
| List of ace commands to register from packages. The application commands
10+
| will be scanned automatically from the "./commands" directory.
11+
|
12+
*/
13+
commands: [() => import('@adonisjs/core/commands'), () => import('@adonisjs/lucid/commands')],
14+
15+
/*
16+
|--------------------------------------------------------------------------
17+
| Service providers
18+
|--------------------------------------------------------------------------
19+
|
20+
| List of service providers to import and register when booting the
21+
| application
22+
|
23+
*/
24+
providers: [
25+
() => import('@adonisjs/core/providers/app_provider'),
26+
() => import('@adonisjs/core/providers/hash_provider'),
27+
{
28+
file: () => import('@adonisjs/core/providers/repl_provider'),
29+
environment: ['repl', 'test'],
30+
},
31+
() => import('@adonisjs/core/providers/vinejs_provider'),
32+
() => import('@adonisjs/cors/cors_provider'),
33+
() => import('@adonisjs/lucid/database_provider'),
34+
() => import('@adonisjs/auth/auth_provider')
35+
],
36+
37+
/*
38+
|--------------------------------------------------------------------------
39+
| Preloads
40+
|--------------------------------------------------------------------------
41+
|
42+
| List of modules to import before starting the application.
43+
|
44+
*/
45+
preloads: [() => import('#start/routes'), () => import('#start/kernel')],
46+
47+
/*
48+
|--------------------------------------------------------------------------
49+
| Tests
50+
|--------------------------------------------------------------------------
51+
|
52+
| List of test suites to organize tests by their type. Feel free to remove
53+
| and add additional suites.
54+
|
55+
*/
56+
tests: {
57+
suites: [
58+
{
59+
files: ['tests/unit/**/*.spec(.ts|.js)'],
60+
name: 'unit',
61+
timeout: 2000,
62+
},
63+
{
64+
files: ['tests/functional/**/*.spec(.ts|.js)'],
65+
name: 'functional',
66+
timeout: 30000,
67+
},
68+
],
69+
forceExit: false,
70+
},
71+
})
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { errors, symbols } from '@adonisjs/auth'
2+
import { AuthClientResponse, GuardContract } from '@adonisjs/auth/types'
3+
import { HttpContext } from '@adonisjs/core/http'
4+
import { supabase } from '../../utils/supabase_util.js'
5+
import { User } from '@supabase/supabase-js'
6+
import jwt, { JwtPayload } from 'jsonwebtoken'
7+
import app from '@adonisjs/core/services/app'
8+
9+
export class SupabaseJwtGuard implements GuardContract<CustomSupabaseUser> {
10+
#ctx: HttpContext
11+
12+
constructor(ctx: HttpContext) {
13+
this.#ctx = ctx
14+
}
15+
16+
declare [symbols.GUARD_KNOWN_EVENTS]: {}
17+
18+
driverName: 'supabase' = 'supabase'
19+
20+
authenticationAttempted: boolean = false
21+
22+
isAuthenticated: boolean = false
23+
24+
user?: CustomSupabaseUser
25+
26+
/**
27+
* Authenticate the current HTTP request and return
28+
* the user instance if there is a valid JWT token
29+
* or throw an exception
30+
*/
31+
async authenticate(): Promise<CustomSupabaseUser> {
32+
if (app.inTest) {
33+
//Skip authentication process due to testing
34+
return await this.testingAuthenticate()
35+
}
36+
37+
/**
38+
* Avoid re-authentication when it has been done already
39+
* for the given request
40+
*/
41+
if (this.authenticationAttempted) {
42+
return this.getUserOrFail()
43+
}
44+
this.authenticationAttempted = true
45+
46+
/**
47+
* Ensure the auth header exists
48+
*/
49+
const authHeader = this.#ctx.request.header('authorization')
50+
if (!authHeader) {
51+
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
52+
guardDriverName: this.driverName,
53+
})
54+
}
55+
56+
/**
57+
* Split the header value and read the token from it
58+
*/
59+
const [, token] = authHeader.split('Bearer ')
60+
if (!token) {
61+
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
62+
guardDriverName: this.driverName,
63+
})
64+
}
65+
66+
/**
67+
* Get user data from supabase
68+
*/
69+
const {
70+
data: { user },
71+
} = await supabase.auth.getUser(token)
72+
73+
if (!user) {
74+
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
75+
guardDriverName: this.driverName,
76+
})
77+
}
78+
79+
this.user = await this.buildCustomSupabaseUser(token, user)
80+
return this.getUserOrFail()
81+
}
82+
83+
/**
84+
* Same as authenticate, but does not throw an exception
85+
*/
86+
async check(): Promise<boolean> {
87+
try {
88+
await this.authenticate()
89+
return true
90+
} catch {
91+
return false
92+
}
93+
}
94+
95+
/**
96+
* Returns the authenticated user or throws an error
97+
*/
98+
getUserOrFail(): CustomSupabaseUser {
99+
if (!this.user) {
100+
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
101+
guardDriverName: this.driverName,
102+
})
103+
}
104+
105+
return this.user
106+
}
107+
108+
/**
109+
* This method is called by Japa during testing when "loginAs"
110+
* method is used to login the user.
111+
*/
112+
async authenticateAsClient(user: CustomSupabaseUser): Promise<AuthClientResponse> {
113+
return {
114+
headers: {
115+
testingUser: JSON.stringify(user),
116+
},
117+
}
118+
}
119+
120+
async buildCustomSupabaseUser(token: string, user: User): Promise<CustomSupabaseUser> {
121+
const customPayload = jwt.decode(token) as CustomSupabaseJwtPayload
122+
123+
return {
124+
...user,
125+
details: customPayload.details,
126+
}
127+
}
128+
129+
async testingAuthenticate(): Promise<CustomSupabaseUser> {
130+
const testingUser = this.#ctx.request.header('testingUser')
131+
if (!testingUser) {
132+
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
133+
guardDriverName: this.driverName,
134+
})
135+
}
136+
137+
this.authenticationAttempted = true
138+
139+
const decodedUser = JSON.parse(testingUser)
140+
this.user = {
141+
...decodedUser,
142+
}
143+
return this.getUserOrFail()
144+
}
145+
}
146+
147+
export interface CustomSupabaseUser extends User {
148+
details: CustomSupabaseUserDetails
149+
// TODO: add your local profile extension if needed
150+
// e.g. profile: Profile
151+
}
152+
153+
export interface CustomSupabaseUserDetails {
154+
// TODO: Add here your custom details
155+
// e.g. role: string | null
156+
}
157+
158+
export interface CustomSupabaseJwtPayload extends JwtPayload {
159+
details: CustomSupabaseUserDetails
160+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import app from '@adonisjs/core/services/app'
2+
import { HttpContext, ExceptionHandler } from '@adonisjs/core/http'
3+
4+
export default class HttpExceptionHandler extends ExceptionHandler {
5+
/**
6+
* In debug mode, the exception handler will display verbose errors
7+
* with pretty printed stack traces.
8+
*/
9+
protected debug = !app.inProduction
10+
11+
/**
12+
* The method is used for handling errors and returning
13+
* response to the client
14+
*/
15+
async handle(error: unknown, ctx: HttpContext) {
16+
return super.handle(error, ctx)
17+
}
18+
19+
/**
20+
* The method is used to report error to the logging service or
21+
* the third party error monitoring service.
22+
*
23+
* @note You should not attempt to send a response from this method.
24+
*/
25+
async report(error: unknown, ctx: HttpContext) {
26+
return super.report(error, ctx)
27+
}
28+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { HttpContext } from '@adonisjs/core/http'
2+
import type { NextFn } from '@adonisjs/core/types/http'
3+
import type { Authenticators } from '@adonisjs/auth/types'
4+
5+
/**
6+
* Auth middleware is used authenticate HTTP requests and deny
7+
* access to unauthenticated users.
8+
*/
9+
export default class AuthMiddleware {
10+
/**
11+
* The URL to redirect to, when authentication fails
12+
*/
13+
redirectTo = '/login'
14+
15+
async handle(
16+
ctx: HttpContext,
17+
next: NextFn,
18+
options: {
19+
guards?: (keyof Authenticators)[]
20+
} = {}
21+
) {
22+
await ctx.auth.authenticateUsing(options.guards, { loginRoute: this.redirectTo })
23+
return next()
24+
}
25+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Logger } from '@adonisjs/core/logger'
2+
import { HttpContext } from '@adonisjs/core/http'
3+
import type { NextFn } from '@adonisjs/core/types/http'
4+
5+
/**
6+
* The container bindings middleware binds classes to their request
7+
* specific value using the container resolver.
8+
*
9+
* - We bind "HttpContext" class to the "ctx" object
10+
* - And bind "Logger" class to the "ctx.logger" object
11+
*/
12+
export default class ContainerBindingsMiddleware {
13+
handle(ctx: HttpContext, next: NextFn) {
14+
ctx.containerResolver.bindValue(HttpContext, ctx)
15+
ctx.containerResolver.bindValue(Logger, ctx.logger)
16+
17+
return next()
18+
}
19+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { HttpContext } from '@adonisjs/core/http'
2+
import type { NextFn } from '@adonisjs/core/types/http'
3+
4+
/**
5+
* Updating the "Accept" header to always accept "application/json" response
6+
* from the server. This will force the internals of the framework like
7+
* validator errors or auth errors to return a JSON response.
8+
*/
9+
export default class ForceJsonResponseMiddleware {
10+
async handle({ request }: HttpContext, next: NextFn) {
11+
const headers = request.headers()
12+
headers.accept = 'application/json'
13+
14+
return next()
15+
}
16+
}

0 commit comments

Comments
 (0)