Skip to content

Providing on* hooks to @Controller handler methods? #1077

@Voyen

Description

@Voyen

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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions