Search code examples
typescriptangular12

Display enum string initializer instead of the enum value


The title is pretty self explanatory. get role(): string doesn't return the string initializer. For ex. it returns "Administrator", instead of a "Head Administrator". Role["Administrator"] would do the thing, but IUser.role is actually Role | undefined, which is the problem and I can't simply do

get role(): string {
  return Role[this.authService.userInfo?.role];
}

because of that.

header.component.ts

import { Component } from '@angular/core';

import { AuthService } from '@core/services';
import { LayoutComponent } from '../layout.component';

@Component({
  selector: 'app-header',
  templateUrl: './header.component.html'
})
export class HeaderComponent {
  get username(): string {
    return this.authService.userInfo?.username || '';
  }

  get role(): string {
    return this.authService.userInfo?.role || '';
  }

  constructor(private authService: AuthService, public app: LayoutComponent) {}

  signOut(event: any) {
    this.authService.signOut();

    event.preventDefault();
  }
}

user.model.ts

export interface IUser extends IEntity {
  email: string;
  username: string;
  role: Role;
}

export enum Role {
  Administrator = 'Head Administrator',
  DepartmentAdministrator = 'Department Administrator',
  User = 'Super User'
}

auth.service.ts

import { Injectable, OnDestroy } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';

import { BehaviorSubject, Observable, of, Subscription } from 'rxjs';
import { delay, map, tap } from 'rxjs/operators';

import { environment } from '@env';
import { IAuthResponse, IUser } from '@core/models';
import { INITIAL_AUTH_STATE } from '@core/constants';

import { JwtService } from '../util';

@Injectable({
  providedIn: 'root'
})
export class AuthService implements OnDestroy {
  private readonly TOKEN_URL = `${environment.apiUrl}/Accounts/token`;

  private currentUserSubject = new BehaviorSubject<IAuthResponse>(INITIAL_AUTH_STATE);
  private timer!: Subscription;

  currentUser$: Observable<IAuthResponse> = this.currentUserSubject.asObservable();

  get userInfo(): IUser | null {
    const accessToken = this.currentUserValue?.accessToken;

    return accessToken ? this.jwtService.decodeToken<IUser>(accessToken) : null;
  }

  private get currentUserValue(): IAuthResponse | null {
    return this.currentUserSubject.value;
  }

  private get localStorageCurrentUser(): IAuthResponse {
    const localStorageUser = localStorage.getItem('currentUser');
    return localStorageUser ? JSON.parse(localStorageUser) : INITIAL_AUTH_STATE;
  }

  constructor(
    private httpClient: HttpClient,
    private router: Router,
    private jwtService: JwtService
  ) {
    this.currentUserSubject.next(this.localStorageCurrentUser);
    window.addEventListener('storage', this.storageEventListener.bind(this));
  }

  ngOnDestroy(): void {
    window.removeEventListener('storage', this.storageEventListener.bind(this));
  }

  signIn(username: string, password: string): Observable<IAuthResponse> {
    const TOKEN_URL: string = this.TOKEN_URL + '/create';

    return this.httpClient
      .post<IAuthResponse>(TOKEN_URL, {
        username,
        password
      })
      .pipe(
        map((res) => {
          if (res && res.accessToken) {
            this.setCurrentUser(res);
          }

          return res;
        })
      );
  }

  signOut(): void {
    this.clearCurrentUser();
    this.router.navigate(['auth']);
  }

  refreshToken(): Observable<IAuthResponse | null> {
    const refreshToken = this.currentUserValue?.refreshToken;
    if (!refreshToken) {
      this.clearCurrentUser();
      return of(null);
    }

    return this.httpClient.post<IAuthResponse>(`${this.TOKEN_URL}/refresh`, { refreshToken }).pipe(
      map((res) => {
        this.setCurrentUser(res);
        return res;
      })
    );
  }

  private setCurrentUser(user: IAuthResponse): void {
    this.currentUserSubject.next(user);
    this.setLocalStorage(user);
    this.startTokenTimer();
  }

  private clearCurrentUser(): void {
    this.currentUserSubject.next(INITIAL_AUTH_STATE);
    this.clearLocalStorage();
    this.stopTokenTimer();
  }

  private setLocalStorage(userState: IAuthResponse): void {
    localStorage.setItem('currentUser', JSON.stringify(userState));
    localStorage.setItem('login-event', 'login' + Math.random());
  }

  private clearLocalStorage(): void {
    localStorage.removeItem('currentUser');
    localStorage.setItem('logout-event', 'logout' + Math.random());
  }

  private getTokenRemainingTime(): number {
    const expiresAtUtc = this.currentUserValue?.expiresAtUtc;
    if (!expiresAtUtc) {
      return 0;
    }
    const expires = new Date(expiresAtUtc);
    return expires.getTime() - Date.now();
  }

  private startTokenTimer(): void {
    const timeout = this.getTokenRemainingTime();
    this.timer = of(true)
      .pipe(
        delay(timeout),
        tap(() => this.refreshToken().subscribe())
      )
      .subscribe();
  }

  private stopTokenTimer(): void {
    this.timer?.unsubscribe();
  }

  private storageEventListener(event: StorageEvent): void {
    if (event.storageArea === localStorage) {
      if (event.key === 'logout-event') {
        this.currentUserSubject.next(INITIAL_AUTH_STATE);
      }

      if (event.key === 'login-event') {
        location.reload();
      }
    }
  }
}


Solution

  • The code you have written is correct.

    I have experimented in an isolated playground and created 2 functions: first to return the string initializer and second to return enum value name.

    enum Role {
      Administrator = 'Head Administrator',
      DepartmentAdministrator = 'Department Administrator',
      User = 'Super User'
    }
    
    function getRoleName1(role: Role): string {
        return role
    }
    
    function getRoleName2(role: Role) {
        const roleEnum = Role as Record<string, string>
        for (const key of Object.keys(roleEnum)) {
            if (roleEnum[key] === role) {
                return key
            }
        }
    
        return undefined
    }
    
    console.log('1:', getRoleName1(Role.Administrator)) // Head Administrator
    console.log('2:', getRoleName2(Role.Administrator)) // Administrator
    

    The first function is doing what you need and I see that in your code you have it written in a similar way.

    I suspect that it doesn't work as expected because the enum doesn't match what your back-end actually returns, i.e. it might be returning 'Administrator' instead of 'Head Administrator'.