Utilizing an Observable in an Angular 6 service to ensure proper synchronization with Okta token present in local storage

Within my application, I have implemented third-party authentication to facilitate user login and store a token in their local storage. To enhance the user experience, I am developing a service to cache profile information. This service utilizes the user's authentication token to invoke a backend method getUser(), which retrieves the user's profile data.

The challenge arises due to a slight delay between setting the token in local storage and the application relying on it for making backend calls during initialization.

export class UserService {
  private userProfileSubject = new BehaviorSubject<Enduser>(new Enduser());
  userProfile$ = this.userProfileSubject.asObservable();

  constructor(
    private _adService: AdService,
    private _authService: AuthnService) { }

  setUserProfile() {
    const username = this._authService.getUser();
    this.userProfile$ = this._adService.getUser(username).pipe( 
      first(),
      map(result => result[0]),
      publishReplay(1),
      refCount()
    );
    return this.userProfile$;
  }
}

This synchronous method validates the local storage token and returns the respective username.

public getUser(): string {
    const jwtHelper = new JwtHelperService()

    const token = localStorage.getItem(environment.JWT_TOKEN_NAME);
    if (!token || jwtHelper.isTokenExpired(token)) {
      return null;
    } else {
      const t = jwtHelper.decodeToken(token);
      return t.username;
    }
  }

Hence, this._authService.getUser(); must complete before being utilized in

this._adService.getUser(username)
.

To resolve this issue, it seems like making the getUser() method return an Observable and utilizing techniques like takeWhile or timer could be beneficial. Despite attempting these solutions diligently, challenges persist.

Any assistance would be greatly appreciated.

__

Edit:

The implementation below appears functional, but the utilization of timer feels somewhat inelegant. Therefore, exploring alternative approaches is desirable:

In user.service.ts:

  setUserProfile() {
    timer(100).pipe(
      concatMap(() => {
        const username = this._authService.getUser();
        return this._adService.getUser(username)
      }),
      map(res => res[0])
    ).subscribe(profile => {
      this.userProfileSubject.next(profile);
    });
  }

In app.component.ts ngOnInit

this._userService.setUserProfile();
    this._userService.userProfile$.pipe(
      map((user: Enduser) => this._userService.setUserPermissions(user)),
      takeUntil(this.ngUnsubscribe)
    ).subscribe();

Edit 2: Working Solution

The method isLoggedIn() pertains to setting local storage. Here, there is a deliberate wait for its completion before proceeding to fetch user profile details.

this._authService.isLoggedIn().pipe(
      concatMap(() => {
        const username = this._authService.getUser();
        return this._adService.getUser(username)
      }),
      map(res => res[0])
    ).subscribe(profile => {
      this.userProfileSubject.next(profile);
    });
  }

isLoggedIn:

isLoggedIn(state): Observable<boolean> {

    ...

    return this.http.get(url, {withCredentials: true}).pipe(
      map((res: any) => {
        const token = res.mdoc.token;

        if (token) {
          localStorage.setItem(environment.JWT_TOKEN_NAME, token);
          return true;
        } else {
          return false;
        }
      })
  }

Answer №1

Upon reviewing your code, it appears that the issue of waiting for this._authService.getUser() to complete may not be necessary. If this._authService.getUser() is synchronous as you mentioned, it will always finish before the next line executes.

Nevertheless, after examining your code, it seems clear what you intend to achieve:

  1. Retrieve a username from this._authService.getUser()
  2. Pass the username to this._adService.getUser()
  3. Wait for this._adService.getUser() to finish and then pass its value to the observable stream, userProfile$

To accomplish this, you do not require complex RxJS operators; your implementation can be straightforward:

export class UserService {
  private userProfileSubject = new BehaviorSubject<Enduser>(new Enduser());
  userProfile$ = this.userProfileSubject.asObservable();

  constructor(
    private _adService: AdService,
    private _authService: AuthnService
  ) {}

  setUserProfile() {
    const username = this._authService.getUser();

    this._adService.getUser(username).subscribe((userProfile: Enduser) => {
      this.userProfileSubject.next(userProfile);
    });
  }
}

Emitting to the userProfile$ stream as demonstrated above allows you to subscribe to it wherever needed in your application to access user profile information.

With this approach, you can effortlessly retrieve user profile data across your app by subscribing to the userProfile$ stream like so:

constructor(private _userService: UserService) {
  _userService.userProfile$.subscribe((userProfile: Enduser) => {
    console.log(userProfile);
  });
}

Answer №2

const usernameObs = of(this._authService.getUser());
return usernameObs.pipe(
   flatMap(username => {
    return this._adService.getUser(username).pipe( 
       first(),
       map(result => result[0]),
       publishReplay(1),
       refCount()
    );
}))

It may be possible to eliminate the nested pipe structure. While I can't verify it, here is a potentially cleaner alternative that should function as well:

const usernameObs = of(this._authService.getUser());
return usernameObs.pipe(
  flatMap(username => {
    return this._adService.getUser(username);
  }),
  first(),
  map(result => result[0]),
  publishReplay(1),
  refCount()
)

Answer №3

Here is how I have set up the implementation:

setUserProfile() {
    this.userProfile$ = this._authService.isLoggedIn(this.activatedRoute.snapshot).pipe(
      concatMap(() => {
        return this._adService.getUser(this._authService.getUser()).pipe(
          map(result => result[0]),
          publishReplay(1),
          refCount()
        );
      })
    )
    return this.userProfile$;
  }
}

_____

// Retrieving user data from _adService

  getUser(username: string): Observable<Enduser> {
    const usernameUrl = encodeURIComponent(username);
    return this.http.get(`${environment.API_URL}person/${usernameUrl}`).pipe(
      map((res: any) => res.data)
    );
  }

_____

// Retrieving user details from _authService

  public getUser(): string {
    const jwtHelper = new JwtHelperService()

    const token = localStorage.getItem(environment.JWT_TOKEN_NAME);
    if (!token || jwtHelper.isTokenExpired(token)) {
      return null;
    } else {
      const t = jwtHelper.decodeToken(token);
      return t.username;
    }
  }

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Organizing a mat-table by date does not properly arrange the rows

My API retrieves a list of records for me. I would like to display these records sorted by date, with the latest record appearing at the top. However, the TypeScript code I have written does not seem to be ordering my rows correctly. Can anyone assist me ...

What is the reason behind the warning about DOM element appearing when custom props are passed to a styled element in MUI?

Working on a project using mui v5 in React with Typescript. I am currently trying to style a div element but keep encountering this error message in the console: "Warning: React does not recognize the openFilterDrawer prop on a DOM element. If you in ...

Display an array depending on the value in Angular 2 when clicked

In my current Angular 2 project, I am dealing with a .json file structured like this: { "PropertyName": "Occupation", "DefaultPromptText": "occupation text", "ValuePromptText": { "WebDeveloper": "for web developer", "Administra ...

An action in redux-toolkit has detected the presence of a non-serializable value

When I download a file, I store it in the payload of the action in the store as a File type. This file will then undergo verification in the saga. const form = new FormData(); if (privateKey && privateKey instanceof Blob) { const blob = new Blo ...

Angular application using ngrx-store-localstorage does not retain data after a page refresh

Looking to incorporate ngrx into my authentication flow with the help of ngrx-store-localstorage for token persistence between browser sessions. After logging in, I can see the token value stored like this: {"token":{"token":"e5cb6515-149c-44df-88d1-4ff1 ...

Make sure the static variable is set up prior to injecting the provider

In our Angular6 application, we utilize a globalcontextServiceFactory to initialize the application before rendering views. This process involves subscribing to get configuration from a back-end endpoint and then using forkJoin to retrieve environment app ...

Encountering build errors with Angular 2 version 2.0.0-beta.9

I recently updated my Angular2 project in visual studio from version 2.0.0-beta.0 to version 2.0.0-beta.9 and encountered build errors. The first error message reads as follows: Cannot find name 'SetConstructor'. This issue is occurring with ...

Steps for linking HTTP requests in Angular 2 depending on the type of response

My attempt to create an api call from a remote server and then, if an error occurs, make another request from my local server is not working as expected. I am encountering errors and need help to determine if my approach is feasible. Here is the code snip ...

Utilizing Radio buttons to establish default values - a step-by-step guide

I am utilizing a Map to store the current state of my component. This component consists of three groups, each containing radio buttons. To initialize default values, I have created an array: const defaultOptions = [ { label: "Mark", value: & ...

Using setTimeout() and clearTimeout() alongside Promises in TypeScript with strict mode and all annotations included

Many examples of timer implementations using Promises in JavaScript seem overly complex to me. I believe a simpler approach could be taken. However, I am looking for a solution specifically tailored for TypeScript with the "strict": true setting and all ne ...

What is the process for updating information once the user has verified their email address on Supabase using Next.js

After a user signs up using a magic link, I want to update the profiles table in my database. Below is the code snippet I am currently using: Login.tsx import { useState } from "react"; import { supabase } from "../lib/initSupabase"; c ...

Acquiring information from a Service and saving it in a Child component - Angular version 11

Utilizing my service, I fetch API data for the child component. Initially, it retrieves the Id and other user data, displaying it in the console. ngOnInit(): void { this.commonService.userSetChange.subscribe( apiData => { this.getUserS ...

What steps can be taken to avoid an abundance of JS event handlers in React?

Issue A problem arises when an application needs to determine the inner size of the window. The recommended React pattern involves registering an event listener using a one-time effect hook. Despite appearing to add the event listener only once, multiple ...

Asynchronous waiting waits not for async await

I'm currently working on a function that loops through an array and updates the model for each ID, then adds the result to another array. This is the code snippet I have: async function getSortedAnimals() { var i = 0; var sortedAnimals = []; id ...

The module cannot be located due to an error: Package path ./dist/style.css is not being exported from the package

I am facing an issue with importing CSS from a public library built with Vite. When I try to import the CSS using: import 'rd-component/dist/style.css'; I encounter an error during the project build process: ERROR in ./src/page/photo/gen/GenPhot ...

Unable to retrieve this information using $http callback

I am currently working with angular 1.5 and typescript, but I am facing an issue where I cannot access the 'this' property from the callback returned by the $http promise. Whenever I try to access a private method from the callback, 'this&a ...

TS1261: The file name 'xxx' that is already included is different from the file name 'xxx' only in terms of casing

In my project, there is a file located at /app/client/modules/activity/pages/New/hooks.ts. The folder name is New, with the first letter capitalized. During the webpack build process, I encountered the following error: ERROR in /root/app/client/modules/ac ...

Encountering issue with POST operation in GraphQL on Angular application integrated with AWS Amplify and DynamoDB

I am in the process of developing a basic Angular application using AWS Amplify with a DynamoDB backend. To handle GraphQL API calls, I utilized the amplify add API command to generate the necessary code. My current objective is to populate a table with ...

What is the best method for converting an Object with 4 properties to an Object with only 3 properties?

I have a pair of objects: The first one is a role object with the following properties: Role { roleId: string; name: string; description: string; isModerator: string; } role = { roleId:"8e8be141-130d-4e5c-82d2-0a642d4b73e1", ...

Connecting extra parameters to an event listener

Scenario: I am facing a situation where my event handler is already receiving one parameter (an error object). However, I now need to pass an additional parameter when binding the event handler. I am aware of the bind() method, but I am concerned that it ...