1
0
mirror of https://github.com/sasjs/adapter.git synced 2025-12-11 09:24:35 +00:00

fix(*): store CSRF tokens in Request Client

This commit is contained in:
Krishna Acondy
2021-01-24 18:23:18 +00:00
parent 3a9cd46e6e
commit e0d85f458b
7 changed files with 572 additions and 679 deletions

View File

@@ -1,14 +1,15 @@
import SASjs, { ServerType, SASjsConfig } from "@sasjs/adapter"; import SASjs, { SASjsConfig } from "@sasjs/adapter";
import { TestSuite } from "@sasjs/test-framework"; import { TestSuite } from "@sasjs/test-framework";
import { ServerType } from "@sasjs/utils/types";
const defaultConfig: SASjsConfig = { const defaultConfig: SASjsConfig = {
serverUrl: window.location.origin, serverUrl: window.location.origin,
pathSAS9: '/SASStoredProcess/do', pathSAS9: "/SASStoredProcess/do",
pathSASViya: '/SASJobExecution', pathSASViya: "/SASJobExecution",
appLoc: '/Public/seedapp', appLoc: "/Public/seedapp",
serverType: ServerType.SASViya, serverType: ServerType.SasViya,
debug: false, debug: false,
contextName: 'SAS Job Execution compute context', contextName: "SAS Job Execution compute context",
useComputeApi: false useComputeApi: false
}; };
@@ -17,7 +18,7 @@ const customConfig = {
pathSAS9: "sas9", pathSAS9: "sas9",
pathSASViya: "viya", pathSASViya: "viya",
appLoc: "/Public/seedapp", appLoc: "/Public/seedapp",
serverType: ServerType.SAS9, serverType: ServerType.Sas9,
debug: false debug: false
}; };
@@ -39,11 +40,12 @@ export const basicTests = (
}, },
{ {
title: "Multiple Log in attempts", title: "Multiple Log in attempts",
description: "Should fail on first attempt and should log the user in on second attempt", description:
"Should fail on first attempt and should log the user in on second attempt",
test: async () => { test: async () => {
await adapter.logOut() await adapter.logOut();
await adapter.logIn('invalid', 'invalid') await adapter.logIn("invalid", "invalid");
return adapter.logIn(userName, password) return adapter.logIn(userName, password);
}, },
assertion: (response: any) => assertion: (response: any) =>
response && response.isLoggedIn && response.userName === userName response && response.isLoggedIn && response.userName === userName

View File

@@ -1,11 +1,7 @@
import { import { Context, EditContextInput, ContextAllAttributes } from './types'
Context, import { isUrl } from './utils'
CsrfToken,
EditContextInput,
ContextAllAttributes
} from './types'
import { makeRequest, isUrl } from './utils'
import { prefixMessage } from '@sasjs/utils/error' import { prefixMessage } from '@sasjs/utils/error'
import { RequestClient } from './request/client'
export class ContextManager { export class ContextManager {
private defaultComputeContexts = [ private defaultComputeContexts = [
@@ -28,8 +24,6 @@ export class ContextManager {
'SAS Visual Forecasting launcher context' 'SAS Visual Forecasting launcher context'
] ]
private csrfToken: CsrfToken | null = null
get getDefaultComputeContexts() { get getDefaultComputeContexts() {
return this.defaultComputeContexts return this.defaultComputeContexts
} }
@@ -37,28 +31,19 @@ export class ContextManager {
return this.defaultLauncherContexts return this.defaultLauncherContexts
} }
constructor( constructor(private serverUrl: string, private requestClient: RequestClient) {
private serverUrl: string,
private setCsrfToken: (csrfToken: CsrfToken) => void
) {
if (serverUrl) isUrl(serverUrl) if (serverUrl) isUrl(serverUrl)
} }
public async getComputeContexts(accessToken?: string) { public async getComputeContexts(accessToken?: string) {
const headers: any = { const { result: contexts } = await this.requestClient
'Content-Type': 'application/json' .get<{ items: Context[] }>(
} `${this.serverUrl}/compute/contexts?limit=10000`,
accessToken
if (accessToken) { )
headers.Authorization = `Bearer ${accessToken}` .catch((err) => {
} throw prefixMessage(err, 'Error while getting compute contexts. ')
})
const { result: contexts } = await this.request<{ items: Context[] }>(
`${this.serverUrl}/compute/contexts?limit=10000`,
{ headers }
).catch((err) => {
throw prefixMessage(err, 'Error while getting compute contexts. ')
})
const contextsList = contexts && contexts.items ? contexts.items : [] const contextsList = contexts && contexts.items ? contexts.items : []
@@ -72,20 +57,14 @@ export class ContextManager {
} }
public async getLauncherContexts(accessToken?: string) { public async getLauncherContexts(accessToken?: string) {
const headers: any = { const { result: contexts } = await this.requestClient
'Content-Type': 'application/json' .get<{ items: Context[] }>(
} `${this.serverUrl}/launcher/contexts?limit=10000`,
accessToken
if (accessToken) { )
headers.Authorization = `Bearer ${accessToken}` .catch((err) => {
} throw prefixMessage(err, 'Error while getting launcher contexts. ')
})
const { result: contexts } = await this.request<{ items: Context[] }>(
`${this.serverUrl}/launcher/contexts?limit=10000`,
{ headers }
).catch((err) => {
throw prefixMessage(err, 'Error while getting launcher contexts. ')
})
const contextsList = contexts && contexts.items ? contexts.items : [] const contextsList = contexts && contexts.items ? contexts.items : []
@@ -183,18 +162,15 @@ export class ContextManager {
requestBody.environment = { autoExecLines } requestBody.environment = { autoExecLines }
} }
const createContextRequest: RequestInit = { const { result: context } = await this.requestClient
method: 'POST', .post<Context>(
headers, `${this.serverUrl}/compute/contexts`,
body: JSON.stringify(requestBody) JSON.stringify(requestBody),
} accessToken
)
const { result: context } = await this.request<Context>( .catch((err) => {
`${this.serverUrl}/compute/contexts`, throw prefixMessage(err, 'Error while creating compute context. ')
createContextRequest })
).catch((err) => {
throw prefixMessage(err, 'Error while creating compute context. ')
})
return context return context
} }
@@ -237,18 +213,15 @@ export class ContextManager {
launchType launchType
} }
const createContextRequest: RequestInit = { const { result: context } = await this.requestClient
method: 'POST', .post<Context>(
headers, `${this.serverUrl}/launcher/contexts`,
body: JSON.stringify(requestBody) JSON.stringify(requestBody),
} accessToken
)
const { result: context } = await this.request<Context>( .catch((err) => {
`${this.serverUrl}/launcher/contexts`, throw prefixMessage(err, 'Error while creating launcher context. ')
createContextRequest })
).catch((err) => {
throw prefixMessage(err, 'Error while creating launcher context. ')
})
return context return context
} }
@@ -267,14 +240,6 @@ export class ContextManager {
true true
) )
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
let originalContext let originalContext
originalContext = await this.getComputeContextByName( originalContext = await this.getComputeContextByName(
@@ -290,39 +255,33 @@ export class ContextManager {
) )
} }
const { result: context, etag } = await this.request<Context>( const { result: context, etag } = await this.requestClient
`${this.serverUrl}/compute/contexts/${originalContext.id}`, .get<Context>(
{ `${this.serverUrl}/compute/contexts/${originalContext.id}`,
headers accessToken
} )
).catch((err) => { .catch((err) => {
if (err && err.status === 404) { if (err && err.status === 404) {
throw new Error( throw new Error(
`The context '${contextName}' was not found on this server.` `The context '${contextName}' was not found on this server.`
) )
} }
throw err throw err
}) })
// An If-Match header with the value of the last ETag for the context // An If-Match header with the value of the last ETag for the context
// is required to be able to update it // is required to be able to update it
// https://developer.sas.com/apis/rest/Compute/#update-a-context-definition // https://developer.sas.com/apis/rest/Compute/#update-a-context-definition
headers['If-Match'] = etag return await this.requestClient.put<Context>(
`/compute/contexts/${context.id}`,
const updateContextRequest: RequestInit = { JSON.stringify({
method: 'PUT',
headers,
body: JSON.stringify({
...context, ...context,
...editedContext, ...editedContext,
attributes: { ...context.attributes, ...editedContext.attributes } attributes: { ...context.attributes, ...editedContext.attributes }
}) }),
} accessToken,
{ 'If-Match': etag }
return await this.request<Context>(
`${this.serverUrl}/compute/contexts/${context.id}`,
updateContextRequest
) )
} }
@@ -330,20 +289,17 @@ export class ContextManager {
contextName: string, contextName: string,
accessToken?: string accessToken?: string
): Promise<Context> { ): Promise<Context> {
const headers: any = { const { result: contexts } = await this.requestClient
'Content-Type': 'application/json' .get<{ items: Context[] }>(
} `${this.serverUrl}/compute/contexts?filter=eq(name, "${contextName}")`,
accessToken
if (accessToken) { )
headers.Authorization = `Bearer ${accessToken}` .catch((err) => {
} throw prefixMessage(
err,
const { result: contexts } = await this.request<{ items: Context[] }>( 'Error while getting compute context by name. '
`${this.serverUrl}/compute/contexts?filter=eq(name, "${contextName}")`, )
{ headers } })
).catch((err) => {
throw prefixMessage(err, 'Error while getting compute context by name. ')
})
if (!contexts || !(contexts.items && contexts.items.length)) { if (!contexts || !(contexts.items && contexts.items.length)) {
throw new Error( throw new Error(
@@ -358,20 +314,16 @@ export class ContextManager {
contextId: string, contextId: string,
accessToken?: string accessToken?: string
): Promise<ContextAllAttributes> { ): Promise<ContextAllAttributes> {
const headers: any = { const {
'Content-Type': 'application/json' result: context
} } = await this.requestClient
.get<ContextAllAttributes>(
if (accessToken) { `${this.serverUrl}/compute/contexts/${contextId}`,
headers.Authorization = `Bearer ${accessToken}` accessToken
} )
.catch((err) => {
const { result: context } = await this.request<ContextAllAttributes>( throw prefixMessage(err, 'Error while getting compute context by id. ')
`${this.serverUrl}/compute/contexts/${contextId}`, })
{ headers }
).catch((err) => {
throw prefixMessage(err, 'Error while getting compute context by id. ')
})
return context return context
} }
@@ -380,20 +332,14 @@ export class ContextManager {
executeScript: Function, executeScript: Function,
accessToken?: string accessToken?: string
) { ) {
const headers: any = { const { result: contexts } = await this.requestClient
'Content-Type': 'application/json' .get<{ items: Context[] }>(
} `${this.serverUrl}/compute/contexts?limit=10000`,
accessToken
if (accessToken) { )
headers.Authorization = `Bearer ${accessToken}` .catch((err) => {
} throw prefixMessage(err, 'Error while fetching compute contexts.')
})
const { result: contexts } = await this.request<{ items: Context[] }>(
`${this.serverUrl}/compute/contexts?limit=10000`,
{ headers }
).catch((err) => {
throw prefixMessage(err, 'Error while fetching compute contexts.')
})
const contextsList = contexts.items || [] const contextsList = contexts.items || []
const executableContexts: any[] = [] const executableContexts: any[] = []
@@ -470,14 +416,9 @@ export class ContextManager {
const context = await this.getComputeContextByName(contextName, accessToken) const context = await this.getComputeContextByName(contextName, accessToken)
const deleteContextRequest: RequestInit = { return await this.requestClient.delete<Context>(
method: 'DELETE',
headers
}
return await this.request<Context>(
`${this.serverUrl}/compute/contexts/${context.id}`, `${this.serverUrl}/compute/contexts/${context.id}`,
deleteContextRequest accessToken
) )
} }
@@ -485,34 +426,6 @@ export class ContextManager {
// TODO: implement deleteLauncherContext method // TODO: implement deleteLauncherContext method
private async request<T>(
url: string,
options: RequestInit,
contentType: 'text' | 'json' = 'json'
) {
if (this.csrfToken) {
options.headers = {
...options.headers,
[this.csrfToken.headerName]: this.csrfToken.value
}
}
return await makeRequest<T>(
url,
options,
(token) => {
this.csrfToken = token
this.setCsrfToken(token)
},
contentType
).catch((err) => {
throw prefixMessage(
err,
'Error while making request in Context Manager. '
)
})
}
private validateContextName(name: string) { private validateContextName(name: string) {
if (!name) throw new Error('Context name is required.') if (!name) throw new Error('Context name is required.')
} }

View File

@@ -1,116 +1,63 @@
import { needsRetry, isUrl } from './utils' import { isUrl } from './utils'
import { CsrfToken } from './types/CsrfToken'
import { UploadFile } from './types/UploadFile' import { UploadFile } from './types/UploadFile'
import { ErrorResponse } from './types' import { ErrorResponse } from './types'
import axios, { AxiosInstance } from 'axios' import { RequestClient } from './request/client'
import { isLogInRequired } from './auth'
const requestRetryLimit = 5
export class FileUploader { export class FileUploader {
private httpClient: AxiosInstance
constructor( constructor(
private appLoc: string, private appLoc: string,
serverUrl: string, serverUrl: string,
private jobsPath: string, private jobsPath: string,
private setCsrfTokenWeb: any, private requestClient: RequestClient
private csrfToken: CsrfToken | null = null
) { ) {
if (serverUrl) isUrl(serverUrl) if (serverUrl) isUrl(serverUrl)
this.httpClient = axios.create({ baseURL: serverUrl })
} }
private retryCount = 0
public uploadFile(sasJob: string, files: UploadFile[], params: any) { public uploadFile(sasJob: string, files: UploadFile[], params: any) {
return new Promise((resolve, reject) => { if (files?.length < 1)
if (files?.length < 1) return Promise.reject(
reject(new ErrorResponse('At least one file must be provided.')) new ErrorResponse('At least one file must be provided.')
if (!sasJob || sasJob === '') )
reject(new ErrorResponse('sasJob must be provided.')) if (!sasJob || sasJob === '')
return Promise.reject(new ErrorResponse('sasJob must be provided.'))
let paramsString = '' let paramsString = ''
for (let param in params) { for (let param in params) {
if (params.hasOwnProperty(param)) { if (params.hasOwnProperty(param)) {
paramsString += `&${param}=${params[param]}` paramsString += `&${param}=${params[param]}`
}
} }
}
const program = this.appLoc const program = this.appLoc
? this.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '') ? this.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
: sasJob : sasJob
const uploadUrl = `${this.jobsPath}/?${ const uploadUrl = `${this.jobsPath}/?${
'_program=' + program '_program=' + program
}${paramsString}` }${paramsString}`
const headers = { const formData = new FormData()
'cache-control': 'no-cache'
}
const formData = new FormData() for (let file of files) {
formData.append('file', file.file, file.fileName)
}
for (let file of files) { const csrfToken = this.requestClient.getCsrfToken('file')
formData.append('file', file.file, file.fileName) if (csrfToken) formData.append('_csrf', csrfToken.value)
}
if (this.csrfToken) formData.append('_csrf', this.csrfToken.value) const headers = {
'cache-control': 'no-cache',
Accept: '*/*',
'Content-Type': 'text/plain'
}
this.httpClient return this.requestClient
.post(uploadUrl, formData, { responseType: 'text', headers }) .post(uploadUrl, formData, undefined, headers)
.then(async (response) => { .then((res) => res.result)
if (response.status !== 200) { .catch((err: Error) => {
if (response.status === 403) { return Promise.reject(
const tokenHeader = response.headers.get('X-CSRF-HEADER') new ErrorResponse('File upload request failed', err)
)
if (tokenHeader) { })
const token = response.headers.get(tokenHeader)
this.csrfToken = {
headerName: tokenHeader,
value: token || ''
}
this.setCsrfTokenWeb(this.csrfToken)
}
}
}
return response.data
})
.then((responseText) => {
if (isLogInRequired(responseText))
reject(new ErrorResponse('You must be logged in to upload a file.'))
if (needsRetry(responseText)) {
if (this.retryCount < requestRetryLimit) {
this.retryCount++
this.uploadFile(sasJob, files, params).then(
(res: any) => resolve(res),
(err: any) => reject(err)
)
} else {
this.retryCount = 0
reject(responseText)
}
} else {
this.retryCount = 0
try {
resolve(JSON.parse(responseText))
} catch (e) {
reject(
new ErrorResponse(
'Error while parsing json from upload response.',
e
)
)
}
}
})
.catch((err: any) => {
reject(new ErrorResponse('Upload request failed.', err))
})
})
} }
} }

View File

@@ -6,7 +6,6 @@ import {
Context, Context,
ContextAllAttributes, ContextAllAttributes,
Folder, Folder,
CsrfToken,
EditContextInput, EditContextInput,
JobDefinition, JobDefinition,
PollOptions PollOptions
@@ -16,35 +15,34 @@ import { SessionManager } from './SessionManager'
import { ContextManager } from './ContextManager' import { ContextManager } from './ContextManager'
import { timestampToYYYYMMDDHHMMSS } from '@sasjs/utils/time' import { timestampToYYYYMMDDHHMMSS } from '@sasjs/utils/time'
import { Logger, LogLevel } from '@sasjs/utils/logger' import { Logger, LogLevel } from '@sasjs/utils/logger'
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired' import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired'
import { parseAndSubmitAuthorizeForm } from './auth' import { parseAndSubmitAuthorizeForm } from './auth'
import { RequestClient } from './request/client'
/** /**
* A client for interfacing with the SAS Viya REST API. * A client for interfacing with the SAS Viya REST API.
* *
*/ */
export class SASViyaApiClient { export class SASViyaApiClient {
private httpClient: AxiosInstance
constructor( constructor(
private serverUrl: string, private serverUrl: string,
private rootFolderName: string, private rootFolderName: string,
private contextName: string, private contextName: string,
private setCsrfToken: (csrfToken: CsrfToken) => void private requestClient: RequestClient
) { ) {
if (serverUrl) isUrl(serverUrl) if (serverUrl) isUrl(serverUrl)
this.httpClient = axios.create({ baseURL: serverUrl })
} }
private csrfToken: CsrfToken | null = null
private fileUploadCsrfToken: CsrfToken | null = null
private _debug = false private _debug = false
private sessionManager = new SessionManager( private sessionManager = new SessionManager(
this.serverUrl, this.serverUrl,
this.contextName, this.contextName,
this.setCsrfToken this.requestClient
)
private contextManager = new ContextManager(
this.serverUrl,
this.requestClient
) )
private contextManager = new ContextManager(this.serverUrl, this.setCsrfToken)
private folderMap = new Map<string, Job[]>() private folderMap = new Map<string, Job[]>()
public get debug() { public get debug() {
@@ -143,10 +141,9 @@ export class SASViyaApiClient {
headers.Authorization = `Bearer ${accessToken}` headers.Authorization = `Bearer ${accessToken}`
} }
const { result: contexts } = await this.get<{ items: Context[] }>( const { result: contexts } = await this.requestClient.get<{
`/compute/contexts?limit=10000`, items: Context[]
accessToken }>(`/compute/contexts?limit=10000`, accessToken)
)
const executionContext = const executionContext =
contexts.items && contexts.items.length contexts.items && contexts.items.length
@@ -163,7 +160,7 @@ export class SASViyaApiClient {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
} }
const { result: createdSession } = await this.post<Session>( const { result: createdSession } = await this.requestClient.post<Session>(
`/compute/contexts/${executionContext.id}/sessions`, `/compute/contexts/${executionContext.id}/sessions`,
{}, {},
accessToken accessToken
@@ -376,13 +373,15 @@ export class SASViyaApiClient {
variables: jobVariables, variables: jobVariables,
arguments: jobArguments arguments: jobArguments
}) })
const { result: postedJob, etag } = await this.post<Job>( const { result: postedJob, etag } = await this.requestClient
`/compute/sessions/${executionSessionId}/jobs`, .post<Job>(
jobRequestBody, `/compute/sessions/${executionSessionId}/jobs`,
accessToken jobRequestBody,
).catch((err: any) => { accessToken
throw err )
}) .catch((err: any) => {
throw err
})
if (!waitForResult) { if (!waitForResult) {
return session return session
@@ -404,12 +403,14 @@ export class SASViyaApiClient {
pollOptions pollOptions
) )
const { result: currentJob } = await this.get<Job>( const { result: currentJob } = await this.requestClient
`/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`, .get<Job>(
accessToken `/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
).catch((err) => { accessToken
throw err )
}) .catch((err) => {
throw err
})
let jobResult let jobResult
let log let log
@@ -417,10 +418,8 @@ export class SASViyaApiClient {
const logLink = currentJob.links.find((l) => l.rel === 'log') const logLink = currentJob.links.find((l) => l.rel === 'log')
if (debug && logLink) { if (debug && logLink) {
log = await this.get<any>( log = await this.requestClient
`${logLink.href}/content?limit=10000`, .get<any>(`${logLink.href}/content?limit=10000`, accessToken)
accessToken
)
.then((res: any) => .then((res: any) =>
res.result.items.map((i: any) => i.line).join('\n') res.result.items.map((i: any) => i.line).join('\n')
) )
@@ -442,34 +441,30 @@ export class SASViyaApiClient {
} }
if (resultLink) { if (resultLink) {
jobResult = await this.get<any>( jobResult = await this.requestClient
resultLink, .get<any>(resultLink, accessToken, 'text/plain')
accessToken, .catch(async (e) => {
'text/plain' if (e && e.status === 404) {
).catch(async (e) => { if (logLink) {
if (e && e.status === 404) { log = await this.requestClient
if (logLink) { .get<any>(`${logLink.href}/content?limit=10000`, accessToken)
log = await this.get<any>( .then((res: any) =>
`${logLink.href}/content?limit=10000`, res.result.items.map((i: any) => i.line).join('\n')
accessToken )
) .catch((err) => {
.then((res: any) => throw err
res.result.items.map((i: any) => i.line).join('\n') })
)
.catch((err) => {
throw err
})
return Promise.reject({ return Promise.reject({
status: 500, status: 500,
log log
}) })
}
} }
} return {
return { result: JSON.stringify(e)
result: JSON.stringify(e) }
} })
})
} }
await this.sessionManager await this.sessionManager
@@ -559,7 +554,9 @@ export class SASViyaApiClient {
} }
} }
const { result: createFolderResponse } = await this.post<Folder>( const {
result: createFolderResponse
} = await this.requestClient.post<Folder>(
`/folders/folders?parentFolderUri=${parentFolderUri}`, `/folders/folders?parentFolderUri=${parentFolderUri}`,
JSON.stringify({ JSON.stringify({
name: folderName, name: folderName,
@@ -599,7 +596,7 @@ export class SASViyaApiClient {
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken) parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken)
} }
return await this.post<Job>( return await this.requestClient.post<Job>(
`${this.serverUrl}/jobDefinitions/definitions?parentFolderUri=${parentFolderUri}`, `${this.serverUrl}/jobDefinitions/definitions?parentFolderUri=${parentFolderUri}`,
JSON.stringify({ JSON.stringify({
name: jobName, name: jobName,
@@ -763,7 +760,7 @@ export class SASViyaApiClient {
headers.Authorization = `Bearer ${accessToken}` headers.Authorization = `Bearer ${accessToken}`
} }
const deleteResponse = await this.delete(url, accessToken) const deleteResponse = await this.requestClient.delete(url, accessToken)
return deleteResponse.result return deleteResponse.result
} }
@@ -835,7 +832,9 @@ export class SASViyaApiClient {
throw new Error(`URI of job definition was not found.`) throw new Error(`URI of job definition was not found.`)
} }
const { result: jobDefinition } = await this.get<JobDefinition>( const {
result: jobDefinition
} = await this.requestClient.get<JobDefinition>(
`${this.serverUrl}${jobDefinitionLink.href}`, `${this.serverUrl}${jobDefinitionLink.href}`,
accessToken accessToken
) )
@@ -913,7 +912,7 @@ export class SASViyaApiClient {
(l) => l.rel === 'getResource' (l) => l.rel === 'getResource'
)?.href )?.href
const { result: jobDefinition } = await this.get<Job>( const { result: jobDefinition } = await this.requestClient.get<Job>(
`${this.serverUrl}${jobDefinitionLink}`, `${this.serverUrl}${jobDefinitionLink}`,
accessToken accessToken
) )
@@ -948,12 +947,13 @@ export class SASViyaApiClient {
jobDefinition, jobDefinition,
arguments: jobArguments arguments: jobArguments
}) })
const { result: postedJob, etag } = await this.post<Job>( const { result: postedJob, etag } = await this.requestClient.post<Job>(
`${this.serverUrl}/jobExecution/jobs?_action=wait`, `${this.serverUrl}/jobExecution/jobs?_action=wait`,
postJobRequestBody postJobRequestBody,
accessToken
) )
const jobStatus = await this.pollJobState(postedJob, etag, accessToken) const jobStatus = await this.pollJobState(postedJob, etag, accessToken)
const { result: currentJob } = await this.get<Job>( const { result: currentJob } = await this.requestClient.get<Job>(
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`, `${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
accessToken accessToken
) )
@@ -964,17 +964,16 @@ export class SASViyaApiClient {
const resultLink = currentJob.results['_webout.json'] const resultLink = currentJob.results['_webout.json']
const logLink = currentJob.links.find((l) => l.rel === 'log') const logLink = currentJob.links.find((l) => l.rel === 'log')
if (resultLink) { if (resultLink) {
jobResult = await this.get<any>( jobResult = await this.requestClient.get<any>(
`${this.serverUrl}${resultLink}/content`, `${this.serverUrl}${resultLink}/content`,
accessToken, accessToken,
'text/plain' 'text/plain'
) )
} }
if (debug && logLink) { if (debug && logLink) {
log = await this.get<any>( log = await this.requestClient
`${this.serverUrl}${logLink.href}/content`, .get<any>(`${this.serverUrl}${logLink.href}/content`, accessToken)
accessToken .then((res: any) => res.result.items.map((i: any) => i.line).join('\n'))
).then((res: any) => res.result.items.map((i: any) => i.line).join('\n'))
} }
if (jobStatus === 'failed') { if (jobStatus === 'failed') {
return Promise.reject({ error: currentJob.error, log }) return Promise.reject({ error: currentJob.error, log })
@@ -991,11 +990,14 @@ export class SASViyaApiClient {
} }
const url = '/folders/folders/@item?path=' + path const url = '/folders/folders/@item?path=' + path
const { result: folder } = await this.get<Folder>(`${url}`, accessToken) const { result: folder } = await this.requestClient.get<Folder>(
`${url}`,
accessToken
)
if (!folder) { if (!folder) {
throw new Error(`The path ${path} does not exist on ${this.serverUrl}`) throw new Error(`The path ${path} does not exist on ${this.serverUrl}`)
} }
const { result: members } = await this.get<{ items: any[] }>( const { result: members } = await this.requestClient.get<{ items: any[] }>(
`/folders/folders/${folder.id}/members?limit=${folder.memberCount}`, `/folders/folders/${folder.id}/members?limit=${folder.memberCount}`,
accessToken accessToken
) )
@@ -1033,7 +1035,7 @@ export class SASViyaApiClient {
Promise.reject(`Job state link was not found.`) Promise.reject(`Job state link was not found.`)
} }
const { result: state } = await this.get<string>( const { result: state } = await this.requestClient.get<string>(
`${this.serverUrl}${stateLink.href}?_action=wait&wait=30`, `${this.serverUrl}${stateLink.href}?_action=wait&wait=30`,
accessToken, accessToken,
'text/plain' 'text/plain'
@@ -1054,7 +1056,7 @@ export class SASViyaApiClient {
postedJobState === 'pending' postedJobState === 'pending'
) { ) {
if (stateLink) { if (stateLink) {
const { result: jobState } = await this.get<string>( const { result: jobState } = await this.requestClient.get<string>(
`${this.serverUrl}${stateLink.href}?_action=wait&wait=30`, `${this.serverUrl}${stateLink.href}?_action=wait&wait=30`,
accessToken, accessToken,
'text/plain' 'text/plain'
@@ -1100,7 +1102,7 @@ export class SASViyaApiClient {
) )
} }
const uploadResponse = await this.uploadFile( const uploadResponse = await this.requestClient.uploadFile(
`${this.serverUrl}/files/files#rawUpload`, `${this.serverUrl}/files/files#rawUpload`,
csv, csv,
accessToken accessToken
@@ -1113,12 +1115,11 @@ export class SASViyaApiClient {
private async getFolderUri(folderPath: string, accessToken?: string) { private async getFolderUri(folderPath: string, accessToken?: string) {
const url = '/folders/folders/@item?path=' + folderPath const url = '/folders/folders/@item?path=' + folderPath
const { result: folder } = await this.get<Folder>( const { result: folder } = await this.requestClient
`${this.serverUrl}${url}`, .get<Folder>(`${this.serverUrl}${url}`, accessToken)
accessToken .catch(() => {
).catch(() => { return { result: null }
return { result: null } })
})
if (!folder) return undefined if (!folder) return undefined
return `/folders/folders/${folder.id}` return `/folders/folders/${folder.id}`
@@ -1127,12 +1128,11 @@ export class SASViyaApiClient {
private async getRecycleBinUri(accessToken: string) { private async getRecycleBinUri(accessToken: string) {
const url = '/folders/folders/@myRecycleBin' const url = '/folders/folders/@myRecycleBin'
const { result: folder } = await this.get<Folder>( const { result: folder } = await this.requestClient
`${this.serverUrl}${url}`, .get<Folder>(`${this.serverUrl}${url}`, accessToken)
accessToken .catch(() => {
).catch(() => { return { result: null }
return { result: null } })
})
if (!folder) return undefined if (!folder) return undefined
@@ -1196,27 +1196,31 @@ export class SASViyaApiClient {
const sourceFolderId = sourceFolderUri?.split('/').pop() const sourceFolderId = sourceFolderUri?.split('/').pop()
const url = sourceFolderUri const url = sourceFolderUri
const { result: folder } = await this.patch<Folder>( const { result: folder } = await this.requestClient
`${this.serverUrl}${url}`, .patch<Folder>(
JSON.stringify({ `${this.serverUrl}${url}`,
id: sourceFolderId, JSON.stringify({
name: targetFolderName, id: sourceFolderId,
parentFolderUri: targetParentFolderUri name: targetFolderName,
}), parentFolderUri: targetParentFolderUri
accessToken }),
).catch((err) => { accessToken
if (err.code && err.code === 'ENOTFOUND') { )
const notFoundError = { .catch((err) => {
body: JSON.stringify({ if (err.code && err.code === 'ENOTFOUND') {
message: `Folder '${sourceFolder.split('/').pop()}' was not found.` const notFoundError = {
}) body: JSON.stringify({
message: `Folder '${sourceFolder
.split('/')
.pop()}' was not found.`
})
}
throw notFoundError
} }
throw notFoundError throw err
} })
throw err
})
if (!folder) return undefined if (!folder) return undefined
@@ -1244,175 +1248,4 @@ export class SASViyaApiClient {
return movedFolder return movedFolder
} }
setCsrfTokenLocal = (csrfToken: CsrfToken) => {
this.csrfToken = csrfToken
this.setCsrfToken(csrfToken)
}
setFileUploadCsrfToken = (csrfToken: CsrfToken) => {
this.fileUploadCsrfToken = csrfToken
}
private get<T>(
url: string,
accessToken?: string,
contentType = 'application/json'
) {
const headers: any = {
'Content-Type': contentType
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
const requestConfig: AxiosRequestConfig = {
headers,
responseType: contentType === 'text/plain' ? 'text' : 'json',
withCredentials: true
}
if (contentType === 'text/plain') {
requestConfig.headers.Accept = '*/*'
requestConfig.transformResponse = undefined
}
return this.httpClient.get<T>(url, requestConfig).then((response) => ({
result: response.data,
etag: response.headers['etag']
}))
}
private post<T>(
url: string,
data: any = {},
accessToken?: string
): Promise<{ result: T; etag: string }> {
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
if (this.csrfToken?.value) {
headers[this.csrfToken.headerName] = this.csrfToken.value
}
return this.httpClient
.post<T>(url, data, { headers, withCredentials: true })
.then((response) => {
return {
result: response.data as T,
etag: response.headers['etag'] as string
}
})
.catch((e) => {
const response = e.response as AxiosResponse
if (response.status === 403 || response.status === 449) {
const tokenHeader = (response.headers[
'x-csrf-header'
] as string)?.toLowerCase()
if (tokenHeader) {
const token = response.headers[tokenHeader]
this.setCsrfTokenLocal({
headerName: tokenHeader,
value: token || ''
})
return this.post<T>(url, data, accessToken)
}
throw e
}
throw e
})
}
private delete<T>(url: string, accessToken?: string) {
const headers: any = {}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
if (this.csrfToken?.value) {
headers[this.csrfToken.headerName] = this.csrfToken.value
}
const requestConfig: AxiosRequestConfig = {
headers
}
return this.httpClient.delete<T>(url, requestConfig).then((response) => ({
result: response.data,
etag: response.headers['etag']
}))
}
private patch<T>(url: string, data: any = {}, accessToken?: string) {
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
if (this.csrfToken?.value) {
headers[this.csrfToken.headerName] = this.csrfToken.value
}
return this.httpClient
.patch<T>(url, data, { headers, withCredentials: true })
.then((response) => {
return {
result: response.data as T,
etag: response.headers['etag'] as string
}
})
}
private uploadFile(
url: string,
content: string,
accessToken?: string
): Promise<any> {
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
if (this.fileUploadCsrfToken?.value) {
headers[
this.fileUploadCsrfToken.headerName
] = this.fileUploadCsrfToken.value
}
return this.httpClient
.post(url, content, { headers, withCredentials: true })
.then((response) => {
return {
result: response.data,
etag: response.headers['etag'] as string
}
})
.catch((e) => {
const response = e.response as AxiosResponse
if (response.status === 403 || response.status === 449) {
const tokenHeader = (response.headers[
'x-csrf-header'
] as string)?.toLowerCase()
if (tokenHeader) {
const token = response.headers[tokenHeader]
this.setFileUploadCsrfToken({
headerName: tokenHeader,
value: token || ''
})
return this.uploadFile(url, content, accessToken)
}
throw e
}
throw e
})
}
} }

View File

@@ -24,6 +24,7 @@ import { SAS9ApiClient } from './SAS9ApiClient'
import { FileUploader } from './FileUploader' import { FileUploader } from './FileUploader'
import { isLogInRequired, AuthManager } from './auth' import { isLogInRequired, AuthManager } from './auth'
import { ServerType } from '@sasjs/utils/types' import { ServerType } from '@sasjs/utils/types'
import { RequestClient } from './request/client'
const defaultConfig: SASjsConfig = { const defaultConfig: SASjsConfig = {
serverUrl: '', serverUrl: '',
@@ -45,7 +46,6 @@ const requestRetryLimit = 5
export default class SASjs { export default class SASjs {
private sasjsConfig: SASjsConfig = new SASjsConfig() private sasjsConfig: SASjsConfig = new SASjsConfig()
private jobsPath: string = '' private jobsPath: string = ''
private csrfTokenApi: CsrfToken | null = null
private csrfTokenWeb: CsrfToken | null = null private csrfTokenWeb: CsrfToken | null = null
private retryCountWeb: number = 0 private retryCountWeb: number = 0
private retryCountComputeApi: number = 0 private retryCountComputeApi: number = 0
@@ -56,6 +56,7 @@ export default class SASjs {
private sas9ApiClient: SAS9ApiClient | null = null private sas9ApiClient: SAS9ApiClient | null = null
private fileUploader: FileUploader | null = null private fileUploader: FileUploader | null = null
private authManager: AuthManager | null = null private authManager: AuthManager | null = null
private requestClient: RequestClient | null = null
constructor(config?: any) { constructor(config?: any) {
this.sasjsConfig = { this.sasjsConfig = {
@@ -420,22 +421,6 @@ export default class SASjs {
return this.authManager!.userName return this.authManager!.userName
} }
/**
* Returns the _csrf token of the current session for the API approach.
*
*/
public getCsrfApi() {
return this.csrfTokenApi?.value
}
/**
* Returns the _csrf token of the current session for the WEB approach.
*
*/
public getCsrfWeb() {
return this.csrfTokenWeb?.value
}
/** /**
* Sets the SASjs configuration. * Sets the SASjs configuration.
* @param config - SASjs configuration. * @param config - SASjs configuration.
@@ -498,8 +483,7 @@ export default class SASjs {
this.sasjsConfig.appLoc, this.sasjsConfig.appLoc,
this.sasjsConfig.serverUrl, this.sasjsConfig.serverUrl,
this.jobsPath, this.jobsPath,
this.setCsrfTokenWeb, this.requestClient!
this.csrfTokenWeb
) )
return fileUploader.uploadFile(sasJob, files, params) return fileUploader.uploadFile(sasJob, files, params)
@@ -604,7 +588,7 @@ export default class SASjs {
serverUrl, serverUrl,
appLoc, appLoc,
this.sasjsConfig.contextName, this.sasjsConfig.contextName,
this.setCsrfTokenApi this.requestClient!
) )
sasApiClient.debug = this.sasjsConfig.debug sasApiClient.debug = this.sasjsConfig.debug
} else if (this.sasjsConfig.serverType === ServerType.Sas9) { } else if (this.sasjsConfig.serverType === ServerType.Sas9) {
@@ -1150,14 +1134,6 @@ export default class SASjs {
return sasjsWaitingRequest.requestPromise.promise return sasjsWaitingRequest.requestPromise.promise
} }
private setCsrfTokenWeb = (csrfToken: CsrfToken) => {
this.csrfTokenWeb = csrfToken
}
private setCsrfTokenApi = (csrfToken: CsrfToken) => {
this.csrfTokenApi = csrfToken
}
private resendWaitingRequests = async () => { private resendWaitingRequests = async () => {
for (const sasjsWaitingRequest of this.sasjsWaitingRequests) { for (const sasjsWaitingRequest of this.sasjsWaitingRequests) {
this.request(sasjsWaitingRequest.SASjob, sasjsWaitingRequest.data).then( this.request(sasjsWaitingRequest.SASjob, sasjsWaitingRequest.data).then(
@@ -1396,10 +1372,13 @@ export default class SASjs {
this.sasjsConfig.serverUrl = this.sasjsConfig.serverUrl.slice(0, -1) this.sasjsConfig.serverUrl = this.sasjsConfig.serverUrl.slice(0, -1)
} }
this.requestClient = new RequestClient(this.sasjsConfig.serverUrl)
this.jobsPath = this.jobsPath =
this.sasjsConfig.serverType === ServerType.SasViya this.sasjsConfig.serverType === ServerType.SasViya
? this.sasjsConfig.pathSASViya ? this.sasjsConfig.pathSASViya
: this.sasjsConfig.pathSAS9 : this.sasjsConfig.pathSAS9
this.authManager = new AuthManager( this.authManager = new AuthManager(
this.sasjsConfig.serverUrl, this.sasjsConfig.serverUrl,
this.sasjsConfig.serverType!, this.sasjsConfig.serverType!,
@@ -1417,7 +1396,7 @@ export default class SASjs {
this.sasjsConfig.serverUrl, this.sasjsConfig.serverUrl,
this.sasjsConfig.appLoc, this.sasjsConfig.appLoc,
this.sasjsConfig.contextName, this.sasjsConfig.contextName,
this.setCsrfTokenApi this.requestClient
) )
this.sasViyaApiClient.debug = this.sasjsConfig.debug this.sasViyaApiClient.debug = this.sasjsConfig.debug
@@ -1432,7 +1411,7 @@ export default class SASjs {
this.sasjsConfig.appLoc, this.sasjsConfig.appLoc,
this.sasjsConfig.serverUrl, this.sasjsConfig.serverUrl,
this.jobsPath, this.jobsPath,
this.setCsrfTokenWeb this.requestClient
) )
} }

View File

@@ -1,6 +1,7 @@
import { Session, Context, CsrfToken, SessionVariable } from './types' import { Session, Context, CsrfToken, SessionVariable } from './types'
import { asyncForEach, makeRequest, isUrl } from './utils' import { asyncForEach, isUrl } from './utils'
import { prefixMessage } from '@sasjs/utils/error' import { prefixMessage } from '@sasjs/utils/error'
import { RequestClient } from './request/client'
const MAX_SESSION_COUNT = 1 const MAX_SESSION_COUNT = 1
const RETRY_LIMIT: number = 3 const RETRY_LIMIT: number = 3
@@ -14,7 +15,7 @@ export class SessionManager {
constructor( constructor(
private serverUrl: string, private serverUrl: string,
private contextName: string, private contextName: string,
private setCsrfToken: (csrfToken: CsrfToken) => void private requestClient: RequestClient
) { ) {
if (serverUrl) isUrl(serverUrl) if (serverUrl) isUrl(serverUrl)
} }
@@ -63,10 +64,8 @@ export class SessionManager {
headers: this.getHeaders(accessToken) headers: this.getHeaders(accessToken)
} }
return await this.request<Session>( return await this.requestClient
`${this.serverUrl}/compute/sessions/${id}`, .delete<Session>(`/compute/sessions/${id}`, accessToken)
deleteSessionRequest
)
.then(() => { .then(() => {
this.sessions = this.sessions.filter((s) => s.id !== id) this.sessions = this.sessions.filter((s) => s.id !== id)
}) })
@@ -98,17 +97,20 @@ export class SessionManager {
} }
private async createAndWaitForSession(accessToken?: string) { private async createAndWaitForSession(accessToken?: string) {
const createSessionRequest = { const {
method: 'POST', result: createdSession,
headers: this.getHeaders(accessToken) etag
} } = await this.requestClient
.post<Session>(
const { result: createdSession, etag } = await this.request<Session>( `${this.serverUrl}/compute/contexts/${
`${this.serverUrl}/compute/contexts/${this.currentContext!.id}/sessions`, this.currentContext!.id
createSessionRequest }/sessions`,
).catch((err) => { {},
throw err accessToken
}) )
.catch((err) => {
throw err
})
await this.waitForSession(createdSession, etag, accessToken) await this.waitForSession(createdSession, etag, accessToken)
@@ -119,13 +121,13 @@ export class SessionManager {
private async setCurrentContext(accessToken?: string) { private async setCurrentContext(accessToken?: string) {
if (!this.currentContext) { if (!this.currentContext) {
const { result: contexts } = await this.request<{ const { result: contexts } = await this.requestClient
items: Context[] .get<{
}>(`${this.serverUrl}/compute/contexts?limit=10000`, { items: Context[]
headers: this.getHeaders(accessToken) }>(`${this.serverUrl}/compute/contexts?limit=10000`, accessToken)
}).catch((err) => { .catch((err) => {
throw err throw err
}) })
const contextsList = const contextsList =
contexts && contexts.items && contexts.items.length contexts && contexts.items && contexts.items.length
@@ -166,10 +168,7 @@ export class SessionManager {
accessToken?: string accessToken?: string
) { ) {
let sessionState = session.state let sessionState = session.state
const headers: any = {
...this.getHeaders(accessToken),
'If-None-Match': etag
}
const stateLink = session.links.find((l: any) => l.rel === 'state') const stateLink = session.links.find((l: any) => l.rel === 'state')
return new Promise(async (resolve, _) => { return new Promise(async (resolve, _) => {
@@ -185,12 +184,10 @@ export class SessionManager {
this.printedSessionState.printed = true this.printedSessionState.printed = true
} }
const { result: state } = await this.requestSessionStatus<string>( const state = await this.getSessionState(
`${this.serverUrl}${stateLink.href}?wait=30`, `${this.serverUrl}${stateLink.href}?wait=30`,
{ etag!,
headers accessToken
},
'text'
).catch((err) => { ).catch((err) => {
throw err throw err
}) })
@@ -223,73 +220,33 @@ export class SessionManager {
}) })
} }
private async request<T>( private async getSessionState(
url: string, url: string,
options: RequestInit, etag: string,
contentType: 'text' | 'json' = 'json' accessToken?: string
) { ) {
if (this.csrfToken) { return await this.requestClient
options.headers = { .get(url, accessToken, 'text/plain', { 'If-None-Match': etag })
...options.headers, .then((res) => res.result as string)
[this.csrfToken.headerName]: this.csrfToken.value .catch((err) => {
} if (err.status === INTERNAL_SAS_ERROR.status)
} return INTERNAL_SAS_ERROR.message
return await makeRequest<T>( throw err
url, })
options,
(token) => {
this.csrfToken = token
this.setCsrfToken(token)
},
contentType
).catch((err) => {
throw err
})
}
private async requestSessionStatus<T>(
url: string,
options: RequestInit,
contentType: 'text' | 'json' = 'json'
) {
if (this.csrfToken) {
options.headers = {
...options.headers,
[this.csrfToken.headerName]: this.csrfToken.value
}
}
return await makeRequest<T>(
url,
options,
(token) => {
this.csrfToken = token
this.setCsrfToken(token)
},
contentType
).catch((err) => {
if (err.status === INTERNAL_SAS_ERROR.status)
return { result: INTERNAL_SAS_ERROR.message }
throw err
})
} }
async getVariable(sessionId: string, variable: string, accessToken?: string) { async getVariable(sessionId: string, variable: string, accessToken?: string) {
const getSessionVariable = { return await this.requestClient
method: 'GET', .get<SessionVariable>(
headers: this.getHeaders(accessToken) `${this.serverUrl}/compute/sessions/${sessionId}/variables/${variable}`,
} accessToken
return await this.request<SessionVariable>(
`${this.serverUrl}/compute/sessions/${sessionId}/variables/${variable}`,
getSessionVariable
).catch((err) => {
throw prefixMessage(
err,
`Error while fetching session variable '${variable}'.`
) )
}) .catch((err) => {
throw prefixMessage(
err,
`Error while fetching session variable '${variable}'.`
)
})
} }
} }

262
src/request/client.ts Normal file
View File

@@ -0,0 +1,262 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { CsrfToken } from '..'
export class RequestClient {
private csrfToken: CsrfToken | undefined
private fileUploadCsrfToken: CsrfToken | undefined
private httpClient: AxiosInstance
constructor(baseUrl: string) {
this.httpClient = axios.create({ baseURL: baseUrl })
}
public getCsrfToken(type: 'general' | 'file' = 'general') {
return type === 'file' ? this.fileUploadCsrfToken : this.csrfToken
}
public async get<T>(
url: string,
accessToken: string | undefined,
contentType: string = 'application/json',
overrideHeaders: { [key: string]: string | number } = {}
): Promise<{ result: T; etag: string }> {
const headers = {
...this.getHeaders(accessToken, contentType),
...overrideHeaders
}
const requestConfig: AxiosRequestConfig = {
headers,
responseType: contentType === 'text/plain' ? 'text' : 'json',
withCredentials: true
}
if (contentType === 'text/plain') {
requestConfig.headers.Accept = '*/*'
requestConfig.transformResponse = undefined
}
try {
const response = await this.httpClient.get<T>(url, requestConfig)
return {
result: response.data as T,
etag: response.headers['etag']
}
} catch (e) {
const response_1 = e.response as AxiosResponse
if (response_1.status === 403 || response_1.status === 449) {
this.parseAndSetCsrfToken(response_1)
if (this.csrfToken) {
return this.get<T>(url, accessToken, contentType, overrideHeaders)
}
throw e
}
throw e
}
}
public post<T>(
url: string,
data: any,
accessToken: string | undefined,
overrideHeaders: { [key: string]: string | number } = {}
): Promise<{ result: T; etag: string }> {
const headers = {
...this.getHeaders(accessToken, 'application/json'),
...overrideHeaders
}
return this.httpClient
.post<T>(url, data, { headers, withCredentials: true })
.then((response) => {
return {
result: response.data as T,
etag: response.headers['etag'] as string
}
})
.catch((e) => {
const response = e.response as AxiosResponse
if (response.status === 403 || response.status === 449) {
this.parseAndSetCsrfToken(response)
if (this.csrfToken) {
return this.post<T>(url, data, accessToken)
}
throw e
}
throw e
})
}
public async put<T>(
url: string,
data: any,
accessToken: string | undefined,
overrideHeaders: { [key: string]: string | number } = {}
): Promise<{ result: T; etag: string }> {
const headers = {
...this.getHeaders(accessToken, 'application/json'),
...overrideHeaders
}
try {
const response = await this.httpClient.put<T>(url, data, {
headers,
withCredentials: true
})
return {
result: response.data as T,
etag: response.headers['etag'] as string
}
} catch (e) {
const response_1 = e.response as AxiosResponse
if (response_1.status === 403 || response_1.status === 449) {
this.parseAndSetCsrfToken(response_1)
if (this.csrfToken) {
return this.put<T>(url, data, accessToken)
}
throw e
}
throw e
}
}
public async delete<T>(
url: string,
accessToken?: string
): Promise<{ result: T; etag: string }> {
const headers = this.getHeaders(accessToken, 'application/json')
const requestConfig: AxiosRequestConfig = {
headers
}
try {
const response = await this.httpClient.delete<T>(url, requestConfig)
return {
result: response.data as T,
etag: response.headers['etag']
}
} catch (e) {
const response_1 = e.response as AxiosResponse
if (response_1.status === 403 || response_1.status === 449) {
this.parseAndSetCsrfToken(response_1)
if (this.csrfToken) {
return this.delete<T>(url, accessToken)
}
throw e
}
throw e
}
}
public async patch<T>(
url: string,
data: any = {},
accessToken?: string
): Promise<{ result: T; etag: string }> {
const headers = this.getHeaders(accessToken, 'application/json')
try {
const response = await this.httpClient.patch<T>(url, data, {
headers,
withCredentials: true
})
return {
result: response.data as T,
etag: response.headers['etag'] as string
}
} catch (e) {
const response_1 = e.response as AxiosResponse
if (response_1.status === 403 || response_1.status === 449) {
this.parseAndSetCsrfToken(response_1)
if (this.csrfToken) {
return this.patch<T>(url, accessToken)
}
throw e
}
throw e
}
}
public async uploadFile(
url: string,
content: string,
accessToken?: string
): Promise<any> {
const headers = this.getHeaders(accessToken, 'application/json')
if (this.fileUploadCsrfToken?.value) {
headers[
this.fileUploadCsrfToken.headerName
] = this.fileUploadCsrfToken.value
}
try {
const response = await this.httpClient.post(url, content, { headers })
return {
result: response.data,
etag: response.headers['etag'] as string
}
} catch (e) {
const response_1 = e.response as AxiosResponse
if (response_1.status === 403 || response_1.status === 449) {
this.parseAndSetFileUploadCsrfToken(response_1)
if (this.fileUploadCsrfToken) {
return this.uploadFile(url, content, accessToken)
}
throw e
}
throw e
}
}
private getHeaders(accessToken: string | undefined, contentType: string) {
const headers: any = {
'Content-Type': contentType
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
if (this.csrfToken?.value) {
headers[this.csrfToken.headerName] = this.csrfToken.value
}
return headers
}
private parseAndSetFileUploadCsrfToken(response: AxiosResponse) {
const token = this.parseCsrfToken(response)
if (token) {
this.fileUploadCsrfToken = token
}
}
private parseAndSetCsrfToken(response: AxiosResponse) {
const token = this.parseCsrfToken(response)
if (token) {
this.csrfToken = token
}
}
private parseCsrfToken(response: AxiosResponse): CsrfToken | undefined {
const tokenHeader = (response.headers[
'x-csrf-header'
] as string)?.toLowerCase()
if (tokenHeader) {
const token = response.headers[tokenHeader]
const csrfToken = {
headerName: tokenHeader,
value: token || ''
}
return csrfToken
}
}
}