Use fetched config in forRoot - javascript

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;
}));
}
}

Related

Conditionally instantiate service class as a provider in NestJS

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

Pass variable to module using forRoot

I've developed a library with shared components in Angular and I want to pass configuration there.
Everything is basically the same as in:
Pass config data using forRoot
Problem is, that I need to pass user to this module, which is fetched on start of application and saved in Redux state.
Is it possible to pass observable with user using forRoot while importing module? Maybe it's possible to pass something to this module 'later' with some lazy loading?
#select() private user$: Observable<User>
#NgModule({
imports: [
LibModule.forRoot({
user: user$
})
...
})
I've made it another way - by injecting my implementation of abstract service for getting settings. Code below:
Lib module declaration:
export interface LibSettings {
user: User;
url: string;
}
export abstract class AbstractLibSettingsService {
abstract getSettings(): LibSettings;
}
#NgModule({
declarations: [...],
imports: [...],
exports: [...]
})
export class LibModule {
static forRoot(implementationProvider: Provider): ModuleWithProviders {
return {
ngModule: LibModule,
providers: [
implementationProvider
]
}
}
}
Lib service, where I needed the user:
constructor(private settingsService: AbstractGlassLibSettingsService) {
}
In application that uses the lib, module declaration with import:
export const LIB_SETTINGS_PROVIDER: ClassProvider = {
provide: AbstractLibSettingsService,
useClass: LibSettingsService
};
#NgModule({
imports: [...
LibModule.forRoot(LIB_SETTINGS_PROVIDER)
],
...
})
Finally, the implementation of the service in application:
#Injectable()
export class LibSettingsService extends AbstractLibSettingsService {
#select() private user$: Observable<User>;
private user: User;
constructor() {
super();
this.user$.subscribe(user => this.user = user);
}
public getSettings(): GlassLibSettings {
...
}

Nest can't resolve dependencies of guard wrapped inside a decorator

I'm trying to inject a provider inside a guard that is wrapped in a decorator, but Nest is not being able to resolve dependencies, giving me the next error:
[ExceptionHandler] Nest can't resolve dependencies of the SecuredGuard (Reflector, ?). Please make sure that the argument at index [1] is available in the SecuredGuard context.
The main purpose of my approach is to avoid using two separate decorators like this:
#Controller()
export class AppController {
#Get()
#Secured('admin')
#UseGuards(SecuredGuard)
getHello(): string {
return this.appService.getHello();
}
}
And instead insert the #UseGuards(SecuredGuard) inside my custom decorator #Secured('admin') so it ends up like this:
#Controller()
export class AppController {
#Get()
#Secured('admin')
getHello(): string {
return this.appService.getHello();
}
}
This is how I'm implementing my custom decorator:
export function Secured(...roles: string[]){
const setMetadata = ReflectMetadata('roles', roles)
const setupGuard = UseGuards(SecuredGuard)
return (target: any, key?: string, descriptor?: any) => {
setMetadata(target, key, descriptor);
setupGuard(target, key, descriptor);
}
}
And this is my SecuredGuard, the AuthService is the dependency that couldn't be resolved:
#Injectable()
export class SecuredGuard implements CanActivate {
constructor(
private readonly _reflector: Reflector,
private readonly _authService: AuthService
) { }
async canActivate(context: ExecutionContext): Promise<boolean> {...}
}
Both secured.guard.ts and secured.decorator.ts are part of secured.module.ts
#Module({
imports: [
SecuredGuard,
AuthModule
],
exports: [
SecuredGuard
],
providers: [
AuthService
]
})
export class SecuredModule {}
Which is using the AuthService being exported from auth.module.ts
#Module({
controllers: [
AuthController
],
providers: [
AuthService
],
imports: [
EmailModule
],
exports: [
AuthService
]
})
export class AuthModule { }
And secured.module.ts is being imported by app.module.ts
#Module({
imports: [
SecuredModule
],
controllers: [
AppController
],
providers: [
AppService
],
})
export class AppModule { }
I don't know if I'm using the appropriate approach, or even if it's possible what I'm trying to do, any clues would be really appreciated!
In general, your solution seems to work, see this running example:
However, there are some mistakes in your module declarations:
1) In your AppModule, the AuthService is not available, since neither is the AuthModule imported directly or exported by the SecuredModule. That's why you're getting the error.
2) You don't have to declare your guards in any module, they will just be available globally. Only put modules in your imports array.
3) You're providing the AuthService multiple times, so you will have different instances of it. You should only provide it once and then only export (or re-export) your provider, but not provide it again.
4) ReflectMetadata was deprecated in v6; use SetMetadata instead.

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) {
}
/*
...
*/
}

How to inject $templateCache into angular hybrid APP_INITIALIZER hook?

I'm new to Angular5 and TypeScript, so it's very possible it's a simple thing I'm overlooking.
I have an Angular hybrid app that uses ngUpgrade to run AngularJS and Angular5 side-by-side. I'm trying to inject $templateCache into the OnAppInit function so that I can load all the AngularJS HTML templates before the app completely initializes. I'm getting the error "Cannot find name '$templateCacheService'" as indicated below. Is my syntax wrong or is this not possible?
I "upgrade" $templateCache in upgraded-providers.ts like this:
import { InjectionToken, Directive, ElementRef, Injector } from '#angular/core';
import { UpgradeComponent } from '#angular/upgrade/static';
export const $templateCacheService = new InjectionToken<any>('$templateCacheService');
export const $templateCacheServiceProvider = {
provide: $templateCacheService,
useFactory: (i: any) => i.get('$templateCache'),
deps: ['$injector']
};
Then in app.module.ts, I try to inject it into OnAppInit:
import { NgModule, APP_INITIALIZER, Inject } from '#angular/core';
import { BrowserModule } from '#angular/platform-browser';
import { BrowserAnimationsModule } from '#angular/platform-browser/animations';
import { MatCommonModule } from '#angular/material';
import { FlexLayoutModule } from '#angular/flex-layout';
import { HttpClientModule, HttpClient, HTTP_INTERCEPTORS } from '#angular/common/http';
import { downgradeInjectable, UpgradeModule, downgradeComponent } from '#angular/upgrade/static';
import { environment } from '../environments/environment';
import {
$templateCacheServiceProvider,
$templateCacheService
} from './upgraded-providers';
import { AppComponent } from './app.component';
import { GlobalVarsService } from './core/global-vars.service';
import { WinAuthInterceptor } from './core/interceptors/win-auth.interceptor';
declare var angular: any;
#NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule,
MatCommonModule,
FlexLayoutModule,
HttpClientModule,
UpgradeModule
],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: WinAuthInterceptor,
multi: true
},
{
provide: APP_INITIALIZER,
useFactory: OnAppInit,
multi: true,
deps: [GlobalVarsService, HttpClient, $templateCacheService]
},
GlobalVarsService,
$templateCacheServiceProvider
]
})
export class AppModule {
constructor(private upgrade: UpgradeModule, private http: HttpClient) { }
ngDoBootstrap() {
angular.module('app').factory('globalVars', downgradeInjectable(GlobalVarsService));
this.upgrade.bootstrap(document.body, ['app'], { strictDi: true });
}
}
////// THIS NEXT LINE GETS error TS2304: Cannot find name '$templateCacheService' /////
export function OnAppInit(globalVars: GlobalVarsService, http: HttpClient, $templateCache: $templateCacheService) {
return (): Promise<any> => {
return new Promise((resolve, reject) => {
http.get(environment.apiBase + '/api/meta/data').subscribe(x => {
globalVars.MetaData = x;
globalVars.VersionNumber = globalVars.MetaData.versionNumber;
globalVars.IsDebugBuild = globalVars.MetaData.isDebugBuild;
globalVars.User = globalVars.MetaData.user;
globalVars.ApiBase = environment.apiBase;
globalVars.Templates.forEach(template => {
$templateCache.put(template.Item1, template.Item2);
});
resolve();
});
});
};
}
This is TypeScript type error, it doesn't affect how the application works (as long as compilation errors are ignored).
templateCacheService is not a valid type here, because $templateCacheService is a variable (injection token), not a type or an interface.
Only Angular class constructors are annotated with types for DI. Since factory functions are annotated with deps property, types in function signature exist only to provide type safety. If it's not needed, types can be skipped:
export function OnAppInit(
globalVars: GlobalVarsService, http: HttpClient,
$templateCache
) { ... }
Otherwise proper types should be used. $templateCache is an object with get, put, etc methods. Appropriate types are provided with AngularJS #types/angular type definitions. It will be something like:
export function OnAppInit(
globalVars: GlobalVarsService, http: HttpClient,
$templateCache: ng.ITemplateCacheService
) { ... }

Categories

Resources