Skip to main content

Guards

If you want to restrict access to certain routes, you can use guards. You can view a finished example of an application with guards in the examples folder or in RealWorld example.

Any guard is a DI provider passed to injectors at the request level (if the controller is non-singleton), or at other levels (if the controller is singleton). Each guard must be a class that implements the CanActivate interface:

interface CanActivate {
canActivate(ctx: RequestContext, params?: any[]): boolean | number | Promise<boolean | number>;
}

For example, it can be done like this:

import { guard, CanActivate, RequestContext } from '@ditsmod/core';
import { AuthService } from './auth.service.js';

@guard({ isSingleton: false })
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService) {}

async canActivate(ctx: RequestContext, params?: any[]) {
return Boolean(await this.authService.updateAndGetSession());
}
}

It is recommended that guard files end with *.guard.ts and their class names end with *Guard.

If canActivate() returns:

  • true or Promise<true>, means Ditsmod will process the corresponding route with this guard;
  • false or Promise<false>, so the response to the request will contain a 401 status and the controller will not process the route;
  • number or Promise<number> is interpreted by Ditsmod as a status number (403, 401, etc.) that should be returned in response to an HTTP request.

Passing guards to injectors

Guards can be passed in module or controller metadata:

import { featureModule } from '@ditsmod/core';

import { AuthGuard } from 'auth.guard';

@featureModule({
providersPerReq: [AuthGuard],
})
export class SomeModule {}

In this case, the guard will work at the request level, for non-singleton controllers.

Use of guards

The guards are passed to the controllers in the array in the third parameter of the route decorator:

import { controller, Res, route } from '@ditsmod/core';
import { AuthGuard } from './auth.guard.js';

@controller()
export class SomeController {
@route('GET', 'some-url', [AuthGuard])
tellHello(res: Res) {
res.send('Hello, admin!');
}
}

Guards with parameters

The guard in the canActivate() method has two parameters. The arguments for the first parameter are automatically passed with the RequestContext datatype, and the arguments for the second parameter can be passed to the route decorator in an array where a certain guard comes first.

Let's consider such an example:

import { controller, Res, route } from '@ditsmod/core';

import { PermissionsGuard } from './permissions.guard.js';
import { Permission } from './permission.js';

@controller()
export class SomeController {
@route('GET', 'some-url', [[PermissionsGuard, Permission.canActivateAdministration]])
tellHello(res: Res) {
res.send('Hello, admin!');
}
}

As you can see, in place of the third parameter in route, an array is passed in an array, where PermissionsGuard is specified in the first place, followed by arguments for it. In this case, PermissionsGuard will receive these arguments in its canActivate() method:

import { injectable, CanActivate, Status, RequestContext } from '@ditsmod/core';

import { AuthService } from './auth.service.js';
import { Permission } from './permission.js';

@injectable()
export class PermissionsGuard implements CanActivate {
constructor(private authService: AuthService) {}

async canActivate(ctx: RequestContext, params?: Permission[]) {
if (await this.authService.hasPermissions(params)) {
return true;
} else {
return Status.FORBIDDEN;
}
}
}

Helpers for guards with parameters

Because parameter guards must be passed as an array within an array, this makes readability and type safety worse. For such cases, it is better to create a helper using the createHelperForGuardWithParams() factory:

import { createHelperForGuardWithParams } from '@ditsmod/core';
import { Permission } from './types.js';
import { PermissionsGuard } from './permissions-guard.js';

export const requirePermissions = createHelperForGuardWithParams<Permission>(PermissionsGuard);

In this example, PermissionsGuard is passed as an argument, which accepts parameters of type Permission in its canActivate() method.

requirePermissions() can now be used to create routes:

import { controller, Res, route } from '@ditsmod/core';

import { requirePermissions } from '../auth/guards-utils.js';
import { Permission } from '../auth/types.js';

@controller()
export class SomeController {
@route('GET', 'administration', [requirePermissions(Permission.canActivateAdministration)])
helloAdmin(res: Res) {
res.send('some secret');
}
}

Setting guards on the imported module

You can also centrally set guards at the module level:

import { featureModule } from '@ditsmod/core';

import { OtherModule } from '../other/other.module.js';
import { AuthModule } from '../auth/auth.module.js';
import { AuthGuard } from '../auth/auth.guard.js';

@featureModule({
imports: [
AuthModule,
{ path: 'some-path', module: OtherModule, guards: [AuthGuard] }
]
})
export class SomeModule {}

In this case, AuthGuard will be automatically added to each route in OtherModule.