обновить токен для angular с использованием клиента graphql и apollo

Я пытаюсь настроить стратегию токена обновления для обновления JWT в angular 9 с помощью GraphQL и клиента apollo, когда мой первый запрос возвращает 401.

Я установил новый модуль angular для graphql, в котором я создаю свой apolloclient. Все отлично работает даже с аутентифицированными запросами, но мне нужно, чтобы моя обычная стратегия токенов обновления работала (заново создать и вернуть исходный запрос после завершения цикла токенов обновления). Я нашел только несколько ресурсов, чтобы помочь с этим, и я подошел очень близко - единственное, что мне не хватает, - это вернуть наблюдаемое из моего наблюдаемого токена обновления.

Вот код, который должен работать:

    import { NgModule } from '@angular/core';
import { HttpLinkModule, HttpLink } from 'apollo-angular-link-http';
import { AuthenticationService } from './authentication/services/authentication.service';
import { ApolloLink } from 'apollo-link';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloModule, APOLLO_OPTIONS } from 'apollo-angular';
import { onError } from 'apollo-link-error';

export function createApollo(httpLink: HttpLink, authenticationService: AuthenticationService) {

  const authLink = new ApolloLink((operation, forward) => {
    operation.setContext({
      headers: {
        Authorization: 'Bearer ' + localStorage.getItem('auth_token')
      }
    });
    return forward(operation);
  });

  const errorLink = onError(({ forward, graphQLErrors, networkError, operation }) => {
    if (graphQLErrors) {
      graphQLErrors.map(({ message, locations, path }) =>
        {
         if (message.toLowerCase() === 'unauthorized') {
          authenticationService.refreshToken().subscribe(() => {
            return forward(operation);
          });
         }
        }
      );
    }
  });

  return {
    link: errorLink.concat(authLink.concat(httpLink.create({ uri: 'http://localhost:3000/graphql' }))),
    cache: new InMemoryCache(),
  };
}


@NgModule({
  exports: [ApolloModule, HttpLinkModule],
  providers: [
    {
      provide: APOLLO_OPTIONS,
      useFactory: createApollo,
      deps: [HttpLink, AuthenticationService]
    }
  ]
})
export class GraphqlModule { }

Я знаю, что мой запрос работает во второй раз, потому что, если я выйду из системы с результатом пересылки (операции), наблюдаемой внутри моей подписки на authenticationService, я смогу увидеть результаты после первоначальной ошибки 401.

 if (message.toLowerCase() === 'unauthorized') {
  authenticationService.refreshToken().subscribe(() => {
    return forward(operation).subscribe(result => {
      console.log(result);
    });
  });
 }

приведенное выше показывает мне данные из исходного запроса, но они не передаются моему компоненту, который изначально вызывал graphql.

Я далек от эксперта в области наблюдаемых, но я думаю, что мне нужно сделать какую-то карту (плоскую карту, карту слияния и т.д.), чтобы этот возврат работал правильно, но я просто не знаю.

Любая помощь будет принята с благодарностью

TIA

ИЗМЕНИТЬ №1: это приближает меня, поскольку теперь он фактически подписывается на мой метод в AuthenticationService (я вижу результаты в tap())

    const errorLink = onError(({ forward, graphQLErrors, networkError, operation }) => {
    if (graphQLErrors) {
      if (graphQLErrors[0].message.toLowerCase() === 'unauthorized') {
        return authenticationService.refreshToken()
        .pipe(
          switchMap(() => forward(operation))
        );
      }
    }
  });

Теперь я вижу эту ошибку:

core.js:6210 ОШИБКА TypeError: вы предоставили недопустимый объект там, где ожидался поток. Вы можете предоставить Observable, Promise, Array или Iterable.

РЕДАКТИРОВАТЬ #2: Включая снимок экрана сигнатуры функции onError():

РЕДАКТИРОВАТЬ #3 Вот окончательное рабочее решение на случай, если кто-то еще столкнется с этим и понадобится для angular. Мне не нравится обновлять свой метод обслуживания, чтобы вернуть обещание, а затем преобразовывать это обещание в Observable, но, как обнаружил для меня @Andrei Gătej, этот Observable находится из другого пространства имен.

import { NgModule } from '@angular/core';
import { HttpLinkModule, HttpLink } from 'apollo-angular-link-http';
import { AuthenticationService } from './authentication/services/authentication.service';
import { ApolloLink } from 'apollo-link';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloModule, APOLLO_OPTIONS } from 'apollo-angular';
import { onError } from 'apollo-link-error';
import { Observable } from 'apollo-link';


export function createApollo(httpLink: HttpLink, authenticationService: AuthenticationService) {

  const authLink = new ApolloLink((operation, forward) => {
    operation.setContext({
      headers: {
        Authorization: 'Bearer ' + localStorage.getItem('auth_token')
      }
    });
    return forward(operation);
  });

  const errorLink = onError(({ forward, graphQLErrors, networkError, operation }) => {
    if (graphQLErrors) {
      if (graphQLErrors.some(x => x.message.toLowerCase() === 'unauthorized')) {
        return promiseToObservable(authenticationService.refreshToken().toPromise()).flatMap(() => forward(operation));
      }
    }
  });

  return {
    link: errorLink.concat(authLink.concat(httpLink.create({ uri: '/graphql' }))),
    cache: new InMemoryCache(),
  };
}

const promiseToObservable = (promise: Promise<any>) =>
    new Observable((subscriber: any) => {
      promise.then(
        value => {
          if (subscriber.closed) {
            return;
          }
          subscriber.next(value);
          subscriber.complete();
        },
        err => subscriber.error(err)
      );
    });


@NgModule({
  exports: [ApolloModule, HttpLinkModule],
  providers: [
    {
      provide: APOLLO_OPTIONS,
      useFactory: createApollo,
      deps: [HttpLink, AuthenticationService]
    }
  ]
})
export class GraphqlModule { }

2 ответа

Решение

Я не совсем знаком с GraphQL, но думаю, что это должно работать нормально:

if (message.toLowerCase() === 'unauthorized') {
return authenticationService.refreshToken()
  .pipe(
    switchMap(() => forward(operation))
  );
}

Кроме того, если вы хотите узнать, как mergeMap(а также concatMap), вы можете взглянуть на этот ответ.

switchMap сохраняет только одну активную внутреннюю наблюдаемую, и как только поступает внешнее значение, текущая внутренняя наблюдаемая будет отписана и будет создана новая на основе вновь поступившего внешнего значения и предоставленной функции.

Вот моя реализация для всех, кто увидит это в будущем

Модуль Garaphql:

      import { NgModule } from '@angular/core';
import { APOLLO_OPTIONS } from 'apollo-angular';
import {
  ApolloClientOptions,
  InMemoryCache,
  ApolloLink,
} from '@apollo/client/core';
import { HttpLink } from 'apollo-angular/http';
import { environment } from '../environments/environment';
import { UserService } from './shared/services/user.service';
import { onError } from '@apollo/client/link/error';
import { switchMap } from 'rxjs/operators';

const uri = environment.apiUrl;

let isRefreshToken = false;
let unHandledError = false;

export function createApollo(
  httpLink: HttpLink,
  userService: UserService
): ApolloClientOptions<any> {
  const auth = new ApolloLink((operation, forward) => {
    userService.user$.subscribe((res) => {
      setTokenInHeader(operation);
      isRefreshToken = false;
    });

    return forward(operation);
  });

  const errorHandler = onError(
    ({ forward, graphQLErrors, networkError, operation }): any => {
      if (graphQLErrors && !unHandledError) {
        if (
          graphQLErrors.some((x) =>
            x.message.toLowerCase().includes('unauthorized')
          )
        ) {
          isRefreshToken = true;

          return userService
            .refreshToken()
            .pipe(switchMap((res) => forward(operation)));
        } else {
          userService.logOut('Other Error');
        }

        unHandledError = true;
      } else {
        unHandledError = false;
      }
    }
  );

  const link = ApolloLink.from([errorHandler, auth, httpLink.create({ uri })]);

  return {
    link,
    cache: new InMemoryCache(),
    connectToDevTools: !environment.production,
  };
}

function setTokenInHeader(operation) {
  const tokenKey = isRefreshToken ? 'refreshToken' : 'token';
  const token = localStorage.getItem(tokenKey) || '';
  operation.setContext({
    headers: {
      token,
      Accept: 'charset=utf-8',
    },
  });
}

@NgModule({
  providers: [
    {
      provide: APOLLO_OPTIONS,
      useFactory: createApollo,
      deps: [HttpLink, UserService],
    },
  ],
})
export class GraphQLModule {}

UserService / AuthService:

      import { BehaviorSubject, Observable, throwError } from 'rxjs';
import { User, RefreshTokenGQL } from '../../../generated/graphql';
import jwt_decode from 'jwt-decode';
import { Injectable, Injector } from '@angular/core';
import { Router } from '@angular/router';
import { catchError, tap } from 'rxjs/operators';
import { AlertService } from './alert.service';

@Injectable({
  providedIn: 'root',
})
export class UserService {
  private userSubject: BehaviorSubject<User>;
  public user$: Observable<User>;

  constructor(
    private router: Router,
    private injector: Injector,
    private alert: AlertService
  ) {
    const token = localStorage.getItem('token');
    let user;
    if (token && token !== 'undefined') {
      try {
        user = jwt_decode(token);
      } catch (error) {
        console.log('error', error);
      }
    }
    this.userSubject = new BehaviorSubject<User>(user);
    this.user$ = this.userSubject.asObservable();
  }

  setToken(token?: string, refreshToken?: string) {
    let user;

    if (token) {
      user = jwt_decode(token);
      localStorage.setItem('token', token);
      localStorage.setItem('refreshToken', refreshToken);
    } else {
      localStorage.removeItem('token');
      localStorage.removeItem('refreshToken');
    }

    this.userSubject.next(user);
    return user;
  }

  logOut(msg?: string) {
    if (msg) {
      this.alert.addInfo('Logging out...', msg);
    }

    this.setToken();
    this.router.navigateByUrl('/auth/login');
  }

  getUser() {
    return this.userSubject.value;
  }

  refreshToken() {
    const refreshTokenMutation = this.injector.get<RefreshTokenGQL>(
      RefreshTokenGQL
    );

    return refreshTokenMutation.mutate().pipe(
      tap(({ data: { refreshToken: res } }) => {
        this.setToken(res.token, res.refreshToken);
      }),
      catchError((error) => {
        console.log('On Refresh Error: ', error);
        this.logOut('Session Expired, Log-in again');
        return throwError('Session Expired, Log-in again');
      })
    );
  }
}


Другие вопросы по тегам