Consider the following scenario (Angular v7):
Point 2 is key here, and looks like this:
@NgModule({
imports: [
...
OAuthModule.forRoot({
resourceServer: {
allowedUrls: [API_SERVER_URL], // <== we need to set the value that we loaded from the external endpoint (JSON) here
sendAccessToken: true
}
}),
...
],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule { }
What I've tried to far:
OAuthModule.forRoot()
is triggered before the APP_INITIALIZER
can download the external configuration JSON.main.ts
into the Angular environment variables, then bootstrap the AppModule. Also doesn't work due to the import { AppModule } from './app/app.module';
statement in main.ts
, which causes the AppModule to load and fire OAuthModule.forRoot()
before the external config is loaded (this comment confirms this behavior).main.ts
, so without the import
statement on top. This is the StackBlitz example given in that comment. It works, but 1) breaks lazy loading WARNING in Lazy routes discovery is not enabled.
and 2) doesn't work with AOT compiling. It does come very close to what I need.Curious to hear if someone is aware of another method to get external configuration loaded before the AppModule loads.
StackBlitz for option 3 (Load the AppModule dynamically): https://stackblitz.com/edit/angular-n8hdty
Angular documentation has a great chapter called NgModule FAQs which contains the following section:
What if two modules provide the same service?
...
If NgModule A provides a service for token 'X' and imports an NgModule B that also provides a service for token 'X', then NgModule A's service definition "wins".
In other words, you can override OAuthModuleConfig for your library in AppModule:
main.ts
(async () => {
const response = await fetch('https://api.myjson.com/bins/lf0ns');
const config = await response.json();
environment['allowedUrls'] = config.apiBaseURL;
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
})();
app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { OAuthModule, OAuthModuleConfig } from 'angular-oauth2-oidc';
import { HttpClientModule } from '@angular/common/http';
import { environment } from '../environments/environment';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpClientModule,
OAuthModule.forRoot(),
],
providers: [
{
provide: OAuthModuleConfig,
useFactory: () => ({
resourceServer: {
allowedUrls: [environment['allowedUrls']],
sendAccessToken: true
}
})
}
],
bootstrap: [AppComponent]
})
export class AppModule {}
Note that we should also use useFactory
instead of useValue
so we don't depend on when AppModule
is imported.
In addition to @yurzui's answer, if you try this in AOT (e.g. ng build --prod
), you will get
ERROR in Error during template compile of 'AppModule' Function expressions are not supported in decorators in 'AuthModule' 'AuthModule' contains the error at src\app\core\auth.module.ts(29,23) Consider changing the function expression into an exported function.
so we create an exported function for the factory:
app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { OAuthModule, OAuthModuleConfig } from 'angular-oauth2-oidc';
import { HttpClientModule } from '@angular/common/http';
import { environment } from '../environments/environment';
export function oAuthConfigFactory() : OAuthModuleConfig {
return {
resourceServer: {
allowedUrls: [environment.servers.apiServer],
sendAccessToken: true
}
}
}
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpClientModule,
OAuthModule.forRoot(),
],
providers: [
{
provide: OAuthModuleConfig,
useFactory: oAuthConfigFactory
}
],
bootstrap: [AppComponent]
})
export class AppModule {}
Another option here. @yurzui answer works, but it requires the use of useFactory
which make the code harder to understand.
useFactory
is required because Angular @NgModule
decorators will be executed as soon as the AppModule
is imported in main.ts
and so the configuration isn't loaded yet.
So I decided to load the configuration even before that by adding a script in the scripts section of angular.js
. Here's how:
src/config/load.js:
// This file is added to the scripts section of 'angular.json' so it can run before Angular bootstrap process.
// It responsability is to load app configuration from JSON files.
(() => {
const fetchSync = url => {
// The code below will log the following warning: "[Deprecation] Synchronous XMLHttpRequest on the main thread is deprecated because of its detrimental effects to the end user's experience. For more help, check https://xhr.spec.whatwg.org/.",
// but since we want the configuration to be set before Angular bootstrap process, we ignore this warning.
const xhr = new XMLHttpRequest();
xhr.open('GET', url, false);
xhr.send(null);
return JSON.parse(xhr.responseText);
};
// We attach the fetched configuration to the 'window' global variable to access it later from Angular.
window.configuration = {
...fetchSync('config/config.base.json'),
...fetchSync('config/config.local.json'),
};
})();
angular.json:
// ...
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
// ...
"assets": [
// ...
"src/config/config.base.json",
"src/config/config.local.json"
],
"scripts": ["src/config/load.js"],
// ...
src/config/configuration.ts:
import get from 'lodash/get';
export class Configuration {
// We get the configuration from the 'window.configuration' property which as been set earlier by 'config/load.js'.
private static value = (window as any).configuration;
/**
* Get configuration value.
* @param path The path of the configuration value. Use '.' for nested values.
* @param defaultValue The returned value if the given path doesn't exist.
* @example
* const baseUrl = Configuration.get<string>('apis.github.baseUrl');
*/
static get<T>(path: string, defaultValue?: T): T {
return get(Configuration.value, path, defaultValue);
}
}
Then you can use:
OAuthModule.forRoot({
resourceServer: {
allowedUrls: Configuration.get('allowedUrls')
sendAccessToken: true
}
}),
See this if you have problem with lodash.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With