1
0
mirror of https://github.com/sasjs/adapter.git synced 2025-12-11 01:14:36 +00:00

chore(*): refactor common functionality into JobExecutor, handle all auth-related scenarios

This commit is contained in:
Krishna Acondy
2021-02-02 08:18:48 +00:00
parent 2ea49a425f
commit 03b5e1d824
11 changed files with 307 additions and 375 deletions

View File

@@ -18,7 +18,6 @@ import { ContextManager } from './ContextManager'
import { timestampToYYYYMMDDHHMMSS } from '@sasjs/utils/time'
import { Logger, LogLevel } from '@sasjs/utils/logger'
import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired'
import { parseAndSubmitAuthorizeForm } from './auth'
import { RequestClient } from './request/RequestClient'
import { NotFoundError } from './types/NotFoundError'
@@ -632,10 +631,7 @@ export class SASViyaApiClient {
.then(async (response) => {
let code = ''
if (isAuthorizeFormRequired(response)) {
const formResponse: any = await parseAndSubmitAuthorizeForm(
response,
this.serverUrl
)
const formResponse: any = await this.requestClient.authorize(response)
const responseBody = formResponse
.split('<body>')[1]

View File

@@ -716,6 +716,7 @@ export default class SASjs {
this.authManager = new AuthManager(
this.sasjsConfig.serverUrl,
this.sasjsConfig.serverType!,
this.requestClient,
this.resendWaitingRequests
)

View File

@@ -1,19 +1,18 @@
import { ServerType } from '@sasjs/utils/types'
import axios, { AxiosInstance } from 'axios'
import { isAuthorizeFormRequired, parseAndSubmitAuthorizeForm } from '.'
import { isAuthorizeFormRequired } from '.'
import { RequestClient } from '../request/RequestClient'
import { serialize } from '../utils'
export class AuthManager {
public userName = ''
private loginUrl: string
private logoutUrl: string
private httpClient: AxiosInstance
constructor(
private serverUrl: string,
private serverType: ServerType,
private requestClient: RequestClient,
private loginCallback: () => Promise<void>
) {
this.httpClient = axios.create({ baseURL: this.serverUrl })
this.loginUrl = `/SASLogon/login`
this.logoutUrl =
this.serverType === ServerType.Sas9
@@ -50,21 +49,21 @@ export class AuthManager {
}
const loginParamsStr = serialize(loginParams)
const loginResponse = await this.httpClient
.post<string>(this.loginUrl, loginParamsStr, {
withCredentials: true,
responseType: 'text',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: '*/*'
}
})
.then((response) => response.data)
const { result: loginResponse } = await this.requestClient.post<string>(
this.loginUrl,
loginParamsStr,
undefined,
'text/plain',
{
'Content-Type': 'application/x-www-form-urlencoded',
Accept: '*/*'
}
)
let loggedIn
if (isAuthorizeFormRequired(loginResponse)) {
await parseAndSubmitAuthorizeForm(loginResponse, this.serverUrl)
await this.requestClient.authorize(loginResponse)
} else {
loggedIn = isLogInSuccess(loginResponse)
}
@@ -89,16 +88,12 @@ export class AuthManager {
* @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`.
*/
public async checkSession() {
const loginResponse = await this.httpClient.get(
const { result: loginResponse } = await this.requestClient.get<string>(
this.loginUrl.replace('.do', ''),
{
responseType: 'text',
headers: {
Accept: '*/*'
}
}
undefined,
'text/plain'
)
const responseText = loginResponse?.data
const responseText = loginResponse
const isLoggedIn = /<button.+onClick.+logout/gm.test(responseText)
let loginForm: any = null
@@ -158,7 +153,8 @@ export class AuthManager {
* Logs out of the configured SAS server.
*/
public logOut() {
return this.httpClient.get(this.logoutUrl).then(() => true)
this.requestClient.clearCsrfTokens()
return this.requestClient.get(this.logoutUrl, undefined).then(() => true)
}
}

View File

@@ -1,6 +1,3 @@
import { AuthManager } from './AuthManager'
export * from './AuthManager'
export * from './isAuthorizeFormRequired'
export * from './isLoginRequired'
export * from './parseAndSubmitAuthorizeForm'

View File

@@ -1,48 +0,0 @@
import axios from 'axios'
export const parseAndSubmitAuthorizeForm = async (
response: string,
serverUrl: string
) => {
let authUrl: string | null = null
const params: any = {}
const responseBody = response.split('<body>')[1].split('</body>')[0]
const bodyElement = document.createElement('div')
bodyElement.innerHTML = responseBody
const form = bodyElement.querySelector('#application_authorization')
authUrl = form ? serverUrl + form.getAttribute('action') : null
const inputs: any = form?.querySelectorAll('input')
for (const input of inputs) {
if (input.name === 'user_oauth_approval') {
input.value = 'true'
}
params[input.name] = input.value
}
const formData = new FormData()
for (const key in params) {
if (params.hasOwnProperty(key)) {
formData.append(key, params[key])
}
}
if (!authUrl) {
throw new Error('Auth Form URL is null or undefined.')
}
return await axios
.post(authUrl, formData, {
withCredentials: true,
responseType: 'text',
headers: {
Accept: '*/*'
}
})
.then((res) => res.data)
}

View File

@@ -1,6 +1,5 @@
import { AuthManager } from '../AuthManager'
import * as dotenv from 'dotenv'
import * as authModule from '..'
import { ServerType } from '@sasjs/utils/types'
import axios from 'axios'
import {
@@ -8,8 +7,8 @@ import {
mockLoginSuccessResponse
} from './mockResponses'
import { serialize } from '../../utils'
import { RequestClient } from '../../request/RequestClient'
jest.mock('axios')
jest.mock('../parseAndSubmitAuthorizeForm')
const mockedAxios = axios as jest.Mocked<typeof axios>
describe('AuthManager', () => {
@@ -18,6 +17,7 @@ describe('AuthManager', () => {
const serverType = ServerType.SasViya
const userName = 'test-username'
const password = 'test-password'
const requestClient = new RequestClient(serverUrl)
beforeAll(() => {
dotenv.config()
@@ -25,12 +25,16 @@ describe('AuthManager', () => {
})
it('should instantiate and set the correct URLs for a Viya server', () => {
const authManager = new AuthManager(serverUrl, serverType, authCallback)
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
expect(authManager).toBeTruthy()
expect((authManager as any).serverUrl).toEqual(serverUrl)
expect((authManager as any).serverType).toEqual(serverType)
expect((authManager as any).httpClient).toBeTruthy()
expect((authManager as any).loginUrl).toEqual(`/SASLogon/login`)
expect((authManager as any).logoutUrl).toEqual('/SASLogon/logout.do?')
})
@@ -39,18 +43,27 @@ describe('AuthManager', () => {
const authCallback = () => Promise.resolve()
const serverType = ServerType.Sas9
const authManager = new AuthManager(serverUrl, serverType, authCallback)
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
expect(authManager).toBeTruthy()
expect((authManager as any).serverUrl).toEqual(serverUrl)
expect((authManager as any).serverType).toEqual(serverType)
expect((authManager as any).httpClient).toBeTruthy()
expect((authManager as any).loginUrl).toEqual(`/SASLogon/login`)
expect((authManager as any).logoutUrl).toEqual('/SASLogon/logout?')
})
it('should call the auth callback and return when already logged in', async (done) => {
const authManager = new AuthManager(serverUrl, serverType, authCallback)
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
Promise.resolve({
isLoggedIn: true,
@@ -68,7 +81,12 @@ describe('AuthManager', () => {
})
it('should post a login request to the server if not logged in', async (done) => {
const authManager = new AuthManager(serverUrl, serverType, authCallback)
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
Promise.resolve({
isLoggedIn: false,
@@ -96,7 +114,6 @@ describe('AuthManager', () => {
loginParams,
{
withCredentials: true,
responseType: 'text',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: '*/*'
@@ -108,9 +125,14 @@ describe('AuthManager', () => {
})
it('should parse and submit the authorisation form when necessary', async (done) => {
const authManager = new AuthManager(serverUrl, serverType, authCallback)
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
jest
.spyOn(authModule, 'parseAndSubmitAuthorizeForm')
.spyOn(requestClient, 'authorize')
.mockImplementation(() => Promise.resolve())
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
Promise.resolve({
@@ -125,15 +147,19 @@ describe('AuthManager', () => {
await authManager.logIn(userName, password)
expect(authModule.parseAndSubmitAuthorizeForm).toHaveBeenCalledWith(
mockLoginAuthoriseRequiredResponse,
serverUrl
expect(requestClient.authorize).toHaveBeenCalledWith(
mockLoginAuthoriseRequiredResponse
)
done()
})
it('should check and return session information if logged in', async (done) => {
const authManager = new AuthManager(serverUrl, serverType, authCallback)
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
mockedAxios.get.mockImplementation(() =>
Promise.resolve({ data: '<button onClick="logout">' })
)
@@ -141,9 +167,12 @@ describe('AuthManager', () => {
const response = await authManager.checkSession()
expect(response.isLoggedIn).toBeTruthy()
expect(mockedAxios.get).toHaveBeenNthCalledWith(1, `/SASLogon/login`, {
withCredentials: true,
responseType: 'text',
transformResponse: undefined,
headers: {
Accept: '*/*'
Accept: '*/*',
'Content-Type': 'text/plain'
}
})
@@ -151,7 +180,12 @@ describe('AuthManager', () => {
})
it('should check and return session information if logged in', async (done) => {
const authManager = new AuthManager(serverUrl, serverType, authCallback)
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
mockedAxios.get.mockImplementation(() =>
Promise.resolve({ data: '<button onClick="logout">' })
)
@@ -159,9 +193,12 @@ describe('AuthManager', () => {
const response = await authManager.checkSession()
expect(response.isLoggedIn).toBeTruthy()
expect(mockedAxios.get).toHaveBeenNthCalledWith(1, `/SASLogon/login`, {
withCredentials: true,
responseType: 'text',
transformResponse: undefined,
headers: {
Accept: '*/*'
Accept: '*/*',
'Content-Type': 'text/plain'
}
})

View File

@@ -1,28 +1,14 @@
import { ServerType } from '@sasjs/utils/types'
import { ErrorResponse } from '..'
import { SASViyaApiClient } from '../SASViyaApiClient'
import {
ComputeJobExecutionError,
LoginRequiredError,
SASjsRequest
} from '../types'
import {
asyncForEach,
parseGeneratedCode,
parseSourceCode,
parseWeboutResponse
} from '../utils'
import { ExecuteFunction, JobExecutor } from './JobExecutor'
import { parseSasWork } from './parseSasWork'
import { ComputeJobExecutionError, LoginRequiredError } from '../types'
import { parseWeboutResponse } from '../utils'
import { BaseJobExecutor } from './JobExecutor'
export class ComputeJobExecutor implements JobExecutor {
waitingRequests: ExecuteFunction[] = []
requests: SASjsRequest[] = []
constructor(
private serverUrl: string,
private sasViyaApiClient: SASViyaApiClient
) {}
export class ComputeJobExecutor extends BaseJobExecutor {
constructor(serverUrl: string, private sasViyaApiClient: SASViyaApiClient) {
super(serverUrl, ServerType.SasViya)
}
async execute(
sasJob: string,
@@ -34,6 +20,7 @@ export class ComputeJobExecutor implements JobExecutor {
const loginCallback = loginRequiredCallback || (() => Promise.resolve())
const waitForResult = true
const expectWebout = true
return this.sasViyaApiClient
?.executeComputeJob(
sasJob,
@@ -46,7 +33,6 @@ export class ComputeJobExecutor implements JobExecutor {
)
.then((response) => {
this.appendRequest(response, sasJob, config.debug)
let responseJson
try {
@@ -67,70 +53,11 @@ export class ComputeJobExecutor implements JobExecutor {
}
if (e instanceof LoginRequiredError) {
await loginCallback()
this.waitingRequests.push(() =>
this.appendWaitingRequest(() =>
this.execute(sasJob, data, config, loginRequiredCallback)
)
}
return Promise.reject(new ErrorResponse(e?.message, e))
})
}
resendWaitingRequests = async () => {
await asyncForEach(
this.waitingRequests,
async (waitingRequest: ExecuteFunction) => {
await waitingRequest()
}
)
this.waitingRequests = []
return
}
getRequests = () => this.requests
clearRequests = () => {
this.requests = []
}
private async appendRequest(response: any, program: string, debug: boolean) {
let sourceCode = ''
let generatedCode = ''
let sasWork = null
if (debug) {
if (response?.result && response?.log) {
sourceCode = parseSourceCode(response.log)
generatedCode = parseGeneratedCode(response.log)
if (response.log) {
sasWork = response.log
} else {
sasWork = JSON.parse(parseWeboutResponse(response.result)).WORK
}
} else if (response?.result) {
sourceCode = parseSourceCode(response.result)
generatedCode = parseGeneratedCode(response.result)
sasWork = await parseSasWork(
response.result,
debug,
this.serverUrl,
ServerType.SasViya
)
}
}
this.requests.push({
logFile: response?.log || response?.result || response,
serviceLink: program,
timestamp: new Date(),
sourceCode,
generatedCode,
SASWORK: sasWork
})
if (this.requests.length > 20) {
this.requests.splice(0, 1)
}
}
}

View File

@@ -1,24 +1,14 @@
import { ServerType } from '@sasjs/utils/types'
import { ErrorResponse } from '..'
import { SASViyaApiClient } from '../SASViyaApiClient'
import { JobExecutionError, LoginRequiredError, SASjsRequest } from '../types'
import {
asyncForEach,
parseGeneratedCode,
parseSourceCode,
parseWeboutResponse
} from '../utils'
import { ExecuteFunction, JobExecutor } from './JobExecutor'
import { parseSasWork } from './parseSasWork'
import { JobExecutionError, LoginRequiredError } from '../types'
import { parseWeboutResponse } from '../utils'
import { BaseJobExecutor } from './JobExecutor'
export class JesJobExecutor implements JobExecutor {
waitingRequests: ExecuteFunction[] = []
requests: SASjsRequest[] = []
constructor(
private serverUrl: string,
private sasViyaApiClient: SASViyaApiClient
) {}
export class JesJobExecutor extends BaseJobExecutor {
constructor(serverUrl: string, private sasViyaApiClient: SASViyaApiClient) {
super(serverUrl, ServerType.SasViya)
}
async execute(
sasJob: string,
@@ -53,70 +43,11 @@ export class JesJobExecutor implements JobExecutor {
}
if (e instanceof LoginRequiredError) {
await loginCallback()
this.waitingRequests.push(() =>
this.appendWaitingRequest(() =>
this.execute(sasJob, data, config, loginRequiredCallback)
)
}
return Promise.reject(new ErrorResponse(e?.message, e))
})
}
resendWaitingRequests = async () => {
await asyncForEach(
this.waitingRequests,
async (waitingRequest: ExecuteFunction) => {
await waitingRequest()
}
)
this.waitingRequests = []
return
}
getRequests = () => this.requests
clearRequests = () => {
this.requests = []
}
private async appendRequest(response: any, program: string, debug: boolean) {
let sourceCode = ''
let generatedCode = ''
let sasWork = null
if (debug) {
if (response?.result && response?.log) {
sourceCode = parseSourceCode(response.log)
generatedCode = parseGeneratedCode(response.log)
if (response.log) {
sasWork = response.log
} else {
sasWork = JSON.parse(parseWeboutResponse(response.result)).WORK
}
} else if (response?.result) {
sourceCode = parseSourceCode(response.result)
generatedCode = parseGeneratedCode(response.result)
sasWork = await parseSasWork(
response.result,
debug,
this.serverUrl,
ServerType.SasViya
)
}
}
this.requests.push({
logFile: response?.log || response?.result || response,
serviceLink: program,
timestamp: new Date(),
sourceCode,
generatedCode,
SASWORK: sasWork
})
if (this.requests.length > 20) {
this.requests.splice(0, 1)
}
}
}

View File

@@ -1,4 +1,12 @@
import { ServerType } from '@sasjs/utils/types'
import { parseSasWork } from '.'
import { SASjsRequest } from '../types'
import {
asyncForEach,
parseGeneratedCode,
parseSourceCode,
parseWeboutResponse
} from '../utils'
export type ExecuteFunction = () => Promise<any>
@@ -10,8 +18,89 @@ export interface JobExecutor {
loginRequiredCallback?: any,
accessToken?: string
) => Promise<any>
waitingRequests: ExecuteFunction[]
resendWaitingRequests: () => Promise<void>
getRequests: () => SASjsRequest[]
clearRequests: () => void
}
export abstract class BaseJobExecutor implements JobExecutor {
constructor(protected serverUrl: string, protected serverType: ServerType) {}
private waitingRequests: ExecuteFunction[] = []
private requests: SASjsRequest[] = []
abstract execute(
sasJob: string,
data: any,
config: any,
loginRequiredCallback?: any,
accessToken?: string | undefined
): Promise<any>
resendWaitingRequests = async () => {
await asyncForEach(
this.waitingRequests,
async (waitingRequest: ExecuteFunction) => {
await waitingRequest()
}
)
this.waitingRequests = []
return
}
getRequests = () => this.requests
clearRequests = () => {
this.requests = []
}
protected appendWaitingRequest(request: ExecuteFunction) {
this.waitingRequests.push(request)
}
protected async appendRequest(
response: any,
program: string,
debug: boolean
) {
let sourceCode = ''
let generatedCode = ''
let sasWork = null
if (debug) {
if (response?.result && response?.log) {
sourceCode = parseSourceCode(response.log)
generatedCode = parseGeneratedCode(response.log)
if (response.log) {
sasWork = response.log
} else {
sasWork = JSON.parse(parseWeboutResponse(response.result)).WORK
}
} else if (response?.result) {
sourceCode = parseSourceCode(response.result)
generatedCode = parseGeneratedCode(response.result)
sasWork = await parseSasWork(
response.result,
debug,
this.serverUrl,
this.serverType
)
}
}
this.requests.push({
logFile: response?.log || response?.result || response,
serviceLink: program,
timestamp: new Date(),
sourceCode,
generatedCode,
SASWORK: sasWork
})
if (this.requests.length > 20) {
this.requests.splice(0, 1)
}
}
}

View File

@@ -4,28 +4,19 @@ import { generateFileUploadForm } from '../file/generateFileUploadForm'
import { generateTableUploadForm } from '../file/generateTableUploadForm'
import { RequestClient } from '../request/RequestClient'
import { SASViyaApiClient } from '../SASViyaApiClient'
import { SASjsRequest } from '../types'
import {
asyncForEach,
isRelativePath,
parseGeneratedCode,
parseSourceCode,
parseWeboutResponse
} from '../utils'
import { ExecuteFunction, JobExecutor } from './JobExecutor'
import { parseSasWork } from './parseSasWork'
export class WebJobExecutor implements JobExecutor {
waitingRequests: ExecuteFunction[] = []
requests: SASjsRequest[] = []
import { isRelativePath } from '../utils'
import { BaseJobExecutor } from './JobExecutor'
export class WebJobExecutor extends BaseJobExecutor {
constructor(
private serverUrl: string,
private serverType: ServerType,
serverUrl: string,
serverType: ServerType,
private jobsPath: string,
private requestClient: RequestClient,
private sasViyaApiClient: SASViyaApiClient
) {}
) {
super(serverUrl, serverType)
}
async execute(
sasJob: string,
@@ -89,15 +80,7 @@ export class WebJobExecutor implements JobExecutor {
}
}
return this.requestClient!.post(
apiUrl,
formData,
undefined,
'application/json',
{
referrerPolicy: 'same-origin'
}
)
return this.requestClient!.post(apiUrl, formData, undefined)
.then(async (res) => {
this.appendRequest(res, sasJob, config.debug)
return res.result
@@ -108,7 +91,7 @@ export class WebJobExecutor implements JobExecutor {
}
if (e instanceof LoginRequiredError) {
await loginCallback()
this.waitingRequests.push(() =>
this.appendWaitingRequest(() =>
this.execute(sasJob, data, config, loginRequiredCallback)
)
}
@@ -116,24 +99,6 @@ export class WebJobExecutor implements JobExecutor {
})
}
resendWaitingRequests = async () => {
await asyncForEach(
this.waitingRequests,
async (waitingRequest: ExecuteFunction) => {
await waitingRequest()
}
)
this.waitingRequests = []
return
}
getRequests = () => this.requests
clearRequests = () => {
this.requests = []
}
private async getJobUri(sasJob: string) {
if (!this.sasViyaApiClient) return ''
let uri = ''
@@ -174,47 +139,6 @@ export class WebJobExecutor implements JobExecutor {
return requestParams
}
private async appendRequest(response: any, program: string, debug: boolean) {
let sourceCode = ''
let generatedCode = ''
let sasWork = null
if (debug) {
if (response?.result && response?.log) {
sourceCode = parseSourceCode(response.log)
generatedCode = parseGeneratedCode(response.log)
if (response.log) {
sasWork = response.log
} else {
sasWork = JSON.parse(parseWeboutResponse(response.result)).WORK
}
} else if (response?.result) {
sourceCode = parseSourceCode(response.result)
generatedCode = parseGeneratedCode(response.result)
sasWork = await parseSasWork(
response.result,
debug,
this.serverUrl,
this.serverType
)
}
}
this.requests.push({
logFile: response?.log || response?.result || response,
serviceLink: program,
timestamp: new Date(),
sourceCode,
generatedCode,
SASWORK: sasWork
})
if (this.requests.length > 20) {
this.requests.splice(0, 1)
}
}
private parseSAS9ErrorResponse(response: string) {
const logLines = response.split('\n')
const parsedLines: string[] = []

View File

@@ -1,5 +1,6 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { CsrfToken, JobExecutionError } from '..'
import { isAuthorizeFormRequired } from '../auth'
import { LoginRequiredError } from '../types'
import { AuthorizeError } from '../types/AuthorizeError'
import { NotFoundError } from '../types/NotFoundError'
@@ -33,10 +34,11 @@ export interface HttpClient {
): Promise<{ result: T; etag: string }>
getCsrfToken(type: 'general' | 'file'): CsrfToken | undefined
clearCsrfTokens(): void
}
export class RequestClient implements HttpClient {
private csrfToken: CsrfToken | undefined
private csrfToken: CsrfToken = { headerName: '', value: '' }
private fileUploadCsrfToken: CsrfToken | undefined
private httpClient: AxiosInstance
@@ -48,6 +50,11 @@ export class RequestClient implements HttpClient {
return type === 'file' ? this.fileUploadCsrfToken : this.csrfToken
}
public clearCsrfTokens() {
this.csrfToken = { headerName: '', value: '' }
this.fileUploadCsrfToken = { headerName: '', value: '' }
}
public async get<T>(
url: string,
accessToken: string | undefined,
@@ -68,27 +75,37 @@ export class RequestClient implements HttpClient {
requestConfig.transformResponse = undefined
}
try {
const response = await this.httpClient.get<T>(url, requestConfig)
const etag = response?.headers ? response.headers['etag'] : ''
return this.httpClient
.get<T>(url, requestConfig)
.then((response) => {
throwIfError(response)
const etag = response?.headers ? response.headers['etag'] : ''
return {
result: response.data as T,
etag
}
} catch (e) {
const response = e.response as AxiosResponse
if (response?.status === 403 || response?.status === 449) {
this.parseAndSetCsrfToken(response)
if (this.csrfToken) {
return {
result: response.data as T,
etag
}
})
.catch(async (e) => {
const response = e.response as AxiosResponse
if (e instanceof AuthorizeError) {
const res = await this.get(e.confirmUrl, undefined, 'text/plain')
if (isAuthorizeFormRequired(res.result as string)) {
await this.authorize(res.result as string)
}
return this.get<T>(url, accessToken, contentType, overrideHeaders)
}
if (response?.status === 403 || response?.status === 449) {
this.parseAndSetCsrfToken(response)
if (this.csrfToken.headerName && this.csrfToken.value) {
return this.get<T>(url, accessToken, contentType, overrideHeaders)
}
throw e
} else if (response?.status === 404) {
throw new NotFoundError(url)
}
throw e
} else if (response?.status === 404) {
throw new NotFoundError(url)
}
throw e
}
})
}
public post<T>(
@@ -99,7 +116,7 @@ export class RequestClient implements HttpClient {
overrideHeaders: { [key: string]: string | number } = {}
): Promise<{ result: T; etag: string }> {
const headers = {
...this.getHeaders(accessToken, contentType),
...this.getHeaders(accessToken, contentType, url.endsWith('login.do')),
...overrideHeaders
}
@@ -116,13 +133,25 @@ export class RequestClient implements HttpClient {
.catch(async (e) => {
const response = e.response as AxiosResponse
if (e instanceof AuthorizeError) {
await this.post(e.confirmUrl, { value: true }, undefined)
return this.post<T>(url, data, accessToken)
const res = await this.get(e.confirmUrl, undefined, 'text/plain')
if (isAuthorizeFormRequired(res.result as string)) {
await this.authorize(res.result as string)
}
return this.post<T>(
url,
data,
accessToken,
contentType,
overrideHeaders
)
}
if (e instanceof LoginRequiredError && this.csrfToken) {
this.csrfToken.value = ''
}
if (response?.status === 403 || response?.status === 449) {
this.parseAndSetCsrfToken(response)
if (this.csrfToken) {
if (this.csrfToken.headerName && this.csrfToken.value) {
return this.post<T>(url, data, accessToken)
}
throw e
@@ -157,7 +186,7 @@ export class RequestClient implements HttpClient {
if (response?.status === 403 || response?.status === 449) {
this.parseAndSetCsrfToken(response)
if (this.csrfToken) {
if (this.csrfToken.headerName && this.csrfToken.value) {
return this.put<T>(url, data, accessToken)
}
throw e
@@ -192,7 +221,7 @@ export class RequestClient implements HttpClient {
if (response?.status === 403 || response?.status === 449) {
this.parseAndSetCsrfToken(response)
if (this.csrfToken) {
if (this.csrfToken.headerName && this.csrfToken.value) {
return this.delete<T>(url, accessToken)
}
throw e
@@ -222,7 +251,7 @@ export class RequestClient implements HttpClient {
if (response?.status === 403 || response?.status === 449) {
this.parseAndSetCsrfToken(response)
if (this.csrfToken) {
if (this.csrfToken.headerName && this.csrfToken.value) {
return this.patch<T>(url, accessToken)
}
throw e
@@ -264,9 +293,64 @@ export class RequestClient implements HttpClient {
}
}
public authorize = async (response: string) => {
let authUrl: string | null = null
const params: any = {}
const responseBody = response.split('<body>')[1].split('</body>')[0]
const bodyElement = document.createElement('div')
bodyElement.innerHTML = responseBody
const form = bodyElement.querySelector('#application_authorization')
authUrl = form
? this.httpClient.defaults.baseURL! + form.getAttribute('action')
: null
const inputs: any = form?.querySelectorAll('input')
for (const input of inputs) {
if (input.name === 'user_oauth_approval') {
input.value = 'true'
}
params[input.name] = input.value
}
const csrfTokenKey = Object.keys(params).find((k) =>
k?.toLowerCase().includes('csrf')
)
if (csrfTokenKey) {
this.csrfToken.value = params[csrfTokenKey]
this.csrfToken.headerName = this.csrfToken.headerName || 'x-csrf-token'
}
const formData = new FormData()
for (const key in params) {
if (params.hasOwnProperty(key)) {
formData.append(key, params[key])
}
}
if (!authUrl) {
throw new Error('Auth Form URL is null or undefined.')
}
return await this.httpClient
.post(authUrl, formData, {
responseType: 'text',
headers: { Accept: '*/*', 'Content-Type': 'text/plain' }
})
.then((res) => res.data)
.catch((error) => {
console.log(error)
})
}
private getHeaders = (
accessToken: string | undefined,
contentType: string
contentType: string,
appendCsrfToken = true
) => {
const headers: any = {
'Content-Type': contentType
@@ -280,7 +364,7 @@ export class RequestClient implements HttpClient {
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
if (this.csrfToken?.value) {
if (this.csrfToken.headerName && this.csrfToken.value) {
headers[this.csrfToken.headerName] = this.csrfToken.value
}
@@ -332,10 +416,8 @@ const throwIfError = (response: AxiosResponse) => {
throw new LoginRequiredError()
}
if (response.data?.auth_request) {
throw new AuthorizeError(
response.data.message,
response.data.options.confirm.location
)
const authorizeRequestUrl = response.request.responseURL
throw new AuthorizeError(response.data.message, authorizeRequestUrl)
}
const error = parseError(response.data as string)