Andrew Campbell
Andrew Campbell

Reputation: 18685

Load URL from Router for use in NgModule

I am setting up a blue/green deployment and am trying to change the redirectUri below based on the current url the user is viewing (redirectUri: this.router.url + '/callback',). I am receiving Uncaught TypeError: Cannot read property 'router' of undefined with the below configuration.

import { APP_BASE_HREF } from '@angular/common';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { CoreModule } from './@core/core.module';
import { AuthGuard } from './auth-guard.service';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { ThemeModule } from './@theme/theme.module';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { NbOAuth2AuthStrategy,
        NbAuthModule,
        NbOAuth2ResponseType,
        NbOAuth2GrantType,
        NbAuthOAuth2Token,
       } from '@nebular/auth';
import { OAuth2LoginComponent } from './auth/oauth2-login.component';
import { OAuth2CallbackComponent } from './auth/oauth2-callback.component';
import { environment } from '../environments/environment';
import { Router } from '@angular/router';

@NgModule({
  declarations: [AppComponent, OAuth2LoginComponent, OAuth2CallbackComponent ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    HttpClientModule,
    NgbModule.forRoot(),
    ThemeModule.forRoot(),
    CoreModule.forRoot(),
    NbAuthModule.forRoot({
      forms: {},
      strategies: [
        NbOAuth2AuthStrategy.setup({
          baseEndpoint: environment.authUrl,
          name: 'cognito',
          clientId: environment.clientId,
          authorize: {
            endpoint: '/oauth2/authorize',
            responseType: NbOAuth2ResponseType.CODE,
            scope: 'aws.cognito.signin.user.admin',
            redirectUri: this.router.url + '/callback',
          },
          redirect: {
            success: '/pages/dashboard',
          },
          token: {
            endpoint: '/oauth2/token',
            grantType: NbOAuth2GrantType.AUTHORIZATION_CODE,
            class: NbAuthOAuth2Token,
            redirectUri: this.router.url + '/callback',
          },
          refresh: {
            endpoint: 'refresh-token',
            grantType: NbOAuth2GrantType.REFRESH_TOKEN,
          },
        }),
       ],
    }),
    AppRoutingModule,
  ],
  bootstrap: [AppComponent],
  providers: [
    AuthGuard,
    { provide: APP_BASE_HREF, useValue: '/' },
  ],
})
export class AppModule {
  constructor(private router: Router) {}
}

I've also tried using redirectUri: window.location.origin + '/callback' which works locally, but is null when built for production.

Upvotes: 1

Views: 1016

Answers (4)

Sergey Mell
Sergey Mell

Reputation: 8050

Your solution won't work because, as Valeriy Katkov mentioned, the class level decorators are applied to the constructor, before any instance of the class is created. So, you won't be able to inject a router into a decorator.

In order to be able to inject the router, you need to move your implementation inside the class. It's possible by means of setOptions method of NbAuthStrategy instance, however, there are some issues, see for example here or here that need to be overcome. In order to make it work you should move the strategy configuration to a component which extends the NbAuthComponent (this is important) for example:

export class AppComponent extends NbAuthComponent {

  constructor(auth: NbAuthService,
              location: Location,
              private router: Router,
              authStrategy: NbPasswordAuthStrategy) {
    super(auth, location);

    authStrategy.setOptions({
      name: 'username',
      login: {
        alwaysFail: false,
        endpoint: 'test',
        method: 'post',
        requireValidToken: false,
        redirect: {
          success: this.router.url + '/success-callback',
          failure: this.location.path() + '/callback'
        },
        defaultErrors: ['Damn'],
        defaultMessages: ['Great'],
      },
    });
  }
}

Also I propose you to use this.location.path() instead of this.router.url because this.router.url will provide you not the url you are in, but the url of the compoment level. this.location.path() will give you the full path of the page you are in.

Here is a StackBlitz example with a working solution.

Please, let me know if smth remained unclear for you or requires some additional detailization.

Upvotes: 0

Valeriy Katkov
Valeriy Katkov

Reputation: 40702

Note that class level decorators are applied to the constructor, before any instance of the class is created. So the router property isn't available for the decorator. In the example this.router.url + '/callback' refers to the global this instead, it's strange that there is no compilation errors.

Regarding window.location, in the aot compilation mode, which is default for the prod builds, expressions in the decorator are executed by Angular compiler at compile time, so window.location isn't available there. Take a look at this GitHub issue: AOT replaces window.location object to null

As a workaround you can dynamically initialize the NbOAuth2AuthStrategy, like:

@NgModule({
  imports: [
    ...
    NbAuthModule.forRoot({
      strategies: [
        NbOAuth2AuthStrategy.setup({
          name: 'cognito'
        })
      ],
      ...
    })
  ],
  ...
})
export class AppModule {
  constructor(
    authService: NbAuthService, // force construction of the auth service
    oauthStrategy: NbOAuth2AuthStrategy
  ) {
    // window.location should be available here
    this.oauthStrategy.setOpitions({
      name: 'cognito',
      ...
    });
  }
}

As I found out, it's important to add the NbAuthService to the constructor arguments as well as NbOAuth2AuthStrategy. It looks like the service initializes the strategy during construction, so it should be constructed before the strategy initialization.

Also note that the setOptions() method completely overrides the options from the module decorator, so the whole strategy initialization should be moved from the decorator to the constructor.

I've also found this GitHub issue, which helped me to find the correct solution.

Upvotes: 2

Nahid Ali
Nahid Ali

Reputation: 646

In the code sample you provided, it seems that you are trying to access the router object this.router in @NgModule annotation before instantiation of class 'AppModule'.

The instance variables are not available in the Annotations. Angular uses [Dependency Injection][1] to provide object instance through constructor.

Looking to your case, though I am not much aware of the NbOAuth2AuthStrategy module, but you can look for options to configure the strategy of auth from within constructor after defining the module in import section of @NgModule annotation. Consider this code snippet, it may be helpful to you. Look for placeholders in code below marked by << >>


    NbAuthModule.forRoot({
        forms: {},
      strategies: [
        NbOAuth2AuthStrategy.setup({
          baseEndpoint: environment.authUrl,
          name: 'cognito',
          clientId: environment.clientId,
          authorize: {
            endpoint: '/oauth2/authorize',
            responseType: NbOAuth2ResponseType.CODE,
            scope: 'aws.cognito.signin.user.admin',
            redirectUri: '<<SET SOME DEFAULT URL>>',
          },
          redirect: {
            success: '/pages/dashboard',
          },
          token: {
            endpoint: '/oauth2/token',
            grantType: NbOAuth2GrantType.AUTHORIZATION_CODE,
            class: NbAuthOAuth2Token,
            redirectUri: '<<SET SOME DEFAULT URL>>',
          },
          refresh: {
            endpoint: 'refresh-token',
            grantType: NbOAuth2GrantType.REFRESH_TOKEN,
          },
        }),
       ...


    export class AppModule {
      constructor(private router: Router) {
        <<Reconfigure your NbOAuth2AuthStrategy>>
        NbOAuth2AuthStrategy.setup....
      }
    }


Hope the solution works for you.



  [1]: https://angular.io/guide/dependency-injection

Upvotes: 0

KLTR
KLTR

Reputation: 1344

If you want to do something like that you can use an injection token and create a factory function that returns the value you want. that will be run in the browser and you will see the value you want.

const REDIRECT_URI = new InjectionToken('REDIRECT_URI');

export function redirectUriFactory {
  return `${location.protocol}/${location.host}/callback`
}
    @NgModule(...)
    class MyModule {
      forRoot() {
        return {
          ngModule: MyModule,
          providers: [
            { provide: REDIRECT_URI, useFactory: redirectUriFactory }
          ]
        }
      }
    }

I did not test this, but InjectionToken with Factory is the way to go when it's coming to AOT

More info https://github.com/angular/angular-cli/issues/10957

Upvotes: 0

Related Questions