Ionic Storage get auth token and add in HTTP header


#1

I have a HTTP wrapper class which sends all the request, I wanted to add Authorization header if user is logged in. Now problem is Storage returns a promise and I am not able to get the token from it.

Here is what I have right now.

// make request 
return this.http.request( new Request(requestOptions) )
        .map(this.responseHandler, this)
        .catch( this.handleError.bind(this) );

If I try to add header like this its not possible since Storage returns a Promise but HTTP returns an Observer ( I am just learning rxjs ).

this.storage.get('token').then( (token) => {
    // add auth header 
    requestOptions.headers.set("Authorization", 'Bearer ' + token)

    // make the call
    return this.http.request( new Request(requestOptions) )
        .map(this.responseHandler, this)
        .catch( this.handleError.bind(this) );
} );

Please help, I am not able to make it work.

Update

I have imported Observable like this

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';

and if wanted to do Observable.fromPromise(this.storage.get('token')) , it’s giving me error

Property 'fromPromise' does not exist on type 'typeof Observable'.

#2

Similar to how you’re importing map, I believe you may need to import fromPromise:

import 'rxjs/add/observable/fromPromise';

#3

Finally got it working, here is working code

convert promise to observable

import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/observable/fromPromise';

private getAuthHeaders() {
   return Observable.fromPromise(this.storage.get('me'));
}

Add header and make call

return this.getAuthHeaders().flatMap( api_token => {

      if( api_token ) {
        // add Authorization header
        requestOptions.headers.append('Authorization', 'Bearer ' + api_token);
      }
      
      // make request 
      return this.http.request( new Request(requestOptions) )
                .map(this.responseHandler, this)
                .catch( this.handleError.bind(this) );
});

#4

thanks, one question, how you applied syntax highlighting in the code block :confused:.
this forum editor should have a help button on how to use it also in toolbar.


#5

I just used triple backticks like github.


#6

Hey @saqueib ! Could you share your whole working HTTP wrapper? :wink:


#7

Here it is @HugoHeneault, I hope it helps you

import { Storage } from '@ionic/storage';
import { Injectable } from '@angular/core';
import { Network } from '@ionic-native/network';

import { Http, Response, RequestOptionsArgs, RequestMethod, Headers, Request } from '@angular/http';
import { LoadingController, Loading, AlertController, Alert, ToastController } from 'ionic-angular';
import { Observable } from 'rxjs/Observable';

import 'rxjs/add/observable/throw';
import 'rxjs/add/observable/fromPromise';

import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mergeMap';

@Injectable()
export class DataService {

  // Base Url for API
  private baseUrl: string = (location.hostname === "localhost") ? 'http://localhost:8000/api/' : 'http://app.com/api/';

  // Spinner for loader
  private spinner: Loading;

  // alert box 
  private alert: Alert;

  // default error popups
  private errorPopup: boolean = true;
  
  // cache flag
  public cached: boolean = true;

  /**
  * Default Headers used in all requests.
  */
  public headers: any = {
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  };

  constructor( 
    protected http: Http, 
    private loadingCtrl: LoadingController,
    private alertCtrl: AlertController,
    private toastCtrl: ToastController,
    private storage: Storage,
    private network: Network ) {
  
    // check device is online 
    console.log('Data Service Initialize');
  }

  /**
   * Show loading spinner 
   * 
   * @param loadingText Loading text
   */
  public withLoader( loadingText: string = 'Loading...' ) {
    this.spinner = this.loadingCtrl.create({ content: loadingText, dismissOnPageChange: true});
    return this;
  }

  /**
   * Dont show popups on error 
   */
  public noErrorPopup() {
    this.errorPopup = false;
    return this;    
  }

  /**
   * Ignore cache and make fresh refresh
   */
  public fresh() {
    this.cached = false;
    return this;    
  }

  /**
   * Make a `get` request 
   * 
   * @param url url to hit
   * @param options options for request
   */
  public get(url: string, options?: RequestOptionsArgs): Observable<any> {
    return this.request(RequestMethod.Get, url, null, options);
  }

  /**
   * Make a `post` request 
   * 
   * @param url url to hit
   * @param body json data to be sent as post
   * @param options options for request
   * @returns It returns an Observable from the request.
   */
  public post(url: string, body: any, options?: RequestOptionsArgs): Observable<any> {
    return this.request(RequestMethod.Post, url, body, options);
  }

  /**
   * Make a `put` request 
   * 
   * @param url url to hit
   * @param body json data to be sent as post
   * @param options options for request
   * @returns It returns an Observable from the request.
   */
  public put(url: string, body: any, options?: RequestOptionsArgs): Observable<any> {
    return this.request(RequestMethod.Put, url, body, options);
  }

  /**
   * Make a `delete` request 
   * 
   * @param url url to hit
   * @param options options for request
   * @returns It returns an Observable from the request.
   */
  public delete(url: string, options?: RequestOptionsArgs): Observable<any> {
    return this.request(RequestMethod.Delete, url, null, options);
  }

  /**
   * Make a `patch` request 
   * 
   * @param url url to hit
   * @param body json data to be sent as post
   * @param options options for request
   * @returns It returns an Observable from the request.
   */
  public patch(url: string, body: any, options?: RequestOptionsArgs): Observable<any> {
    return this.request(RequestMethod.Patch, url, body, options);
  }

  /**
   * Make a `head` request 
   * 
   * @param url url to hit
   * @param options options for request
   * @returns It returns an Observable from the request.
   */
  public head(url: string, options?: RequestOptionsArgs): Observable<any> {
    return this.request(RequestMethod.Head, url, null, options);
  }


  /**
   * This makes request and and shows loader if asked
   * 
   * @param method RequestMethod to be used 
   * @param url  URL for request
   * @param body body can be json object which will be stringify
   * @param options Any RequestOptionsArgs you want to pass
   * @returns It returns an Observable from the request.
   */  
  private request(method: RequestMethod, url: string, body?: any, options?: RequestOptionsArgs) {

    let requestOptions = Object.assign({
          method: method,
          url: this.generateUrl(url),
          body: JSON.stringify(body)
        }, this.generateOptions(options));

    // check network state, online get from api
    if(this.network.type === 'none') {
      
      let toast = this.toastCtrl.create({
            message: 'Internet connection appears offline.',
            duration: 5000,
            showCloseButton: true,
            dismissOnPageChange: true
          });

          toast.present();
    }
    
    // check if loader needs to be shown
    if(this.spinner) this.spinner.present();

    // check if its a fresh request, dont return cache 

    // Add General headers
    if (!requestOptions.headers) {
      requestOptions.headers = new Headers();
    }

    
    return this.getAuthToken().flatMap( data => {

          if( data && data.api_token ) {
            // add Authorization header
            requestOptions.headers.append('Authorization', 'Bearer ' + data.api_token);
          }
          
          // make request 
          return this.http.request( new Request(requestOptions) )
                .map(this.responseHandler, this)
                .catch( this.handleError.bind(this) );
        });
  }

  /**
   * Generate url for all requests. It uses baseUrl if url doesn't start with 'http'' or 'www'.
   * @param url     Url string
   * @returns       Generated url string
   */
  protected generateUrl(url: string): string {
    return !!(url && url.match(/^((?:http(|s):\/\/www\.)|(?:http:\/\/))/)) ? url : this.baseUrl + url;
  }

  /**
   * Handler which transform response to JavaScript format if response exists.
   * @param resp     Http response
   * @returns        Http response
   */
  protected responseHandler(resp: Response): Response {
    // rest spinner
    this.destroySpinner();

    if (!!resp.text()) {
      return resp.json();
    }

    return resp;
  }

  private destroySpinner() {
    if( this.spinner ) this.spinner.dismiss();
        this.spinner = undefined;
  }

  /**
   * Return auth token from promise
   */
  private getAuthToken() {
    return Observable.fromPromise(this.storage.get('me'));
  }

  /**
   * Handle error occured during request
   * @param error error observer
   */
  private handleError (error: Response | any) {
    // rest spinner
    this.destroySpinner();
    
    // Handle validation and Auth error 
    let errMsg: string = '';
    let alertTitle: string;

    if (error instanceof Response) {

      const body = error.json() || '';
      const err = body.error || JSON.stringify(body);

      // Unauthorized
      if( error.status >= 400 ) {

        // if validation error
        if( error.status === 422 ) {
          alertTitle = 'Invalid';
          // build a string from laravel validation error response
          for( let feild in body ) {
            let errFeild = body[feild];
            errMsg += `<p> ${errFeild[0]} </p>`
          }
        } else {
          alertTitle = error.statusText || 'Error';
          errMsg = err;
        }
      }
      
      // show the alert
      if (  this.errorPopup ) {
        // create new alert
        this.alert = this.alertCtrl.create({
            title: alertTitle, 
            subTitle: errMsg, 
            buttons: ['Dismiss']
        });

        if( error.status !== 0 ) {
          this.alert.present();
        }
      
        this.alert = undefined;
      }

    } else {
      errMsg = error.message ? error.message : error.toString();
      console.log(errMsg);
    }
    
    // handle error
    return Observable.throw(error);
  }

   /**
   * Handler which generate options for all requests from headers.
   * @param options   Request options arguments
   * @returns         Request options arguments
   */
  protected generateOptions(options: RequestOptionsArgs = {}): RequestOptionsArgs {
    if (!options.headers) {
      options.headers = new Headers();
    }

    Object.keys(this.headers)
      .filter((key) => this.headers.hasOwnProperty(key))
      .forEach((key) => {
        options.headers.append(key, this.headers[key]);
      });

    return options;
  }

}

#8

I think storing loading spinners in object properties is a mistake. It facilitates double-dispose and reuse bugs.


#9

Thanks a million @saqueib! I’ll just remove the loader part as @rapropos said!


#10

I’m trying to use async/await:

async authWithToken() {
    try {
      let response = await this.http.post('user.auth', {}).toPromise();
      return response.json().data;

    } catch (error) {
      await this.handleError(error);
    }
  }

But I got this.http.post(...).toPromise is not a function. Any ideas why? :frowning:


#11

Thanks a man! it will gonna help me a lot :yum:
its make me the code very simple .Try Sharing


#12

Why the request calling twice ?


#13

why get method calling twice help me please ?