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 { ServerType } from "@sasjs/utils/types";
const defaultConfig: SASjsConfig = {
serverUrl: window.location.origin,
pathSAS9: '/SASStoredProcess/do',
pathSASViya: '/SASJobExecution',
appLoc: '/Public/seedapp',
serverType: ServerType.SASViya,
pathSAS9: "/SASStoredProcess/do",
pathSASViya: "/SASJobExecution",
appLoc: "/Public/seedapp",
serverType: ServerType.SasViya,
debug: false,
contextName: 'SAS Job Execution compute context',
contextName: "SAS Job Execution compute context",
useComputeApi: false
};
@@ -17,7 +18,7 @@ const customConfig = {
pathSAS9: "sas9",
pathSASViya: "viya",
appLoc: "/Public/seedapp",
serverType: ServerType.SAS9,
serverType: ServerType.Sas9,
debug: false
};
@@ -39,11 +40,12 @@ export const basicTests = (
},
{
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 () => {
await adapter.logOut()
await adapter.logIn('invalid', 'invalid')
return adapter.logIn(userName, password)
await adapter.logOut();
await adapter.logIn("invalid", "invalid");
return adapter.logIn(userName, password);
},
assertion: (response: any) =>
response && response.isLoggedIn && response.userName === userName

View File

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

View File

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

View File

@@ -6,7 +6,6 @@ import {
Context,
ContextAllAttributes,
Folder,
CsrfToken,
EditContextInput,
JobDefinition,
PollOptions
@@ -16,35 +15,34 @@ import { SessionManager } from './SessionManager'
import { ContextManager } from './ContextManager'
import { timestampToYYYYMMDDHHMMSS } from '@sasjs/utils/time'
import { Logger, LogLevel } from '@sasjs/utils/logger'
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired'
import { parseAndSubmitAuthorizeForm } from './auth'
import { RequestClient } from './request/client'
/**
* A client for interfacing with the SAS Viya REST API.
*
*/
export class SASViyaApiClient {
private httpClient: AxiosInstance
constructor(
private serverUrl: string,
private rootFolderName: string,
private contextName: string,
private setCsrfToken: (csrfToken: CsrfToken) => void
private requestClient: RequestClient
) {
if (serverUrl) isUrl(serverUrl)
this.httpClient = axios.create({ baseURL: serverUrl })
}
private csrfToken: CsrfToken | null = null
private fileUploadCsrfToken: CsrfToken | null = null
private _debug = false
private sessionManager = new SessionManager(
this.serverUrl,
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[]>()
public get debug() {
@@ -143,10 +141,9 @@ export class SASViyaApiClient {
headers.Authorization = `Bearer ${accessToken}`
}
const { result: contexts } = await this.get<{ items: Context[] }>(
`/compute/contexts?limit=10000`,
accessToken
)
const { result: contexts } = await this.requestClient.get<{
items: Context[]
}>(`/compute/contexts?limit=10000`, accessToken)
const executionContext =
contexts.items && contexts.items.length
@@ -163,7 +160,7 @@ export class SASViyaApiClient {
'Content-Type': 'application/json'
}
}
const { result: createdSession } = await this.post<Session>(
const { result: createdSession } = await this.requestClient.post<Session>(
`/compute/contexts/${executionContext.id}/sessions`,
{},
accessToken
@@ -376,13 +373,15 @@ export class SASViyaApiClient {
variables: jobVariables,
arguments: jobArguments
})
const { result: postedJob, etag } = await this.post<Job>(
`/compute/sessions/${executionSessionId}/jobs`,
jobRequestBody,
accessToken
).catch((err: any) => {
throw err
})
const { result: postedJob, etag } = await this.requestClient
.post<Job>(
`/compute/sessions/${executionSessionId}/jobs`,
jobRequestBody,
accessToken
)
.catch((err: any) => {
throw err
})
if (!waitForResult) {
return session
@@ -404,12 +403,14 @@ export class SASViyaApiClient {
pollOptions
)
const { result: currentJob } = await this.get<Job>(
`/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
accessToken
).catch((err) => {
throw err
})
const { result: currentJob } = await this.requestClient
.get<Job>(
`/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
accessToken
)
.catch((err) => {
throw err
})
let jobResult
let log
@@ -417,10 +418,8 @@ export class SASViyaApiClient {
const logLink = currentJob.links.find((l) => l.rel === 'log')
if (debug && logLink) {
log = await this.get<any>(
`${logLink.href}/content?limit=10000`,
accessToken
)
log = await this.requestClient
.get<any>(`${logLink.href}/content?limit=10000`, accessToken)
.then((res: any) =>
res.result.items.map((i: any) => i.line).join('\n')
)
@@ -442,34 +441,30 @@ export class SASViyaApiClient {
}
if (resultLink) {
jobResult = await this.get<any>(
resultLink,
accessToken,
'text/plain'
).catch(async (e) => {
if (e && e.status === 404) {
if (logLink) {
log = await this.get<any>(
`${logLink.href}/content?limit=10000`,
accessToken
)
.then((res: any) =>
res.result.items.map((i: any) => i.line).join('\n')
)
.catch((err) => {
throw err
})
jobResult = await this.requestClient
.get<any>(resultLink, accessToken, 'text/plain')
.catch(async (e) => {
if (e && e.status === 404) {
if (logLink) {
log = await this.requestClient
.get<any>(`${logLink.href}/content?limit=10000`, accessToken)
.then((res: any) =>
res.result.items.map((i: any) => i.line).join('\n')
)
.catch((err) => {
throw err
})
return Promise.reject({
status: 500,
log
})
return Promise.reject({
status: 500,
log
})
}
}
}
return {
result: JSON.stringify(e)
}
})
return {
result: JSON.stringify(e)
}
})
}
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}`,
JSON.stringify({
name: folderName,
@@ -599,7 +596,7 @@ export class SASViyaApiClient {
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken)
}
return await this.post<Job>(
return await this.requestClient.post<Job>(
`${this.serverUrl}/jobDefinitions/definitions?parentFolderUri=${parentFolderUri}`,
JSON.stringify({
name: jobName,
@@ -763,7 +760,7 @@ export class SASViyaApiClient {
headers.Authorization = `Bearer ${accessToken}`
}
const deleteResponse = await this.delete(url, accessToken)
const deleteResponse = await this.requestClient.delete(url, accessToken)
return deleteResponse.result
}
@@ -835,7 +832,9 @@ export class SASViyaApiClient {
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}`,
accessToken
)
@@ -913,7 +912,7 @@ export class SASViyaApiClient {
(l) => l.rel === 'getResource'
)?.href
const { result: jobDefinition } = await this.get<Job>(
const { result: jobDefinition } = await this.requestClient.get<Job>(
`${this.serverUrl}${jobDefinitionLink}`,
accessToken
)
@@ -948,12 +947,13 @@ export class SASViyaApiClient {
jobDefinition,
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`,
postJobRequestBody
postJobRequestBody,
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}`,
accessToken
)
@@ -964,17 +964,16 @@ export class SASViyaApiClient {
const resultLink = currentJob.results['_webout.json']
const logLink = currentJob.links.find((l) => l.rel === 'log')
if (resultLink) {
jobResult = await this.get<any>(
jobResult = await this.requestClient.get<any>(
`${this.serverUrl}${resultLink}/content`,
accessToken,
'text/plain'
)
}
if (debug && logLink) {
log = await this.get<any>(
`${this.serverUrl}${logLink.href}/content`,
accessToken
).then((res: any) => res.result.items.map((i: any) => i.line).join('\n'))
log = await this.requestClient
.get<any>(`${this.serverUrl}${logLink.href}/content`, accessToken)
.then((res: any) => res.result.items.map((i: any) => i.line).join('\n'))
}
if (jobStatus === 'failed') {
return Promise.reject({ error: currentJob.error, log })
@@ -991,11 +990,14 @@ export class SASViyaApiClient {
}
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) {
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}`,
accessToken
)
@@ -1033,7 +1035,7 @@ export class SASViyaApiClient {
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`,
accessToken,
'text/plain'
@@ -1054,7 +1056,7 @@ export class SASViyaApiClient {
postedJobState === 'pending'
) {
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`,
accessToken,
'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`,
csv,
accessToken
@@ -1113,12 +1115,11 @@ export class SASViyaApiClient {
private async getFolderUri(folderPath: string, accessToken?: string) {
const url = '/folders/folders/@item?path=' + folderPath
const { result: folder } = await this.get<Folder>(
`${this.serverUrl}${url}`,
accessToken
).catch(() => {
return { result: null }
})
const { result: folder } = await this.requestClient
.get<Folder>(`${this.serverUrl}${url}`, accessToken)
.catch(() => {
return { result: null }
})
if (!folder) return undefined
return `/folders/folders/${folder.id}`
@@ -1127,12 +1128,11 @@ export class SASViyaApiClient {
private async getRecycleBinUri(accessToken: string) {
const url = '/folders/folders/@myRecycleBin'
const { result: folder } = await this.get<Folder>(
`${this.serverUrl}${url}`,
accessToken
).catch(() => {
return { result: null }
})
const { result: folder } = await this.requestClient
.get<Folder>(`${this.serverUrl}${url}`, accessToken)
.catch(() => {
return { result: null }
})
if (!folder) return undefined
@@ -1196,27 +1196,31 @@ export class SASViyaApiClient {
const sourceFolderId = sourceFolderUri?.split('/').pop()
const url = sourceFolderUri
const { result: folder } = await this.patch<Folder>(
`${this.serverUrl}${url}`,
JSON.stringify({
id: sourceFolderId,
name: targetFolderName,
parentFolderUri: targetParentFolderUri
}),
accessToken
).catch((err) => {
if (err.code && err.code === 'ENOTFOUND') {
const notFoundError = {
body: JSON.stringify({
message: `Folder '${sourceFolder.split('/').pop()}' was not found.`
})
const { result: folder } = await this.requestClient
.patch<Folder>(
`${this.serverUrl}${url}`,
JSON.stringify({
id: sourceFolderId,
name: targetFolderName,
parentFolderUri: targetParentFolderUri
}),
accessToken
)
.catch((err) => {
if (err.code && err.code === 'ENOTFOUND') {
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
@@ -1244,175 +1248,4 @@ export class SASViyaApiClient {
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 { isLogInRequired, AuthManager } from './auth'
import { ServerType } from '@sasjs/utils/types'
import { RequestClient } from './request/client'
const defaultConfig: SASjsConfig = {
serverUrl: '',
@@ -45,7 +46,6 @@ const requestRetryLimit = 5
export default class SASjs {
private sasjsConfig: SASjsConfig = new SASjsConfig()
private jobsPath: string = ''
private csrfTokenApi: CsrfToken | null = null
private csrfTokenWeb: CsrfToken | null = null
private retryCountWeb: number = 0
private retryCountComputeApi: number = 0
@@ -56,6 +56,7 @@ export default class SASjs {
private sas9ApiClient: SAS9ApiClient | null = null
private fileUploader: FileUploader | null = null
private authManager: AuthManager | null = null
private requestClient: RequestClient | null = null
constructor(config?: any) {
this.sasjsConfig = {
@@ -420,22 +421,6 @@ export default class SASjs {
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.
* @param config - SASjs configuration.
@@ -498,8 +483,7 @@ export default class SASjs {
this.sasjsConfig.appLoc,
this.sasjsConfig.serverUrl,
this.jobsPath,
this.setCsrfTokenWeb,
this.csrfTokenWeb
this.requestClient!
)
return fileUploader.uploadFile(sasJob, files, params)
@@ -604,7 +588,7 @@ export default class SASjs {
serverUrl,
appLoc,
this.sasjsConfig.contextName,
this.setCsrfTokenApi
this.requestClient!
)
sasApiClient.debug = this.sasjsConfig.debug
} else if (this.sasjsConfig.serverType === ServerType.Sas9) {
@@ -1150,14 +1134,6 @@ export default class SASjs {
return sasjsWaitingRequest.requestPromise.promise
}
private setCsrfTokenWeb = (csrfToken: CsrfToken) => {
this.csrfTokenWeb = csrfToken
}
private setCsrfTokenApi = (csrfToken: CsrfToken) => {
this.csrfTokenApi = csrfToken
}
private resendWaitingRequests = async () => {
for (const sasjsWaitingRequest of this.sasjsWaitingRequests) {
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.requestClient = new RequestClient(this.sasjsConfig.serverUrl)
this.jobsPath =
this.sasjsConfig.serverType === ServerType.SasViya
? this.sasjsConfig.pathSASViya
: this.sasjsConfig.pathSAS9
this.authManager = new AuthManager(
this.sasjsConfig.serverUrl,
this.sasjsConfig.serverType!,
@@ -1417,7 +1396,7 @@ export default class SASjs {
this.sasjsConfig.serverUrl,
this.sasjsConfig.appLoc,
this.sasjsConfig.contextName,
this.setCsrfTokenApi
this.requestClient
)
this.sasViyaApiClient.debug = this.sasjsConfig.debug
@@ -1432,7 +1411,7 @@ export default class SASjs {
this.sasjsConfig.appLoc,
this.sasjsConfig.serverUrl,
this.jobsPath,
this.setCsrfTokenWeb
this.requestClient
)
}

View File

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