Conditionally instantiate service class as a provider in NestJS - javascript

I have a controller called User and two service classes: UserAdminService and UserSuperAdminService.
When a user makes a request to any endpoint of the User controller, I want to check if the user making the request is an Admin or a Super Admin (based on the roles in the token) and instantiate the correct service (UserAdminService or UserSuperAdminService). Note that the two services implement the same UserService interface (just the internals of the methods that change a bit). How can I make this with NestJS?
What I tried:
user.module.ts
providers: [
{
provide: "UserService",
inject: [REQUEST],
useFactory: (request: Request) => UserServiceFactory(request)
}
],
user-service.factory.ts
export function UserServiceFactory(request: Request) {
const { realm_access } = JwtService.parseJwt(
request.headers["authorization"].split(' ')[1]
);
if (realm_access["roles"].includes(RolesEnum.SuperAdmin))
return UserSuperAdminService;
else
return UserAdminService;
}
user.controller.ts
constructor(
#Inject("UserService") private readonly userService: UserServiceInterface
) {}
One of the reasons my code is not working is because I am returning the classes and not the instantiated objects from the factory, but I want NestJS to resolve the services dependencies. Any ideas?

Rather than passing back the class to instantiate, which Nest doesn't handle, you could add the UserSuperAdminService and UserAdminService to the inject array, and pass back the instance that Nest then would create per request.
providers: [
{
provide: "UserService",
inject: [REQUEST, UserSuperAdminService, UserAdminService],
useFactory: (request: Request, superAdminService: UserSuperAdminService, adminService: UserAdminService) => UserServiceFactory(request, superAdminService, adminService)
}
...
]
export function UserServiceFactory(request: Request, superAdminService: UserSuperAdminService, adminService: UserAdminService) {
const { realm_access } = JwtService.parseJwt(
request.headers["authorization"].split(' ')[1]
);
if (realm_access["roles"].includes(RolesEnum.SuperAdmin))
return superAdminService;
else
return adminService;
}

Instead of trying to conditionally instantiate a service class you could create a global middleware to redirect the request to the appropriate controller e.g.
import { Injectable, NestMiddleware } from '#nestjs/common';
#Injectable()
export class AdminUserMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
const { realm_access } = JwtService.parseJwt(
req.headers["authorization"].split(' ')[1]
);
if (realm_access["roles"].includes(RolesEnum.SuperAdmin)) {
req.url = req.url.replace(/^\/, '/super-admin/');
}
next();
}
}
Then you can apply it to all routes in your app.module.ts
#Module({
imports: [HttpModule],
controllers: [UserAdminController, UserSuperAdminController]
providers: [UserSuperAdminService, UserAdminService]
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(AdminUserMiddleware)
.forRoutes('/');
}
}
and have the following controlers:
#Controller('/')
export class UserAdminController {
private readonly logger: Logger = new Logger(UserAdminController.name);
constructor(private readonly userAdminService: UserAdminService) {}
#Controller('/super-admin')
export class UserSuperAdminController {
private readonly logger: Logger = new Logger(UserSuperAdminController.name);
constructor(private readonly userSuperAdminService: UserSuperAdminService) {}
}
See the NestJS docs and this post for further details

Related

Inject correct service based on parameter

Let's assume I have a two modules which are exporting BService and CService where both of those services extends AService
So code looks like this:
abstract class AService {
public run() {}
}
#Injectable()
export class BService extends AService {}
#Injectable()
export class CService extends AService {}
#Module({
providers: [BService],
exports: [BService],
})
export class BModule {}
#Module({
providers: [CService],
exports: [CService],
})
export class CModule {}
#Injectable()
class AppService {
constructor(protected readonly service: AService) {}
public run(context: string) { // let's assume context may be B or C
this.service.run();
}
}
#Module({
imports: [CModule, BModule],
providers: [{
provide: AppService,
useFactory: () => {
return new AppService(); // how to use BService/CService depending on the context?
}
}]
})
export class AppModule {}
But the key is, I cannot use REQUEST (to inject it directly in useFactory) from #nestjs/core as I'm using this service in cron jobs and with the API call
I also don't think Factory pattern is useful there, I mean it would work but I want to do it correctly
I was thinking about property based injection.
But I'm not sure how to use it in my case
In my opinion, the factory approach is exactly what you need. You described that you need a different service based on the context which is a great for for the factory approach. Let's try this:
Create an injectable factory:
import { Injectable } from '#nestjs/common';
import { AService } from './AService';
import { BService } from './BService';
import { CService } from './CService';
#Injectable()
export class ServiceFactory {
public getService(context: string) : AService {
switch(context) {
case 'a': return new BService();
case 'b': return new CService();
default: throw new Error(`No service defined for the context: "${context}"`);
}
}
}
Now import that factory into your app module:
import { ServiceFactory } from './ServiceFactory';
import { AService } from './AService';
#Module({
providers: [AppService, ServiceFactory]
})
export class AppModule {}
Now your app service will get the factory as a dependency which will create the appropriate service based on the context:
import { ServiceFactory } from './ServiceFactory';
import { AService } from './AService';
#Injectable()
class AppService {
constructor(readonly serviceFactory: ServiceFactory) { }
public run(context: string) {
const service: AService = this.serviceFactory.getService(context);
service.run();
}
}
If the property is static (e.g. environment variable), you can use a custom provider to choose the proper instance. However, if the property is in someway dynamic, you cannot soley rely on nest's dependency injection as it instantiates the provider on startup (with the exception of REQUEST scope, which isn't an option for you).
Static Property
Create a custom provider that instantiates the needed implementation based on a static property (e.g. environment variable).
{
provide: AService,
useClass: process.ENV.useBService ? BService : CService,
}
Dynamic Property with Request-Scope
Let's assume we have two different implementations of a service:
#Injectable()
export class BService {
public count = 0;
run() {
this.count++;
return 'B';
}
}
#Injectable()
export class CService {
public count = 0;
run() {
this.count++;
return 'C';
}
}
When the sum of the count variables of both is even, the BService should be used; CService when it's odd. For this, we create a custom provider with request scope.
{
provide: 'MyService',
scope: Scope.REQUEST,
useFactory: (bService: BService, cService: CService) => {
if ((bService.count + cService.count) % 2 === 0) {
return bService;
} else {
return cService;
}
},
inject: [BService, CService],
},
If our controller now injects the MyService token (#Inject('MyService')) and exposes its run method via an endpoint it will return B C B ...
Dynamic Property with Default-Scope
As we want to use the default scope (Singleton!), the static instantiation of nest's dependency injection cannot be used. Instead you can use the delegate pattern to select the wanted instance in the root class (AService in your example).
Provide all services as they are:
providers: [AService, BService, CService]
Decide dynamically in your AService which implementation to use:
#Injectable()
export class AService {
constructor(private bService: BService, private cService: CService) {}
run(dynamicProperty) {
if (dynamicProperty === 'BService') {
return this.bService.run();
} else {
return this.cService.run();
}
}
}

Initialize Guard with value

Is it possible to initialize guard with a specifig value ?
For example the current example will not work:
#Module({
imports: [
CoreModule,
],
providers: [
{
provide: AuthGuard, // while using APP_GUARD works
useFactory: (configService: ConfigService) => {
return new AuthGuard(configService.get('some_key'));
},
inject: [ConfigService],
},
],
})
While using APP_GUARD for provide will initialise the guard with config value. So it works only for global scope, but not for #UseGuards(AuthGuard)
This doesn't work because guards are not registered as providers in a module. They get directly instantiated by the framework.
You can either use dependency injection in the guard:
#Injectable()
export class MyAuthGuard {
constructor(private readonly configService: ConfigService) {
// use the configService here
}
}
and
#UseGuards(MyAuthGuard)
or instantiate the guard yourself:
#UseGuards(new AuthGuard(configService.get('some_key')))
In the special case of the AuthGuard, you can set a defaultStrategy in the PassportModule. Then you can just use #UseGuards(AuthGuard())
PassportModule.register({ defaultStrategy: 'jwt'})
or async:
PassportModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({ defaultStrategy: configService.authStrategy}),
inject: [ConfigService],
})
Let's say you want your specific guard instance to perform differently depending on some input, basically be able to configure it. There is no option to consume this config from constructor(). Factory way might look like a bit bulky solution. But you're still able to utilise static methods to achieve wanted behaviour.
Example:
#Injectable()
class SomeController {
#Get()
#UseGuard(AuthGuard) // but how to pass smth inside AuthGuard?
public async doSomething() {}
}
Solution:
// [auth.guard.ts] file
import { UnauthorizedException, Injectable } from '#nestjs/common';
import type { CanActivate, ExecutionContext } from '#nestjs/common';
import type { GuardOptions, PatchedRequest } from './auth.types';
export interface GuardOptions {
allowAnonymous?: boolean,
allowExpired?: boolean,
}
#Injectable()
export class AuthGuard
implements CanActivate {
public options: GuardOptions = {};
public canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> {
// Guard logic
return true;
}
static configure(options: GuardOptions) {
const instance = new AuthGuard;
instance.options = options;
return instance;
}
}
// [someEntity.controller.ts] file
// imports...
#Injectable()
class SomeController {
#Get()
#UseGuard(AuthGuard.configure({ allowExpired: true })) // voila
public async doSomething() {}
}
Enjoy! Glory to Ukraine!
I would try ht less verbose approach and inject ConfigService directly into the AuthGuard in such a manner:
#Module({
imports: [
CoreModule,
],
providers: [
AuthGuard,
],
exports: [
AuthGuard,
],
})
#Injectable()
export default class AuthGuard {
constructor (protected readonly config: ConfigService) {
}
/*
...
*/
}

Getting User Data by using Guards (Roles, JWT)

The documentation is kinda thin here so I ran into a problem. I try to use Guards to secure Controller or it's Actions, so I gonna ask for the role of authenticated requests (by JWT). In my auth.guard.ts I ask for "request.user" but it's empty, so I can't check the users role. I don't know how to define "request.user". Here is my auth module and it's imports.
auth.controller.ts
import { Controller, Get, UseGuards } from '#nestjs/common';
import { AuthGuard } from '#nestjs/passport';
import { AuthService } from './auth.service';
import { RolesGuard } from './auth.guard';
#Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
#Get('token')
async createToken(): Promise<any> {
return await this.authService.signIn();
}
#Get('data')
#UseGuards(RolesGuard)
findAll() {
return { message: 'authed!' };
}
}
roles.guard.ts
Here user.request is empty, because I never define it. The documentation doesn't show how or where.
import { Injectable, CanActivate, ExecutionContext } from '#nestjs/common';
import { Reflector } from '#nestjs/core';
#Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user; // it's undefined
const hasRole = () =>
user.roles.some(role => !!roles.find(item => item === role));
return user && user.roles && hasRole();
}
}
auth.module.ts
import { Module } from '#nestjs/common';
import { AuthService } from './auth.service';
import { HttpStrategy } from './http.strategy';
import { UserModule } from './../user/user.module';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './jwt.strategy';
import { PassportModule } from '#nestjs/passport';
import { JwtModule } from '#nestjs/jwt';
#Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secretOrPrivateKey: 'secretKey',
signOptions: {
expiresIn: 3600,
},
}),
UserModule,
],
providers: [AuthService, HttpStrategy],
controllers: [AuthController],
})
export class AuthModule {}
auth.service.ts
import { Injectable } from '#nestjs/common';
import { UserService } from '../user/user.service';
import { JwtService } from '#nestjs/jwt';
#Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
private readonly jwtService: JwtService,
) {}
async signIn(): Promise<object> {
// In the real-world app you shouldn't expose this method publicly
// instead, return a token once you verify user credentials
const user: any = { email: 'user#email.com' };
const token: string = this.jwtService.sign(user);
return { token };
}
async validateUser(payload: any): Promise<any> {
// Validate if token passed along with HTTP request
// is associated with any registered account in the database
return await this.userService.findOneByEmail(payload.email);
}
}
jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { AuthService } from './auth.service';
import { PassportStrategy } from '#nestjs/passport';
import { Injectable, UnauthorizedException } from '#nestjs/common';
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'secretKey',
});
}
async validate(payload: any) {
const user = await this.authService.validateUser(payload);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
Documentation: https://docs.nestjs.com/guards
Thanks for any help.
Additionally to your RolesGuard you need to use an AuthGuard.
Standard
You can use the standard AuthGuard implementation which attaches the user object to the request. It throws a 401 error, when the user is unauthenticated.
#UseGuards(AuthGuard('jwt'))
Extension
If you need to write your own guard because you need different behavior, extend the original AuthGuard and override the methods you need to change (handleRequest in the example):
#Injectable()
export class MyAuthGuard extends AuthGuard('jwt') {
handleRequest(err, user, info: Error) {
// don't throw 401 error when unauthenticated
return user;
}
}
Why do this?
If you look at the source code of the AuthGuard you can see that it attaches the user to the request as a callback to the passport method. If you don't want to use/extend the AuthGuard, you will have to implement/copy the relevant parts.
const user = await passportFn(
type || this.options.defaultStrategy,
options,
// This is the callback passed to passport. handleRequest returns the user.
(err, info, user) => this.handleRequest(err, info, user)
);
// Then the user object is attached to the request
// under the default property 'user' which you can change by configuration.
request[options.property || defaultOptions.property] = user;
You can attach multiple guards together (#UseGuards(AuthGuard('jwt'), RolesGuard)) to pass the context between them. Then you will have access 'req.user' object inside 'RolesGuard'.
After I got the selected answer working (thank you), I found this option as well that you can add to the constructor that essentially does the same thing.
http://www.passportjs.org/docs/authorize/
Association in Verify Callback
One downside to the approach described above is that it requires two
instances of the same strategy and supporting routes.
To avoid this, set the strategy's passReqToCallback option to true.
With this option enabled, req will be passed as the first argument to
the verify callback.
#Injectable()
export class LocalStrategy extends PassportStrategy(Strategy, 'local') {
constructor(private authService: AuthService) {
super({
passReqToCallback: true
})
}
// rest of the strategy (validate)
}
Does it work if you use req.authInfo?
As long as you don't provide a custom callback to passport.authenticate method, the user data should be attached to the request object like this.
req.authInfo should be the object you returned in your validate method

Use fetched config in forRoot

I'm writing an angular app which uses #ngx-translate. With TranslateModule.forRoot(...) i provide a TranslateLoader:
#NgModule({
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient, ConfigService, LogService]
}
})
]
})
Also i have a ConfigService which loads an config.json utilizing APP_INITIALIZER.
The problem is, the TranslateLoader needs an url from the config. But forRoot() runs before APP_INITIALIZER which leads to ConfigService not having loaded the config and an empty url.
Is there another way to do this?
Currently i'm thinking about manually bootstrapping angular.
For anyone still looking at this, I found a way to load translations using the TranslateLoader provider after App Init. The ngx-translate lib allows you to override the current loader. So we can pass a new factory after the app has completed bootstrapping.
export function HttpLoaderFactory(handler: HttpBackend, valueAvailableAfterInit) {
const http = new HttpClient(handler);
return new TranslateHttpLoader(http, valueAvailableAfterInit, '.json');
}
export class AppModule {
constructor(
private translate: TranslateService,
private handler: HttpBackend
) {}
ngDoBootstrap() {
const valueAccessableAfterBootstrap = `I'll leave this to your use-case. For me it is an environment variable overwritten via ngOnInit in app.component`;
this.translate.currentLoader = HttpLoaderFactory(this.handler, valueAccessableAfterBootstrap); // replace loader
this.translate.reloadLang('en-US').pipe(take(1)).subscribe(); // reload translations with new loader
}
}
I think the following solution is simpler, since you don't need to reload the translations:
#NgModule({
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient, AppConfigService],
},
})
]
})
export function HttpLoaderFactory(http: HttpClient, appConfigService: AppConfigService) {
return new CustomTranslateHttpLoader(http, appConfigService, '', `?v=${environment.version}`);
}
export class CustomTranslateHttpLoader extends TranslateHttpLoader {
constructor(http: HttpClient,
private readonly appConfigService: AppConfigService,
prefix?: string,
suffix?: string) {
super(http, prefix, suffix);
}
override getTranslation(lang: string): Observable<Object> {
this.prefix = `${this.appConfigService.getConfig().apiUrl}api/translations/`;
return super.getTranslation(lang);
}
}
You can retrieve your config values after the app is initialized, as soon as getTranslation is called.
You could also resolve the translation service and make the http call to the endpoint. This only works if your services resolve the api url as well.
export class CustomTranslateHttpLoader extends TranslateLoader {
constructor(private readonly injector: Injector) {
super();
}
override getTranslation(lang: string): Observable<Object> {
const translationService = this.injector.get(TranslationsService);
return translationService.getAllTranslations(lang, environment.version)
.pipe(map(response => {
return response as Object;
}));
}
}

Angular 2 share Service between components in different routes

I have a behaviour in Angular 2 project that i don't know how to solve. I'm using webpack with Angular 2 2.3 (if this helps).
I have a complex project with structure like this:
- index.ts
- app.module.ts
- app.component.ts
- app.routes.ts
- services
- login.service.ts
- +innerapp
- inner.routes.ts
- inner.module.ts
- inner.component.ts
- inner.component.html
- services
-inner.service.ts
- insideinner
- insideinner.component.ts
- insideinner.component.html
- header
- header.component.ts
- header.component.html
- form
- form.component.ts
- form.component.html
When you execute shows login and then route to +innerapp. Inner.component.ts loads inner.services.ts and do a http call for data. A lot of data is moved from server and a let of BehaivorSubjects are initialized inside inner.service.ts.
All works fine, but in a moment user clicks button and loads form.component.ts with a big form. User fills form and click submit, in this moment inner.service is called to add data form. My surprise is inner.service haven't data, it's just initialised.
Code below.
//inner.routes.ts
export const routes = [
{ path: '', children: [
{ path: '', component: InnerComponent },
{ path: 'form', component: FormComponent },
]},
];
inner.module.ts
import { routes } from './inner.routes';
import { InnerComponent } from './inner.component';
import { FormComponent } from './insideinner/form/form.component';
// Services
import { InnerService } from './service/inner.service';
#NgModule({
declarations: [
// Components / Directives/ Pipes
InnerComponent,
FormComponent
],
imports: [
RouterModule.forChild(routes),
],
providers: [
InnerService
]
})
export class InnerModule {
public static routes = routes;
}
inner.component.ts:
#Component({
selector: 'inner',
templateUrl: './inner.component.html'
})
export class InnerComponent implements OnInit {
constructor ( private innerService: innerService ) {
this.innerService.fetchData()
.subscribe(
(response) => {
this.innerService.addData(response.json());
},
(error) => {
alert(error);
}
);
}
services/inner.services.ts
import { Injectable } from '#angular/core';
import { Observable, BehaviorSubject } from 'rxjs';
import { Headers, RequestOptions, Http, Response } from '#angular/http';
#Injectable()
export class InnerService {
// Observable string streams
public readonly data: Observable<string>;
// Observable string sources
private _data: BehaviorSubject<string> = new BehaviorSubject(null);
// private properties
private options: RequestOptions;
constructor( public http: Http ) {
this.data = this._user.asObservable();
// http standard values
let token = localStorage.getItem('token');
let cabs = new Headers({ Authorization: 'Bearer ' + token });
this.options = new RequestOptions({ headers: cabs });
}
// Service message commands
public addData (t: string) {
this._data.next(t);
}
public saveData(t: string) {
return this.http.post(blabla,
{
data: t
},
this.options
).map((res: Response) => {
this.addData(t);
return true;
}).catch(this.handleError);
}
private handleError (error: any) {
//code
}
public fetchData(): Observable<any> {
return this.http.get(blabla, this.options)
.map((res) => { return res.body })
.catch(this.handleError);
}
}
insideinner/form/form.component.ts
import { Component, OnInit, ViewEncapsulation } from '#angular/core';
// services & others
import { InnerService } from '../../services/inner.service';
#Component({
selector: 'add-members',
templateUrl: './form.component.html',
encapsulation: ViewEncapsulation.None
})
export class FormComponent implements OnInit {
constructor(
private fb: FormBuilder,
private innerService: InnerService
) {}
public ngOnInit() {
this.showForm = false;
}
public onSubmit(value: string) {
this.innerService.saveData(value); //Fail here, inner service are without data
}
public showAdd() {
this.showForm = true;
}
}
I read a lot of docs and read here similar problems, but solutions aren't working for me.
EDIT 2017-05-31
I think that is dupe question. I see that problem is related with lazyload in routes. I try this solution:
Angular 2 lazy loaded module - service not singleton
and this solution:
Angular 2 How to make singleton service available to lazy loaded modules
But no one work for me. I want to say that this project is in garbage and I began again with Angular 1.6 but I'm really interested in solve this problem to make future projects.

Categories

Resources