Skip to content

Commit

Permalink
Merge pull request #8 from shoutem/feature/fix-import-without-configure
Browse files Browse the repository at this point in the history
Added support for importing without configure
  • Loading branch information
Domagoj Rukavina authored Aug 21, 2017
2 parents 250a47e + e39fe02 commit c761cd2
Show file tree
Hide file tree
Showing 13 changed files with 347 additions and 97 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ config: {
```

All required methods return a promise to enable reading of request or response body.
You should avoid reading the body directly on provided requests and responses and instead clone
them first. The library does not clone objects to avoid unnecessary overhead in cases where
You should avoid reading the body directly on provided requests and responses and instead **clone
them first.** The library does not clone objects to avoid unnecessary overhead in cases where
reading a body is not required to provide data.

To configure the interceptor you should import and call `configure` function. And when you obtain
Expand Down Expand Up @@ -117,6 +117,10 @@ to stop fetch interception.

Clears all tokens from interceptor.

`unload()`

Completely unloads the library and restores initial state.

`isResponseUnauthorized(response)`

Utility method which determines if given response should be considered unauthorized.
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@shoutem/fetch-token-intercept",
"version": "0.2.2",
"version": "0.3.0",
"description": "Fetch interceptor for managing refresh token flow.",
"main": "lib/index.js",
"files": [
Expand Down Expand Up @@ -57,5 +57,7 @@
"nock": "^8.0.0",
"sinon": "^1.17.4"
},
"dependencies": {}
"dependencies": {
"lodash": "^4.17.4"
}
}
11 changes: 10 additions & 1 deletion src/AccessTokenProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* a renewed version of access token at the moment. All subsequent requests will be chained
* to renewing fetch promise and resolved once the response is received.
*/
export default class AccessTokenProvider {
class AccessTokenProvider {
constructor(fetch, config) {
this.fetch = fetch;

Expand All @@ -29,6 +29,13 @@ export default class AccessTokenProvider {
this.handleError = this.handleError.bind(this);
}

/**
* Configures access token provider
*/
configure(config) {
this.config = { ...this.config, ...config };
}

/**
* Renews current access token with provided refresh token
*/
Expand Down Expand Up @@ -120,3 +127,5 @@ export default class AccessTokenProvider {
.catch(error => this.handleError(error, reject));
}
}

export default AccessTokenProvider;
148 changes: 98 additions & 50 deletions src/FetchInterceptor.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import isFunction from 'lodash/isFunction';
import {
ERROR_INVALID_CONFIG,
} from './const';
Expand All @@ -6,26 +7,70 @@ import TokenExpiredException from './services/TokenExpiredException';
import RetryCountExceededException from './services/RetryCountExceededException';
import AccessTokenProvider from './AccessTokenProvider';

/**
* Prepares signed request object which can be used for renewing access token
*
* @callback createAccessTokenRequest
* @param {string} refreshToken Refresh token used to sign the request
* @returns {Request} Signed request object which can be used to get access token
*/

/**
* Parses access token from access token response object
*
* @callback parseAccessToken
* @param {Response} response Response object with access token
* @returns {string} Access token parsed from response
*/

/**
* Checks whether interceptor will intercept this request or just let it pass through
*
* @callback shouldIntercept
* @param {Request} request Request object
* @returns {bool} A value indicating whether this request should be intercepted
*/

/**
* Checks whether provided response invalidates current access token
*
* @callback shouldInvalidateAccessToken
* @param {Response} response Response object
* @returns {bool} A value indicating whether token should be invalidated
*/

/**
* Adds authorization for intercepted requests
*
* @callback authorizeRequest
* @param {Request} request Request object being intercepted
* @param {string} accessToken Current access token
* @returns {Request} Authorized request object
*/

const getDefaultConfig = () => ({
fetchRetryCount: 1,
createAccessTokenRequest: null,
shouldIntercept: () => false,
shouldInvalidateAccessToken: () => false,
isResponseUnauthorized: http.isResponseUnauthorized,
parseAccessToken: null,
authorizeRequest: null,
onAccessTokenChange: null,
onResponse: null,
});

/**
* Provides a default implementation for intercepting fetch requests. It will try to resolve
* unauthorized responses by renewing the access token and repeating the initial request.
*/
export default class FetchInterceptor {
class FetchInterceptor {
constructor(fetch) {
// stores reference to vanilla fetch method
this.fetch = fetch;
this.accessTokenProvider = new AccessTokenProvider(this.fetch);

this.config = {
fetchRetryCount: 1,
createAccessTokenRequest: null,
shouldIntercept: () => true,
shouldInvalidateAccessToken: () => false,
isResponseUnauthorized: http.isResponseUnauthorized,
parseAccessToken: null,
authorizeRequest: null,
onAccessTokenChange: null,
onResponse: null,
};
this.config = getDefaultConfig();

this.intercept = this.intercept.bind(this);

Expand All @@ -48,37 +93,28 @@ export default class FetchInterceptor {
* Configures fetch interceptor with given config object. All required properties can optionally
* return a promise which will be resolved by fetch interceptor automatically.
*
* @param config
*
* (Required) Prepare fetch request for renewing new access token
* createAccessTokenRequest: (refreshToken) => request,
*
* (Required) Parses access token from access token response
* parseAccessToken: (response) => accessToken,
*
* (Required) Defines whether interceptor will intercept this request or just let it pass through
* shouldIntercept: (request) => boolean,
*
* (Required) Defines whether access token will be invalidated after this response
* shouldInvalidateAccessToken: (response) => boolean,
*
* (Required) Adds authorization for intercepted requests
* authorizeRequest: (request, accessToken) => authorizedRequest,
*
* Checks if response should be considered unauthorized (by default only 401 responses are
* considered unauthorized. Override this method if you need to trigger token renewal for
* other response statuses.
* isResponseUnauthorized: (response) => boolean,
*
* Number of retries after initial request was unauthorized
* fetchRetryCount: 1,
*
* Event invoked when access token has changed
* onAccessTokenChange: null,
*
* Event invoked when response is resolved
* onResponse: null,
*
* @param {object} config
* @param {createAccessTokenRequest} config.createAccessTokenRequest
* Prepare fetch request for renewing new access token
* @param {parseAccessToken} config.parseAccessToken
* Parses access token from access token response
* @param {shouldIntercept} config.shouldIntercept
* Defines whether interceptor will intercept this request or just let it pass through
* @param {shouldInvalidateAccessToken} config.shouldInvalidateAccessToken
* Defines whether access token will be invalidated after this response
* @param {authorizeRequest} config.authorizeRequest
* Adds authorization for intercepted requests
* @param {function} [config.isResponseUnauthorized=null]
* Checks if response should be considered unauthorized (by default only 401 responses are
* considered unauthorized. Override this method if you need to trigger token renewal for
* other response statuses.
* @param {number} [config.fetchRetryCount=1]
* Number of retries after initial request was unauthorized
* @param {number} [config.onAccessTokenChange=null]
* Event invoked when access token has changed
* @param {number} [config.onResponse=null]
* Event invoked when response is resolved
* </pre>
*/
configure(config) {
this.config = { ...this.config, ...config };
Expand All @@ -87,13 +123,13 @@ export default class FetchInterceptor {
throw new Error(ERROR_INVALID_CONFIG);
}

this.accessTokenProvider = new AccessTokenProvider(this.fetch, this.config);
this.accessTokenProvider.configure(this.config);
}

/**
* Authorizes fetch interceptor with given refresh token
* @param refreshToken
* @param accessToken
* @param {string} refreshToken Refresh token
* @param {string} accessToken Access token
*/
authorize(refreshToken, accessToken) {
this.accessTokenProvider.authorize(refreshToken, accessToken);
Expand All @@ -114,6 +150,15 @@ export default class FetchInterceptor {
this.accessTokenProvider.clear();
}

/**
* Clears current authorization and restores default configuration, e.g. interceptor
* will stop intercepting requests.
*/
unload() {
this.clear();
this.config = getDefaultConfig();
}

/**
* Main intercept method, you should chain this inside wrapped fetch call
* @param args Args initially provided to fetch method
Expand All @@ -124,10 +169,11 @@ export default class FetchInterceptor {
}

isConfigValid() {
return this.config.shouldIntercept &&
this.config.authorizeRequest &&
this.config.createAccessTokenRequest &&
this.config.parseAccessToken;
return this.config.shouldIntercept && isFunction(this.config.shouldIntercept) &&
this.config.authorizeRequest && isFunction(this.config.authorizeRequest) &&
this.config.isResponseUnauthorized && isFunction(this.config.isResponseUnauthorized) &&
this.config.createAccessTokenRequest && isFunction(this.config.createAccessTokenRequest) &&
this.config.parseAccessToken && isFunction(this.config.parseAccessToken);
}

resolveIntercept(resolve, reject, ...args) {
Expand Down Expand Up @@ -357,3 +403,5 @@ export default class FetchInterceptor {
throw new Error(error);
}
}

export default FetchInterceptor;
61 changes: 46 additions & 15 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import {
isReactNative,
isWorker,
isWeb,
isNode,
resolveEnvironment,
} from './services/environment';
import { isResponseUnauthorized } from './services/http';
import FetchInterceptor from './FetchInterceptor';

let interceptor = null;
let environment = null;

export function attach(env) {
if (!env.fetch) {
Expand All @@ -28,38 +26,71 @@ export function attach(env) {
env.fetch = fetchWrapper(env.fetch);
}

function init() {
if (isReactNative()) {
attach(global);
} else if (isWorker()) {
attach(self);
} else if (isWeb()) {
attach(window);
} else if (isNode()) {
attach(global);
} else {
function initialize() {
environment = resolveEnvironment();
if (!environment) {
throw new Error('Unsupported environment for fetch-token-intercept');
}

attach(environment);
}

/**
* Initializes and configures interceptor
* @param config Configuration object
* @see FetchInterceptor#configure
*/
export function configure(config) {
if (!interceptor) {
initialize();
}

interceptor.configure(config);
}

/**
* Initializes tokens which will be used by interceptor
* @param args
* @see FetchInterceptor#authorize
*/
export function authorize(...args) {
interceptor.authorize(...args);
}

/**
* Returns current set of tokens used by interceptor
* @returns {{accessToken: string, refreshToken: string}|*}
*/
export function getAuthorization() {
return interceptor.getAuthorization();
}

/**
* Clears authorization tokens from interceptor
*/
export function clear() {
return interceptor.clear();
}

/**
* Gets a value indicating whether interceptor is currently active
* @returns {boolean}
*/
export function isActive() {
return !!interceptor;
}

/**
* Removes interceptor and restores default behaviour
*/
export function unload() {
if (interceptor) {
interceptor.unload();
}
}

export {
isResponseUnauthorized,
};

init();
initialize();
2 changes: 1 addition & 1 deletion src/services/RetryCountExceededException.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default class RetryCountExceededException extends Error {
this.requestContext = requestContext;

// Use V8's native method if available, otherwise fallback
if ("captureStackTrace" in Error) {
if ('captureStackTrace' in Error) {
Error.captureStackTrace(this, RetryCountExceededException);
} else {
this.stack = (new Error()).stack;
Expand Down
2 changes: 1 addition & 1 deletion src/services/TokenExpiredException.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default class TokenExpiredException extends Error {
this.name = this.constructor.name;

// Use V8's native method if available, otherwise fallback
if ("captureStackTrace" in Error) {
if ('captureStackTrace' in Error) {
Error.captureStackTrace(this, TokenExpiredException);
} else {
this.stack = (new Error()).stack;
Expand Down
Loading

0 comments on commit c761cd2

Please sign in to comment.