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

chore(merge): Merge branch 'master' into redirected-login

This commit is contained in:
Saad Jutt
2021-09-07 05:05:51 +05:00
11 changed files with 828 additions and 23375 deletions

1753
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -42,34 +42,34 @@
"devDependencies": {
"@types/axios": "^0.14.0",
"@types/form-data": "^2.5.0",
"@types/jest": "^26.0.24",
"@types/jest": "^27.0.1",
"@types/mime": "^2.0.3",
"@types/tough-cookie": "^4.0.1",
"copyfiles": "^2.4.1",
"cp": "^0.2.0",
"dotenv": "^10.0.0",
"jest": "^27.0.6",
"jest": "^27.1.0",
"jest-extended": "^0.11.5",
"node-polyfill-webpack-plugin": "^1.1.4",
"path": "^0.12.7",
"process": "^0.11.10",
"rimraf": "^3.0.2",
"semantic-release": "^17.4.4",
"terser-webpack-plugin": "^5.1.4",
"semantic-release": "^17.4.7",
"terser-webpack-plugin": "^5.2.0",
"ts-jest": "^27.0.3",
"ts-loader": "^9.2.2",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"typedoc": "^0.21.4",
"typedoc": "^0.21.9",
"typedoc-neo-theme": "^1.1.1",
"typedoc-plugin-external-module-name": "^4.0.6",
"typescript": "^4.3.5",
"typescript": "4.3.5",
"webpack": "^5.44.0",
"webpack-cli": "^4.7.2"
},
"main": "index.js",
"dependencies": {
"@sasjs/utils": "^2.27.1",
"@sasjs/utils": "^2.30.0",
"axios": "^0.21.1",
"axios-cookiejar-support": "^1.0.1",
"form-data": "^4.0.0",

File diff suppressed because it is too large Load Diff

View File

@@ -41,6 +41,19 @@ export const basicTests = (
assertion: (response: any) =>
response && response.isLoggedIn && response.userName === userName
},
{
title: 'Fetch username for already logged in user',
description: 'Should log the user in',
test: async () => {
await adapter.logIn(userName, password)
const newAdapterIns = new SASjs(adapter.getSasjsConfig())
return await newAdapterIns.checkSession()
},
assertion: (response: any) =>
response?.isLoggedIn && response?.userName === userName
},
{
title: 'Multiple Log in attempts',
description:
@@ -48,7 +61,7 @@ export const basicTests = (
test: async () => {
await adapter.logOut()
await adapter.logIn('invalid', 'invalid')
return adapter.logIn(userName, password)
return await adapter.logIn(userName, password)
},
assertion: (response: any) =>
response && response.isLoggedIn && response.userName === userName
@@ -151,7 +164,7 @@ export const basicTests = (
description:
'Should complete successful request with extra attributes present in response',
test: async () => {
const config = {
const config: Partial<SASjsConfig> = {
useComputeApi: false
}

View File

@@ -70,43 +70,61 @@ export class AuthManager {
* Logs into the SAS server with the supplied credentials.
* @param username - a string representing the username.
* @param password - a string representing the password.
* @returns - a boolean `isLoggedin` and a string `username`
*/
public async logIn(username: string, password: string) {
const loginParams: any = {
public async logIn(
username: string,
password: string
): Promise<{
isLoggedIn: boolean
userName: string
}> {
const loginParams = {
_service: 'default',
username,
password
}
this.userName = loginParams.username
let {
isLoggedIn: isLoggedInAlready,
loginForm,
userName: currentSessionUsername
} = await this.checkSession()
const { isLoggedIn, loginForm } = await this.checkSession()
if (isLoggedInAlready) {
if (currentSessionUsername === loginParams.username) {
await this.loginCallback()
if (isLoggedIn) {
await this.loginCallback()
return {
isLoggedIn,
userName: this.userName
this.userName = currentSessionUsername!
return {
isLoggedIn: true,
userName: this.userName
}
} else {
this.logOut()
}
}
} else this.userName = ''
let loginResponse = await this.sendLoginRequest(loginForm, loginParams)
let loggedIn = isLogInSuccess(loginResponse)
let isLoggedIn = isLogInSuccess(loginResponse)
if (!loggedIn) {
if (!isLoggedIn) {
if (isCredentialsVerifyError(loginResponse)) {
const newLoginForm = await this.getLoginForm(loginResponse)
loginResponse = await this.sendLoginRequest(newLoginForm, loginParams)
}
const currentSession = await this.checkSession()
loggedIn = currentSession.isLoggedIn
const res = await this.checkSession()
isLoggedIn = res.isLoggedIn
if (isLoggedIn) this.userName = res.userName!
} else {
this.userName = loginParams.username
}
if (loggedIn) {
if (isLoggedIn) {
if (this.serverType === ServerType.Sas9) {
const casAuthenticationUrl = `${this.serverUrl}/SASStoredProcess/j_spring_cas_security_check`
@@ -117,10 +135,10 @@ export class AuthManager {
}
this.loginCallback()
}
} else this.userName = ''
return {
isLoggedIn: !!loggedIn,
isLoggedIn,
userName: this.userName
}
}
@@ -150,14 +168,21 @@ export class AuthManager {
/**
* Checks whether a session is active, or login is required.
* @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`.
* @returns - a promise which resolves with an object containing three values
* - a boolean `isLoggedIn`
* - a string `userName` and
* - a form `loginForm` if not loggedin.
*/
public async checkSession() {
public async checkSession(): Promise<{
isLoggedIn: boolean
userName?: string
loginForm?: any
}> {
//For VIYA we will send request on API endpoint. Which is faster then pinging SASJobExecution.
//For SAS9 we will send request on SASStoredProcess
const url =
this.serverType === 'SASVIYA'
? `${this.serverUrl}/identities`
? `${this.serverUrl}/identities/users/@currentUser`
: `${this.serverUrl}/SASStoredProcess`
const { result: loginResponse } = await this.requestClient
@@ -167,6 +192,10 @@ export class AuthManager {
})
const isLoggedIn = loginResponse !== 'authErr'
const userName = isLoggedIn
? this.extractUserName(loginResponse)
: undefined
let loginForm = null
if (!isLoggedIn) {
@@ -185,11 +214,29 @@ export class AuthManager {
return Promise.resolve({
isLoggedIn,
userName: this.userName,
userName: userName?.toLowerCase(),
loginForm
})
}
private extractUserName = (response: any): string => {
switch (this.serverType) {
case ServerType.SasViya:
return response?.id
case ServerType.Sas9:
const matched = response?.match(/"title":"Log Off [0-1a-zA-Z ]*"/)
const username = matched?.[0].slice(17, -1)
if (!username.includes(' ')) return username
return username
.split(' ')
.map((name: string) => name.slice(0, 3).toLowerCase())
.join('')
}
}
private getLoginForm(response: any) {
const pattern: RegExp = /<form.+action="(.*Logon[^"]*).*>/
const matches = pattern.exec(response)

View File

@@ -67,7 +67,7 @@ describe('AuthManager', () => {
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
Promise.resolve({
isLoggedIn: true,
userName: 'test',
userName,
loginForm: 'test'
})
)
@@ -89,7 +89,6 @@ describe('AuthManager', () => {
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
Promise.resolve({
isLoggedIn: false,
userName: 'test',
loginForm: { name: 'test' }
})
)
@@ -175,7 +174,7 @@ describe('AuthManager', () => {
expect(response.isLoggedIn).toBeTruthy()
expect(mockedAxios.get).toHaveBeenNthCalledWith(
1,
`http://test-server.com/identities`,
`http://test-server.com/identities/users/@currentUser`,
{
withCredentials: true,
responseType: 'text',

View File

@@ -7,6 +7,7 @@ import {
} from '../types/errors'
import { ExtraResponseAttributes } from '@sasjs/utils/types'
import { BaseJobExecutor } from './JobExecutor'
import { appendExtraResponseAttributes } from '../utils'
export class JesJobExecutor extends BaseJobExecutor {
constructor(serverUrl: string, private sasViyaApiClient: SASViyaApiClient) {
@@ -29,21 +30,10 @@ export class JesJobExecutor extends BaseJobExecutor {
.then((response: any) => {
this.appendRequest(response, sasJob, config.debug)
let responseObject = {}
if (extraResponseAttributes && extraResponseAttributes.length > 0) {
const extraAttributes = extraResponseAttributes.reduce(
(map: any, obj: any) => ((map[obj] = response[obj]), map),
{}
)
responseObject = {
result: response.result,
...extraAttributes
}
} else {
responseObject = response.result
}
const responseObject = appendExtraResponseAttributes(
response,
extraResponseAttributes
)
resolve(responseObject)
})

View File

@@ -1,4 +1,8 @@
import { ServerType } from '@sasjs/utils/types'
import {
AuthConfig,
ExtraResponseAttributes,
ServerType
} from '@sasjs/utils/types'
import {
ErrorResponse,
JobExecutionError,
@@ -11,7 +15,8 @@ import { SASViyaApiClient } from '../SASViyaApiClient'
import {
isRelativePath,
getValidJson,
parseSasViyaDebugResponse
parseSasViyaDebugResponse,
appendExtraResponseAttributes
} from '../utils'
import { BaseJobExecutor } from './JobExecutor'
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
@@ -37,7 +42,9 @@ export class WebJobExecutor extends BaseJobExecutor {
sasJob: string,
data: any,
config: any,
loginRequiredCallback?: any
loginRequiredCallback?: any,
authConfig?: AuthConfig,
extraResponseAttributes: ExtraResponseAttributes[] = []
) {
const loginCallback = loginRequiredCallback || (() => Promise.resolve())
@@ -118,27 +125,33 @@ export class WebJobExecutor extends BaseJobExecutor {
const requestPromise = new Promise((resolve, reject) => {
this.requestClient!.post(apiUrl, formData, undefined)
.then(async (res: any) => {
if (this.serverType === ServerType.SasViya && config.debug) {
const jsonResponse = await parseSasViyaDebugResponse(
res.result,
this.requestClient,
this.serverUrl
)
this.appendRequest(res, sasJob, config.debug)
resolve(jsonResponse)
}
if (this.serverType === ServerType.Sas9 && config.debug) {
let jsonResponse = res.result
if (typeof res.result === 'string')
jsonResponse = parseWeboutResponse(res.result, apiUrl)
let jsonResponse = res.result
getValidJson(jsonResponse)
this.appendRequest(res, sasJob, config.debug)
resolve(res.result)
if (config.debug) {
switch (this.serverType) {
case ServerType.SasViya:
jsonResponse = await parseSasViyaDebugResponse(
res.result,
this.requestClient,
this.serverUrl
)
break
case ServerType.Sas9:
jsonResponse =
typeof res.result === 'string'
? parseWeboutResponse(res.result, apiUrl)
: res.result
break
}
}
this.appendRequest(res, sasJob, config.debug)
getValidJson(res.result as string)
resolve(res.result)
const responseObject = appendExtraResponseAttributes(
{ result: jsonResponse },
extraResponseAttributes
)
resolve(responseObject)
})
.catch(async (e: Error) => {
if (e instanceof JobExecutionError) {
@@ -153,7 +166,9 @@ export class WebJobExecutor extends BaseJobExecutor {
sasJob,
data,
config,
loginRequiredCallback
loginRequiredCallback,
authConfig,
extraResponseAttributes
).then(
(res: any) => {
resolve(res)

View File

@@ -499,46 +499,60 @@ export const throwIfError = (response: AxiosResponse) => {
}
const parseError = (data: string) => {
if (!data) return null
try {
const responseJson = JSON.parse(data?.replace(/[\n\r]/g, ' '))
return responseJson.errorCode && responseJson.message
? new JobExecutionError(
responseJson.errorCode,
responseJson.message,
data?.replace(/[\n\r]/g, ' ')
)
: null
} catch (_) {
try {
const hasError = data?.includes('{"errorCode')
if (hasError) {
const parts = data.split('{"errorCode')
if (parts.length > 1) {
const error = '{"errorCode' + parts[1].split('"}')[0] + '"}'
const errorJson = JSON.parse(error.replace(/[\n\r]/g, ' '))
return new JobExecutionError(
errorJson.errorCode,
errorJson.message,
data?.replace(/[\n\r]/g, '\n')
)
}
return null
}
try {
const hasError = !!data?.match(/stored process not found: /i)
if (hasError) {
const parts = data.split(/stored process not found: /i)
if (parts.length > 1) {
const storedProcessPath = parts[1].split('<i>')[1].split('</i>')[0]
const message = `Stored process not found: ${storedProcessPath}`
return new JobExecutionError(404, message, '')
}
}
} catch (_) {
return null
}
} catch (_) {
return null
if (responseJson.errorCode && responseJson.message) {
return new JobExecutionError(
responseJson.errorCode,
responseJson.message,
data?.replace(/[\n\r]/g, ' ')
)
}
}
} catch (_) {}
try {
const hasError = data?.includes('{"errorCode')
if (hasError) {
const parts = data.split('{"errorCode')
if (parts.length > 1) {
const error = '{"errorCode' + parts[1].split('"}')[0] + '"}'
const errorJson = JSON.parse(error.replace(/[\n\r]/g, ' '))
return new JobExecutionError(
errorJson.errorCode,
errorJson.message,
data?.replace(/[\n\r]/g, '\n')
)
}
}
} catch (_) {}
try {
const hasError = !!data?.match(/stored process not found: /i)
if (hasError) {
const parts = data.split(/stored process not found: /i)
if (parts.length > 1) {
const storedProcessPath = parts[1].split('<i>')[1].split('</i>')[0]
const message = `Stored process not found: ${storedProcessPath}`
return new JobExecutionError(404, message, '')
}
}
} catch (_) {}
try {
const hasError =
!!data?.match(/Stored Process Error/i) &&
!!data?.match(/This request completed with errors./i)
if (hasError) {
const parts = data.split('<h2>SAS Log</h2>')
if (parts.length > 1) {
const log = parts[1].split('<pre>')[1].split('</pre>')[0]
const message = `This request completed with errors.`
return new JobExecutionError(404, message, log)
}
}
} catch (_) {}
return null
}

View File

@@ -0,0 +1,22 @@
import { ExtraResponseAttributes } from '@sasjs/utils/types'
export async function appendExtraResponseAttributes(
response: any,
extraResponseAttributes: ExtraResponseAttributes[]
) {
let responseObject = {}
if (extraResponseAttributes?.length) {
const extraAttributes = extraResponseAttributes.reduce(
(map: any, obj: any) => ((map[obj] = response[obj]), map),
{}
)
responseObject = {
result: response.result,
...extraAttributes
}
} else responseObject = response.result
return responseObject
}

View File

@@ -16,3 +16,4 @@ export * from './parseWeboutResponse'
export * from './fetchLogByChunks'
export * from './getValidJson'
export * from './parseViyaDebugResponse'
export * from './appendExtraResponseAttributes'