-
Notifications
You must be signed in to change notification settings - Fork 26
Description
This is a continuation of #761 due to the issue being closed and the provided solution not being viable for my usecase.
The reason the suggested solution (using @Hook) doesn't work for me, is because I need to use different validators on different endpoints, so a catch-all hook for the whole controller won't work for me.
I'm using @fastify/auth for authentication, and have an auth plugin that registers fastify-auth and provides decorators for isAuthenticated, isAdmin, etc.
The issue is that fastify-auth shows all the implementation examples using fastify.route (giving you access to fastify.auth):
fastify
.decorate('verifyJWTandLevel', function (request, reply, done) {
// your validation logic
done() // pass an error if the authentication fails
})
.decorate('verifyUserAndPassword', function (request, reply, done) {
// your validation logic
done() // pass an error if the authentication fails
})
.register(require('@fastify/auth'))
.after(() => {
fastify.route({
method: 'POST',
url: '/auth-multiple',
preHandler: fastify.auth([
fastify.verifyJWTandLevel,
fastify.verifyUserAndPassword
]),
handler: (req, reply) => {
req.log.info('Auth route')
reply.send({ hello: 'world' })
}
})
})But this isn't congruent with the controller pattern since you don't get access to a fastify instance:
@Controller({ route: '/auth', tags: [{ name: 'Auth', description: 'Authentication' }] })
export default class AuthController {
@Inject(AuthServiceToken)
private readonly authService!: AuthService
@Inject(FastifyInstanceToken)
private readonly fastify!: FastifyInstance
@GET('/me', {
schema: {
description: "Retrieve the current user's information",
response: { 200: UserResponseSchema, 401: UnauthorizedError },
},
onRequest: this.fastify.auth([this.fastify.isAuthenticated]), // error on 'this': Object is possibly 'undefined'
})
async getMyAccount(request: FastifyRequest, reply: FastifyReply) {...}
}So my solution to this was to create an AuthValidators class like this:
export default class AuthValidators {
private static readonly fastify = getInstanceByToken<FastifyInstance>(FastifyInstanceToken)
...
// Requires a valid JWT token
static readonly isAuthenticated = AuthValidators.fastify.auth([AuthValidators.fastify.isAuthenticated])
...
}And this works as expected when I use it like this:
@GET('/me', {
schema: {
description: "Retrieve the current user's information",
response: { 200: UserResponseSchema, 401: UnauthorizedError },
},
onRequest: AuthValidators.isAuthenticated,
})
async getMyAccount(request: FastifyRequest, reply: FastifyReply) {...}HOWEVER, the issue comes up when trying to unit test these controllers.
One of the tests is this:
tap.test('Authenticated', async (t) => {
const app = fastify()
const authController = await configureControllerTest({
controller: AuthController,
plugins: [authPlugin], // <-- This is what provides the `.isAuthenticated` decorator
instance: app,
mocks: [
{ provide: UserRepositoryToken, useValue: userRepository },
{ provide: AuthServiceToken, useValue: authService },
],
})
t.test('Authenticated request to /auth/me should succeed', async (t) => {
const result = await authController.inject({
method: 'GET',
url: '/auth/me',
cookies: { access: 'valid' },
})
t.equal(result.statusCode, 200)
t.match(result.json(), { id: String, firstName: String, lastName: String, email: String, provider: String })
})
})When I run tests, the above prints this:
2> src/plugins/auth.test.ts
/path/to/project/lib/utils/get-instance-by-token.ts:27
if (!injectable) throw new Error(`Injectable not found for token "${token.toString()}"`);
^
Error: Injectable not found for token "Symbol(fastify-decorators.fastify-instance)"
at verifyInjectable (/path/to/project/lib/utils/get-instance-by-token.ts:27:26)
at getInstanceByToken (/path/to/project/lib/utils/get-instance-by-token.ts:18:3)
at Function.<static_initializer> (/path/to/project/src/validators/auth-validators.ts:5:37)
at <anonymous> (/path/to/project/src/validators/auth-validators.ts:2:1)
at ModuleJob.run (node:internal/modules/esm/module_job:272:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:552:26)
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:98:5)
So how can I use @fastify/auth in combination with fastify-decorators when using the @controller pattern?