Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## [v1.20.0](https://github.com/contentstack/contentstack-management-javascript/tree/v1.20.0) (2025-04-01)
- Feature
- Added OAuth support
- Added the Unit Test cases and added sanity test case for OAuth
- Handle retry the requests that were pending due to token expiration
- Updated Axios Version

## [v1.19.6](https://github.com/contentstack/contentstack-management-javascript/tree/v1.19.6) (2025-03-24)
- Enhancement
- Added stack headers in global fields response
Expand Down
35 changes: 34 additions & 1 deletion lib/contentstackClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Organization } from './organization/index'
import cloneDeep from 'lodash/cloneDeep'
import { User } from './user/index'
import error from './core/contentstackError'
import OAuthHandler from './core/oauthHandler'

export default function contentstackClient ({ http }) {
/**
Expand Down Expand Up @@ -172,12 +173,44 @@ export default function contentstackClient ({ http }) {
}, error)
}

/**
* @description The oauth call is used to sign in to your Contentstack account and obtain the accesstoken.
* @memberof ContentstackClient
* @func oauth
* @param {Object} parameters - oauth parameters
* @prop {string} parameters.appId - appId of the application
* @prop {string} parameters.clientId - clientId of the application
* @prop {string} parameters.clientId - clientId of the application
* @prop {string} parameters.responseType - responseType
* @prop {string} parameters.scope - scope
* @prop {string} parameters.clientSecret - clientSecret of the application
* @returns {OAuthHandler} Instance of OAuthHandler
* @example
* import * as contentstack from '@contentstack/management'
* const client = contentstack.client()
*
* client.oauth({ appId: <appId>, clientId: <clientId>, redirectUri: <redirectUri>, clientSecret: <clientSecret>, responseType: <responseType>, scope: <scope> })
* .then(() => console.log('Logged in successfully'))
*
*/
function oauth (params = {}) {
http.defaults.versioningStrategy = 'path'
const appId = params.appId || '6400aa06db64de001a31c8a9'
const clientId = params.clientId || 'Ie0FEfTzlfAHL4xM'
const redirectUri = params.redirectUri || 'http://localhost:8184'
const responseType = params.responseType || 'code'
const scope = params.scope
const clientSecret = params.clientSecret
return new OAuthHandler(http, appId, clientId, redirectUri, clientSecret, responseType, scope)
}

return {
login: login,
logout: logout,
getUser: getUser,
stack: stack,
organization: organization,
axiosInstance: http
axiosInstance: http,
oauth
}
}
57 changes: 48 additions & 9 deletions lib/core/concurrency-queue.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Axios from 'axios'
import OAuthHandler from './oauthHandler'
const defaultConfig = {
maxRequests: 5,
retryLimit: 5,
Expand Down Expand Up @@ -75,17 +76,17 @@ export function ConcurrencyQueue ({ axios, config }) {
request.formdata = request.data
request.data = transformFormData(request)
}
request.retryCount = request.retryCount || 0
if (request.headers.authorization && request.headers.authorization !== undefined) {
if (this.config.authorization && this.config.authorization !== undefined) {
request.headers.authorization = this.config.authorization
request.authorization = this.config.authorization
if (axios?.oauth?.accessToken) {
const isTokenExpired = axios.oauth.tokenExpiryTime && Date.now() > axios.oauth.tokenExpiryTime
if (isTokenExpired) {
return refreshAccessToken().catch((error) => {
throw new Error('Failed to refresh access token: ' + error.message)
})
}
delete request.headers.authtoken
} else if (request.headers.authtoken && request.headers.authtoken !== undefined && this.config.authtoken && this.config.authtoken !== undefined) {
request.headers.authtoken = this.config.authtoken
request.authtoken = this.config.authtoken
}

request.retryCount = request?.retryCount || 0
setAuthorizationHeaders(request)
if (request.cancelToken === undefined) {
const source = Axios.CancelToken.source()
request.cancelToken = source.token
Expand All @@ -108,6 +109,44 @@ export function ConcurrencyQueue ({ axios, config }) {
})
}

const setAuthorizationHeaders = (request) => {
if (request.headers.authorization && request.headers.authorization !== undefined) {
if (this.config.authorization && this.config.authorization !== undefined) {
request.headers.authorization = this.config.authorization
request.authorization = this.config.authorization
}
delete request.headers.authtoken
} else if (request.headers.authtoken && request.headers.authtoken !== undefined && this.config.authtoken && this.config.authtoken !== undefined) {
request.headers.authtoken = this.config.authtoken
request.authtoken = this.config.authtoken
} else if (axios?.oauth?.accessToken) {
// If OAuth access token is available in axios instance
request.headers.authorization = `Bearer ${axios.oauth.accessToken}`
request.authorization = `Bearer ${axios.oauth.accessToken}`
delete request.headers.authtoken
}
}

// Refresh Access Token
const refreshAccessToken = async () => {
try {
// Try to refresh the token
await new OAuthHandler(axios).refreshAccessToken()
this.paused = false // Resume the request queue once the token is refreshed

// Retry the requests that were pending due to token expiration
this.running.forEach(({ request, resolve, reject }) => {
// Retry the request
axios(request).then(resolve).catch(reject)
})
this.running = [] // Clear the running queue after retrying requests
} catch (error) {
this.paused = false // stop queueing requests on failure
this.running.forEach(({ reject }) => reject(error)) // Reject all queued requests
this.running = [] // Clear the running queue
}
}

const delay = (time, isRefreshToken = false) => {
if (!this.paused) {
this.paused = true
Expand Down
22 changes: 22 additions & 0 deletions lib/core/contentstackHTTPClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,31 @@ export default function contentstackHttpClient (options) {
config.basePath = `/${config.basePath.split('/').filter(Boolean).join('/')}`
}
const baseURL = config.endpoint || `${protocol}://${hostname}:${port}${config.basePath}/{api-version}`
let uiHostName = hostname
let developerHubBaseUrl = hostname

if (uiHostName?.endsWith('io')) {
uiHostName = uiHostName.replace('io', 'com')
}

if (uiHostName?.startsWith('api')) {
uiHostName = uiHostName.replace('api', 'app')
}
const uiBaseUrl = config.endpoint || `${protocol}://${uiHostName}`

developerHubBaseUrl = developerHubBaseUrl
?.replace('api', 'developerhub-api')
.replace(/^dev\d+/, 'dev') // Replaces any 'dev1', 'dev2', etc. with 'dev'
.replace('io', 'com')
.replace(/^http/, '') // Removing `http` if already present
.replace(/^/, 'https://') // Adds 'https://' at the start if not already there

// set ui host name
const axiosOptions = {
// Axios
baseURL,
uiBaseUrl,
developerHubBaseUrl,
...config,
paramsSerializer: function (params) {
var query = params.query
Expand Down
Loading
Loading