Guards

Control access to routes based on conditions like authentication and roles.

Guards determine whether a request should be handled by the route handler. They are the recommended way to implement authentication and authorization in MiiaJS.

CanActivate interface

A guard is a class that implements the CanActivate interface:

import type { CanActivate, RequestContext } from '@miiajs/core'

class AuthGuard implements CanActivate {
  canActivate(ctx: RequestContext): boolean | Promise<boolean> {
    const token = ctx.req.headers.get('authorization')
    return !!token
  }
}
  • Return true to allow the request
  • Return false to reject with 403 Forbidden
  • Throw an HttpException for a custom error response

Applying guards

Per route

import { UseGuard } from '@miiajs/core'

@Controller('/items')
class ItemController {
  @Delete('/:id')
  @UseGuard(AuthGuard)
  remove(ctx: RequestContext) {
    return { deleted: true }
  }
}

Per controller

@Controller('/admin')
@UseGuard(AuthGuard)
class AdminController {
  @Get('/dashboard')
  dashboard() {
    return { stats: {} }
  }
}

Global

const app = new Miia()
  .useGuard(AuthGuard)
  .register(AppModule)

Guards with DI

Guards can use dependency injection:

@Injectable()
class AuthGuard implements CanActivate {
  private tokenService = inject(TokenService)

  async canActivate(ctx: RequestContext): Promise<boolean> {
    const header = ctx.req.headers.get('authorization')
    if (!header?.startsWith('Bearer ')) {
      throw new UnauthorizedException()
    }

    const user = await this.tokenService.verify(header.slice(7))
    if (!user) throw new UnauthorizedException()

    ctx.user = user
    return true
  }
}

Parameterized guards

Create guard factories for reusable logic:

function Roles(...roles: string[]) {
  class RolesGuard implements CanActivate {
    canActivate(ctx: RequestContext) {
      return roles.includes(ctx.user?.role)
    }
  }
  return RolesGuard
}

Combine multiple guards:

@Delete('/:id')
@UseGuard(AuthGuard, Roles('admin'))
remove(ctx: RequestContext) {
  return { deleted: true }
}

Guards execute in order. If AuthGuard fails, Roles is never called.

Guard execution order

Global guards (app.useGuard())
  -> Controller guards (@UseGuard on class)
  -> Route guards (@UseGuard on method)
  -> Route handler