I have a NestJS REST api with an RBAC Guard, Since I don't want to get the role of the user via the cookies but rather identify the user in the database and access it's correct role, I'm importing the UserService into the Guard to access that user.
Problem is that the UserService uses the same Guard for it's routes which creates a circular dependency that is not solved with the forwardRef() method provider by the documentation
Here is how I import the guard in the UserService
// ...
import { User } from "./user.entity";
import { InjectRepository } from "@nestjs/typeorm";
import {RolesGuard} from "@src/guards/role.guard";
@Injectable()
export class UserService {
constructor(
@Inject(forwardRef(() => RolesGuard))
@InjectRepository(User) private userRepository: Repository<User>,
private TrackingService: TrackingService,
) {}
// logic of the service
here is how I import the GuardModule into the UserModule
// ...
import { GuardModule } from "@src/guards/guards.module";
@Module({
imports: [
TypeOrmModule.forFeature([User, BaseEntity]),
forwardRef(() => GuardModule),
],
// ...
Then for the Role itself, here is how I would like to use the UserService
import {CanActivate, ExecutionContext, forwardRef, Inject, Injectable} from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { ROLES_KEY } from "../decorators/roles.decorator";
import { Role } from "src/enums/role.enum";
import { RoleService } from "@src/utils/roles.service";
import {UserService} from "@src/user/user.service";
@Injectable()
export class RolesGuard implements CanActivate {
constructor(
private reflector: Reflector,
private RoleService: RoleService,
@Inject( forwardRef(() => UserService))
private userService: UserService,
) {}
async canActivate(
context: ExecutionContext
): Promise<boolean> {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
// get unique UUID saved by client in the cookies
const userID = context.switchToHttp().getRequest().userID;
// search for user with corresponding ID
const user = await this.userService.getUser({id: userID})
// get role of user
const userRole = user.role;
for (const role of requiredRoles) {
const result = this.RoleService.isAuthorized({
requiredRole: role,
currentRole: userRole,
});
if (result) {
return true;
}
}
}
}
finally, here is how I tried to import the UserModule in the GuardModule
import {forwardRef, Module} from "@nestjs/common";
import { RolesGuard } from "@src/guards/role.guard";
import { RoleService } from "@src/utils/roles.service";
import { UtilsModule } from "@src/utils/utils.module";
import { Reflector } from "@nestjs/core";
import {UserModule} from "@src/user/user.module";
@Module({
imports: [UtilsModule, // this causes the circular dependency : forwardRef(() => UserModule)],
providers: [RolesGuard, RoleService, Reflector],
exports: [RolesGuard, RoleService],
})
export class GuardModule {}
So far I believe I have the following solutions :
- Exclude the Guard from the
UserService - Make a new route in the controller and function in the service that uses a secret token and make a request to this route from the Guard
- Inject the User repository into the Guard to do that one request
to me the best solution so far would be the Injection of the repository in the Guard, but I still think there could be another solution that I didn't thought of.
Do you have any idea that could be cleaner / safer / ... ?