From 5d36a9879eb5db4e17c5a92c6f661abc7a9aadd0 Mon Sep 17 00:00:00 2001 From: Aleksey Konstantinov Date: Mon, 27 Feb 2023 15:19:44 +0300 Subject: [PATCH] Added TokenProvider class --- .semaphore/semaphore.yml | 9 ++-- package-lock.json | 4 +- package.json | 2 +- src/error.js | 23 +++++++++ src/index.js | 1 + src/token-provider.js | 108 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 src/token-provider.js diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 1b79be5..c136a28 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -13,7 +13,8 @@ blocks: - name: prepare commands: - checkout - - nvm use $(cat .nvmrc) # semaphore does not use node version from .nvmrc for some reason + - nvm install $(cat .nvmrc) + - nvm use - cache restore node-modules-$SEMAPHORE_GIT_BRANCH-$(checksum package-lock.json),node-modules-$SEMAPHORE_GIT_BRANCH,node-modules-master - npm ci - cache store node-modules-$SEMAPHORE_GIT_BRANCH-$(checksum package-lock.json) node_modules @@ -23,14 +24,16 @@ blocks: - name: lint commands: - checkout - - nvm use $(cat .nvmrc) # semaphore does not use node version from .nvmrc for some reason + - nvm install $(cat .nvmrc) + - nvm use - cache restore node-modules-$SEMAPHORE_GIT_BRANCH-$(checksum package-lock.json) - npm ci - npm run lint - name: test commands: - checkout - - nvm use $(cat .nvmrc) # semaphore does not use node version from .nvmrc for some reason + - nvm install $(cat .nvmrc) + - nvm use - cache restore node-modules-$SEMAPHORE_GIT_BRANCH-$(checksum package-lock.json) - npm ci - npm test diff --git a/package-lock.json b/package-lock.json index fc94f29..58c367c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ulms/api-clients", - "version": "5.13.1", + "version": "5.14.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ulms/api-clients", - "version": "5.13.1", + "version": "5.14.0", "license": "MIT", "dependencies": { "events": "3.3.0", diff --git a/package.json b/package.json index aae8508..61c5f08 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ulms/api-clients", - "version": "5.13.1", + "version": "5.14.0", "description": "JavaScript API clients for ULMS platform", "keywords": [], "homepage": "https://github.com/foxford/ulms-api-clients-js#readme", diff --git a/src/error.js b/src/error.js index b277b3a..a6a5eaa 100644 --- a/src/error.js +++ b/src/error.js @@ -57,3 +57,26 @@ export class PresenceError extends Error { return new PresenceError(errorType) } } + +export class TokenProviderError extends Error { + constructor(...args) { + super(...args) + + this.name = 'TokenProviderError' + } + + static get types() { + return { + UNAUTHENTICATED: 'UNAUTHENTICATED', + NETWORK_ERROR: 'NETWORK_ERROR', + UNKNOWN_ERROR: 'UNKNOWN_ERROR', + } + } + + static fromType(type) { + const errorType = + TokenProviderError.types[type] || TokenProviderError.types.UNKNOWN_ERROR + + return new TokenProviderError(errorType) + } +} diff --git a/src/index.js b/src/index.js index b072c3b..cec9091 100644 --- a/src/index.js +++ b/src/index.js @@ -10,4 +10,5 @@ export { default as Presence } from './presence' export { default as PresenceWS } from './presence-ws' export { default as Telemetry } from './telemetry' export { default as Tenant } from './tenant' +export { default as TokenProvider } from './token-provider' export { default as Portal } from './portal' diff --git a/src/token-provider.js b/src/token-provider.js new file mode 100644 index 0000000..fb88c3c --- /dev/null +++ b/src/token-provider.js @@ -0,0 +1,108 @@ +/* eslint-disable camelcase, promise/always-return */ +import { makeDeferred } from './common' +import { TokenProviderError } from './error' + +class TokenProvider { + constructor(baseUrl, httpClient) { + this.baseUrl = baseUrl + this.context = undefined + this.httpClient = httpClient + this.tokenData = undefined + this.tokenP = undefined + } + + setContext(context) { + this.context = context + } + + getToken() { + if (this.tokenP) { + return this.tokenP.promise + } + + const isTokenDataEmpty = !this.tokenData + const isAccessTokenExpired = isTokenDataEmpty + ? false + : Date.now() > this.tokenData.expires_ts + + if (isTokenDataEmpty || isAccessTokenExpired) { + this.tokenP = makeDeferred() + + this.fetchTokenData() + .then((response) => { + this.updateTokenData(response) + this.resolveAndReset() + }) + .catch((error) => { + this.rejectAndReset(error) + }) + + return this.tokenP.promise + } + + return Promise.resolve(this.tokenData.access_token) + } + + fetchTokenData() { + const qs = this.context ? `?context=${this.context}` : '' + const url = `${this.baseUrl}/api/user/ulms_token${qs}` + + return this.httpClient.post(url, undefined, { credentials: 'include' }) + } + + rejectAndReset(error) { + /* + * Errors + * + * - unrecoverable (нет смысла повторять запрос) + * 401 {"error":"Авторизуйтесь"} (когда разлогинился в админке или на портале) + * 401 {"error":"Войдите, пожалуйста, чтобы приступить к занятиям."} (когда указан неверный контекст для запроса) + * + * - network error + * error instanceof TypeError && error.message.startsWith('Failed to fetch') + * - отсутствует соединение с сетью + * - ошибка CORS (внезапно ответ 200, но ошибка по заголовкам) + * */ + let transformedError + + if ( + error instanceof TypeError && + error.message.startsWith('Failed to fetch') + ) { + transformedError = TokenProviderError.fromType( + TokenProviderError.types.NETWORK_ERROR + ) + } else if (error.error) { + transformedError = TokenProviderError.fromType( + TokenProviderError.types.UNAUTHENTICATED + ) + } else { + transformedError = TokenProviderError.fromType( + TokenProviderError.types.UNKNOWN_ERROR + ) + } + + this.tokenP.reject(transformedError) + + this.tokenP = undefined + } + + resolveAndReset() { + this.tokenP.resolve(this.tokenData.access_token) + + this.tokenP = undefined + } + + updateTokenData(updates) { + const { expires_in } = updates + const expires_ts = Date.now() + expires_in * 1e3 - 3e3 + + this.tokenData = { + ...this.tokenData, + ...updates, + expires_ts, + } + } +} + +export default TokenProvider