Skip to content

Feat/oauth support #304

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Mar 26, 2025
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