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
orPromise<true>
, means Ditsmod will process the corresponding route with this guard;false
orPromise<false>
, so the response to the request will contain a 401 status and the controller will not process the route;number
orPromise<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
.