From 23af0de34aaf82aebeea901357dd32117674b2c9 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 10 Jan 2019 19:17:15 +0100 Subject: [PATCH 1/2] Simplified ShlinkApiCLient by using the new simplified authentication approach --- src/utils/services/ShlinkApiClient.js | 81 +++++---------------- test/utils/services/ShlinkApiClient.test.js | 68 +++++++++-------- 2 files changed, 56 insertions(+), 93 deletions(-) diff --git a/src/utils/services/ShlinkApiClient.js b/src/utils/services/ShlinkApiClient.js index f2439bba..72ddfca8 100644 --- a/src/utils/services/ShlinkApiClient.js +++ b/src/utils/services/ShlinkApiClient.js @@ -2,7 +2,6 @@ import qs from 'qs'; import { isEmpty, isNil, reject } from 'ramda'; const API_VERSION = '1'; -const STATUS_UNAUTHORIZED = 401; const buildRestUrl = (url) => url ? `${url}/rest/v${API_VERSION}` : ''; export default class ShlinkApiClient { @@ -10,98 +9,54 @@ export default class ShlinkApiClient { this.axios = axios; this._baseUrl = buildRestUrl(baseUrl); this._apiKey = apiKey || ''; - this._token = ''; } listShortUrls = (options = {}) => - this._performRequest('/short-codes', 'GET', options) - .then((resp) => resp.data.shortUrls) - .catch((e) => this._handleAuthError(e, this.listShortUrls, [ options ])); + this._performRequest('/short-urls', 'GET', options) + .then((resp) => resp.data.shortUrls); createShortUrl = (options) => { const filteredOptions = reject((value) => isEmpty(value) || isNil(value), options); - return this._performRequest('/short-codes', 'POST', {}, filteredOptions) - .then((resp) => resp.data) - .catch((e) => this._handleAuthError(e, this.createShortUrl, [ filteredOptions ])); + return this._performRequest('/short-urls', 'POST', {}, filteredOptions) + .then((resp) => resp.data); }; getShortUrlVisits = (shortCode, dates) => - this._performRequest(`/short-codes/${shortCode}/visits`, 'GET', dates) - .then((resp) => resp.data.visits.data) - .catch((e) => this._handleAuthError(e, this.getShortUrlVisits, [ shortCode, dates ])); + this._performRequest(`/short-urls/${shortCode}/visits`, 'GET', dates) + .then((resp) => resp.data.visits.data); getShortUrl = (shortCode) => - this._performRequest(`/short-codes/${shortCode}`, 'GET') - .then((resp) => resp.data) - .catch((e) => this._handleAuthError(e, this.getShortUrl, [ shortCode ])); + this._performRequest(`/short-urls/${shortCode}`, 'GET') + .then((resp) => resp.data); deleteShortUrl = (shortCode) => - this._performRequest(`/short-codes/${shortCode}`, 'DELETE') - .then(() => ({})) - .catch((e) => this._handleAuthError(e, this.deleteShortUrl, [ shortCode ])); + this._performRequest(`/short-urls/${shortCode}`, 'DELETE') + .then(() => ({})); updateShortUrlTags = (shortCode, tags) => - this._performRequest(`/short-codes/${shortCode}/tags`, 'PUT', {}, { tags }) - .then((resp) => resp.data.tags) - .catch((e) => this._handleAuthError(e, this.updateShortUrlTags, [ shortCode, tags ])); + this._performRequest(`/short-urls/${shortCode}/tags`, 'PUT', {}, { tags }) + .then((resp) => resp.data.tags); listTags = () => this._performRequest('/tags', 'GET') - .then((resp) => resp.data.tags.data) - .catch((e) => this._handleAuthError(e, this.listTags, [])); + .then((resp) => resp.data.tags.data); deleteTags = (tags) => this._performRequest('/tags', 'DELETE', { tags }) - .then(() => ({ tags })) - .catch((e) => this._handleAuthError(e, this.deleteTags, [ tags ])); + .then(() => ({ tags })); editTag = (oldName, newName) => this._performRequest('/tags', 'PUT', {}, { oldName, newName }) - .then(() => ({ oldName, newName })) - .catch((e) => this._handleAuthError(e, this.editTag, [ oldName, newName ])); + .then(() => ({ oldName, newName })); - _performRequest = async (url, method = 'GET', query = {}, body = {}) => { - if (isEmpty(this._token)) { - this._token = await this._authenticate(); - } - - return await this.axios({ + _performRequest = async (url, method = 'GET', query = {}, body = {}) => + await this.axios({ method, url: `${this._baseUrl}${url}`, - headers: { Authorization: `Bearer ${this._token}` }, + headers: { 'X-Api-Key': this._apiKey }, params: query, data: body, paramsSerializer: (params) => qs.stringify(params, { arrayFormat: 'brackets' }), - }).then((resp) => { - // Save new token - const { authorization = '' } = resp.headers; - - this._token = authorization.substr('Bearer '.length); - - return resp; }); - }; - - _authenticate = async () => { - const resp = await this.axios({ - method: 'POST', - url: `${this._baseUrl}/authenticate`, - data: { apiKey: this._apiKey }, - }); - - return resp.data.token; - }; - - _handleAuthError = (e, method, args) => { - // If auth failed, reset token to force it to be regenerated, and perform a new request - if (e.response.status === STATUS_UNAUTHORIZED) { - this._token = ''; - - return method(...args); - } - - // Otherwise, let caller handle the rejection - return Promise.reject(e); - }; } diff --git a/test/utils/services/ShlinkApiClient.test.js b/test/utils/services/ShlinkApiClient.test.js index f92b00f7..24f17858 100644 --- a/test/utils/services/ShlinkApiClient.test.js +++ b/test/utils/services/ShlinkApiClient.test.js @@ -3,26 +3,20 @@ import { head, last } from 'ramda'; import ShlinkApiClient from '../../../src/utils/services/ShlinkApiClient'; describe('ShlinkApiClient', () => { - const createAxiosMock = (extraData) => () => - Promise.resolve({ - headers: { authorization: 'Bearer abc123' }, - data: { token: 'abc123' }, - ...extraData, - }); - const createApiClient = (extraData) => - new ShlinkApiClient(createAxiosMock(extraData)); + const createAxiosMock = (data) => () => Promise.resolve(data); + const createApiClient = (data) => new ShlinkApiClient(createAxiosMock(data)); describe('listShortUrls', () => { it('properly returns short URLs list', async () => { const expectedList = [ 'foo', 'bar' ]; - const apiClient = createApiClient({ + const { listShortUrls } = createApiClient({ data: { shortUrls: expectedList, }, }); - const actualList = await apiClient.listShortUrls(); + const actualList = await listShortUrls(); expect(expectedList).toEqual(actualList); }); @@ -34,17 +28,17 @@ describe('ShlinkApiClient', () => { }; it('returns create short URL', async () => { - const apiClient = createApiClient({ data: shortUrl }); - const result = await apiClient.createShortUrl({}); + const { createShortUrl } = createApiClient({ data: shortUrl }); + const result = await createShortUrl({}); expect(result).toEqual(shortUrl); }); it('removes all empty options', async () => { const axiosSpy = sinon.spy(createAxiosMock({ data: shortUrl })); - const apiClient = new ShlinkApiClient(axiosSpy); + const { createShortUrl } = new ShlinkApiClient(axiosSpy); - await apiClient.createShortUrl( + await createShortUrl( { foo: 'bar', empty: undefined, anotherEmpty: null } ); const lastAxiosCall = last(axiosSpy.getCalls()); @@ -64,14 +58,14 @@ describe('ShlinkApiClient', () => { }, }, })); - const apiClient = new ShlinkApiClient(axiosSpy); + const { getShortUrlVisits } = new ShlinkApiClient(axiosSpy); - const actualVisits = await apiClient.getShortUrlVisits('abc123', {}); + const actualVisits = await getShortUrlVisits('abc123', {}); const lastAxiosCall = last(axiosSpy.getCalls()); const axiosArgs = head(lastAxiosCall.args); expect(expectedVisits).toEqual(actualVisits); - expect(axiosArgs.url).toContain('/short-codes/abc123/visits'); + expect(axiosArgs.url).toContain('/short-urls/abc123/visits'); expect(axiosArgs.method).toEqual('GET'); }); }); @@ -82,14 +76,14 @@ describe('ShlinkApiClient', () => { const axiosSpy = sinon.spy(createAxiosMock({ data: expectedShortUrl, })); - const apiClient = new ShlinkApiClient(axiosSpy); + const { getShortUrl } = new ShlinkApiClient(axiosSpy); - const result = await apiClient.getShortUrl('abc123'); + const result = await getShortUrl('abc123'); const lastAxiosCall = last(axiosSpy.getCalls()); const axiosArgs = head(lastAxiosCall.args); expect(expectedShortUrl).toEqual(result); - expect(axiosArgs.url).toContain('/short-codes/abc123'); + expect(axiosArgs.url).toContain('/short-urls/abc123'); expect(axiosArgs.method).toEqual('GET'); }); }); @@ -100,14 +94,14 @@ describe('ShlinkApiClient', () => { const axiosSpy = sinon.spy(createAxiosMock({ data: { tags: expectedTags }, })); - const apiClient = new ShlinkApiClient(axiosSpy); + const { updateShortUrlTags } = new ShlinkApiClient(axiosSpy); - const result = await apiClient.updateShortUrlTags('abc123', expectedTags); + const result = await updateShortUrlTags('abc123', expectedTags); const lastAxiosCall = last(axiosSpy.getCalls()); const axiosArgs = head(lastAxiosCall.args); expect(expectedTags).toEqual(result); - expect(axiosArgs.url).toContain('/short-codes/abc123/tags'); + expect(axiosArgs.url).toContain('/short-urls/abc123/tags'); expect(axiosArgs.method).toEqual('PUT'); }); }); @@ -120,9 +114,9 @@ describe('ShlinkApiClient', () => { tags: { data: expectedTags }, }, })); - const apiClient = new ShlinkApiClient(axiosSpy); + const { listTags } = new ShlinkApiClient(axiosSpy); - const result = await apiClient.listTags(); + const result = await listTags(); const lastAxiosCall = last(axiosSpy.getCalls()); const axiosArgs = head(lastAxiosCall.args); @@ -136,9 +130,9 @@ describe('ShlinkApiClient', () => { it('properly deletes provided tags', async () => { const tags = [ 'foo', 'bar' ]; const axiosSpy = sinon.spy(createAxiosMock({})); - const apiClient = new ShlinkApiClient(axiosSpy); + const { deleteTags } = new ShlinkApiClient(axiosSpy); - await apiClient.deleteTags(tags); + await deleteTags(tags); const lastAxiosCall = last(axiosSpy.getCalls()); const axiosArgs = head(lastAxiosCall.args); @@ -149,13 +143,13 @@ describe('ShlinkApiClient', () => { }); describe('editTag', () => { - it('properly deletes provided tags', async () => { + it('properly edits provided tag', async () => { const oldName = 'foo'; const newName = 'bar'; const axiosSpy = sinon.spy(createAxiosMock({})); - const apiClient = new ShlinkApiClient(axiosSpy); + const { editTag } = new ShlinkApiClient(axiosSpy); - await apiClient.editTag(oldName, newName); + await editTag(oldName, newName); const lastAxiosCall = last(axiosSpy.getCalls()); const axiosArgs = head(lastAxiosCall.args); @@ -164,4 +158,18 @@ describe('ShlinkApiClient', () => { expect(axiosArgs.data).toEqual({ oldName, newName }); }); }); + + describe('deleteShortUrl', () => { + it('properly deletes provided short URL', async () => { + const axiosSpy = sinon.spy(createAxiosMock({})); + const { deleteShortUrl } = new ShlinkApiClient(axiosSpy); + + await deleteShortUrl('abc123'); + const lastAxiosCall = last(axiosSpy.getCalls()); + const axiosArgs = head(lastAxiosCall.args); + + expect(axiosArgs.url).toContain('/short-urls/abc123'); + expect(axiosArgs.method).toEqual('DELETE'); + }); + }); }); From 811008ee1c89f3269440b3b8783e87feadac2270 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 10 Jan 2019 19:20:09 +0100 Subject: [PATCH 2/2] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a33da748..a298931d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), #### Removed * [#59](https://github.com/shlinkio/shlink-web-client/issues/59) Dropped support for old browsers. Internet explorer and dead browsers are no longer supported. +* [#97](https://github.com/shlinkio/shlink-web-client/issues/97) Dropped support for authentication via `Authorization` header with Bearer type and JWT, which will make this version no longer work with shlink earlier than v1.13.0. #### Fixed