1
0
mirror of https://github.com/sasjs/adapter.git synced 2026-01-04 03:00:05 +00:00

Compare commits

...

30 Commits

Author SHA1 Message Date
Allan Bowe
2cfba99bda Merge pull request #540 from sasjs/issue-532
fix: move SASjsRequest array from BaseJobExecutor class to RequestClient class
2021-09-11 17:17:33 +03:00
Allan Bowe
a181914c36 Merge pull request #522 from sasjs/redirected-login
feat(auth): redirected login
2021-09-10 18:03:24 +03:00
Saad Jutt
539405e249 chore: fixed sasjs-tests build 2021-09-09 15:30:12 +05:00
Krishna Acondy
d9c27efa8d chore(auth-manager): rename variable 2021-09-09 07:51:07 +01:00
Krishna Acondy
2ccc7b5499 chore(request-client): rename method 2021-09-09 07:41:20 +01:00
Saad Jutt
4623b9665b chore: login prompt dialog bug fixed 2021-09-09 11:29:12 +05:00
9c099b899c fix: If the request client has already been instantiated, update config 2021-09-09 11:03:48 +05:00
Saad Jutt
3ae0809ee5 test(AuthManager): improved coverage 2021-09-09 04:37:30 +05:00
d52c5b26a0 chore: merge master into issue-532 2021-09-08 15:16:06 +05:00
Saad Jutt
0ea6e839ac test: added for verifySasLogin 2021-09-08 15:08:55 +05:00
46ef7b19f6 fix: before instantiating RequestClient check if its already instantiated 2021-09-08 15:02:22 +05:00
Saad Jutt
a00bf5ba67 test(AuthManager): specs added for redirected login 2021-09-08 13:04:03 +05:00
Saad Jutt
e0b09adbba chore: code refactor renamed variables/functions 2021-09-08 06:30:53 +05:00
Saad Jutt
19a57dbf6e chore: code refactor renamed variables/functions 2021-09-08 05:49:24 +05:00
Saad Jutt
cd2b32f2f4 test(checkSession): extract username from server response 2021-09-07 06:08:17 +05:00
Saad Jutt
a1f5355d6a chore: fetch username for Redirected-Login and return 2021-09-07 05:26:42 +05:00
Saad Jutt
0972c0deaa chore(merge): Merge branch 'master' into redirected-login 2021-09-07 05:05:51 +05:00
73f50c0435 chore: merge master into issue-532 2021-09-06 13:39:58 +05:00
5ee57f3d07 chore: added jsdoc header 2021-09-03 14:54:35 +05:00
146b0715bf fix: set debug: false in config of fileUploader tests 2021-09-03 13:57:39 +05:00
dfc1d567a5 fix: append sasjs requests array from uploadFile 2021-09-03 13:55:49 +05:00
779200f5fc fix: throw error if null or undefined is passed to getValidJson 2021-09-03 13:54:02 +05:00
cf4c4cfca9 fix: move SASjsRequest array from BaseJobExecutor class to RequestClient class 2021-09-03 13:51:58 +05:00
Saad Jutt
f40a86f0f6 chore(redirectLogin): onLoggedOut callback should be an async 2021-09-02 13:43:07 +05:00
Saad Jutt
f231edb4a6 chore: redirect login with onLoggedOut callback 2021-08-31 12:36:20 +05:00
Saad Jutt
389ef94cd5 feat(login): redirect mechanism - in page link to open popup 2021-08-28 10:01:20 +05:00
Saad Jutt
4c90f66dbc chore(merge): Merge branch 'master' into redirected-login 2021-08-27 23:34:52 +05:00
Saad Jutt
1a59f95be7 fix: split code to files + corrected usage of loginCallback 2021-08-25 07:55:04 +05:00
Saad Jutt
97918f301b chore(redirectLogin): centered popup + verifying sas9 login + sasviya login fixes 2021-08-22 03:57:23 +05:00
Krishna Acondy
830a907bd1 feat(login): add redirected login mechanism 2021-08-21 21:36:50 +01:00
27 changed files with 1334 additions and 263 deletions

View File

@@ -1,4 +1,4 @@
import SASjs, { SASjsConfig } from '@sasjs/adapter' import SASjs, { LoginMechanism, SASjsConfig } from '@sasjs/adapter'
import { TestSuite } from '@sasjs/test-framework' import { TestSuite } from '@sasjs/test-framework'
import { ServerType } from '@sasjs/utils/types' import { ServerType } from '@sasjs/utils/types'
@@ -13,7 +13,8 @@ const defaultConfig: SASjsConfig = {
debug: false, debug: false,
contextName: 'SAS Job Execution compute context', contextName: 'SAS Job Execution compute context',
useComputeApi: false, useComputeApi: false,
allowInsecureRequests: false allowInsecureRequests: false,
loginMechanism: LoginMechanism.Default
} }
const customConfig = { const customConfig = {

View File

@@ -66,6 +66,7 @@ export class FileUploader {
return this.requestClient return this.requestClient
.post(uploadUrl, formData, undefined, 'application/json', headers) .post(uploadUrl, formData, undefined, 'application/json', headers)
.then(async (res) => { .then(async (res) => {
this.requestClient!.appendRequest(res, sasJob, this.sasjsConfig.debug)
if ( if (
this.sasjsConfig.serverType === ServerType.SasViya && this.sasjsConfig.serverType === ServerType.SasViya &&
this.sasjsConfig.debug this.sasjsConfig.debug

View File

@@ -51,6 +51,16 @@ export class SASViyaApiClient {
) )
private folderMap = new Map<string, Job[]>() private folderMap = new Map<string, Job[]>()
/**
* A helper method used to call appendRequest method of RequestClient
* @param response - response from sasjs request
* @param program - name of program
* @param debug - a boolean that indicates whether debug was enabled or not
*/
public appendRequest(response: any, program: string, debug: boolean) {
this.requestClient!.appendRequest(response, program, debug)
}
public get debug() { public get debug() {
return this._debug return this._debug
} }

View File

@@ -1,5 +1,11 @@
import { compareTimestamps, asyncForEach } from './utils' import { compareTimestamps, asyncForEach } from './utils'
import { SASjsConfig, UploadFile, EditContextInput, PollOptions } from './types' import {
SASjsConfig,
UploadFile,
EditContextInput,
PollOptions,
LoginMechanism
} from './types'
import { SASViyaApiClient } from './SASViyaApiClient' import { SASViyaApiClient } from './SASViyaApiClient'
import { SAS9ApiClient } from './SAS9ApiClient' import { SAS9ApiClient } from './SAS9ApiClient'
import { FileUploader } from './FileUploader' import { FileUploader } from './FileUploader'
@@ -19,6 +25,7 @@ import {
Sas9JobExecutor Sas9JobExecutor
} from './job-execution' } from './job-execution'
import { ErrorResponse } from './types/errors' import { ErrorResponse } from './types/errors'
import { LoginOptions, LoginResult } from './types/Login'
const defaultConfig: SASjsConfig = { const defaultConfig: SASjsConfig = {
serverUrl: '', serverUrl: '',
@@ -29,7 +36,8 @@ const defaultConfig: SASjsConfig = {
debug: false, debug: false,
contextName: 'SAS Job Execution compute context', contextName: 'SAS Job Execution compute context',
useComputeApi: null, useComputeApi: null,
allowInsecureRequests: false allowInsecureRequests: false,
loginMechanism: LoginMechanism.Default
} }
/** /**
@@ -526,8 +534,27 @@ export default class SASjs {
* @param username - a string representing the username. * @param username - a string representing the username.
* @param password - a string representing the password. * @param password - a string representing the password.
*/ */
public async logIn(username: string, password: string) { public async logIn(
return this.authManager!.logIn(username, password) username?: string,
password?: string,
options: LoginOptions = {}
): Promise<LoginResult> {
if (this.sasjsConfig.loginMechanism === LoginMechanism.Default) {
if (!username || !password) {
throw new Error(
'A username and password are required when using the default login mechanism.'
)
}
return this.authManager!.logIn(username, password)
}
if (typeof window === typeof undefined) {
throw new Error(
'The redirected login mechanism is only available for use in the browser.'
)
}
return this.authManager!.redirectedLogIn(options)
} }
/** /**
@@ -878,20 +905,18 @@ export default class SASjs {
}) })
} }
/**
* this method returns an array of SASjsRequest
* @returns SASjsRequest[]
*/
public getSasRequests() { public getSasRequests() {
const requests = [ const requests = [...this.requestClient!.getRequests()]
...this.webJobExecutor!.getRequests(),
...this.computeJobExecutor!.getRequests(),
...this.jesJobExecutor!.getRequests()
]
const sortedRequests = requests.sort(compareTimestamps) const sortedRequests = requests.sort(compareTimestamps)
return sortedRequests return sortedRequests
} }
public clearSasRequests() { public clearSasRequests() {
this.webJobExecutor!.clearRequests() this.requestClient!.clearRequests()
this.computeJobExecutor!.clearRequests()
this.jesJobExecutor!.clearRequests()
} }
private setupConfiguration() { private setupConfiguration() {
@@ -914,10 +939,17 @@ 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( if (!this.requestClient) {
this.sasjsConfig.serverUrl, this.requestClient = new RequestClient(
this.sasjsConfig.allowInsecureRequests this.sasjsConfig.serverUrl,
) this.sasjsConfig.allowInsecureRequests
)
} else {
this.requestClient.setConfig(
this.sasjsConfig.serverUrl,
this.sasjsConfig.allowInsecureRequests
)
}
this.jobsPath = this.jobsPath =
this.sasjsConfig.serverType === ServerType.SasViya this.sasjsConfig.serverType === ServerType.SasViya

View File

@@ -4,7 +4,7 @@ import { getTokens } from '../../auth/getTokens'
import { RequestClient } from '../../request/RequestClient' import { RequestClient } from '../../request/RequestClient'
import { JobStatePollError } from '../../types/errors' import { JobStatePollError } from '../../types/errors'
import { Link, WriteStream } from '../../types' import { Link, WriteStream } from '../../types'
import { isNode } from '../../utils' import { delay, isNode } from '../../utils'
export async function pollJobState( export async function pollJobState(
requestClient: RequestClient, requestClient: RequestClient,
@@ -246,5 +246,3 @@ const doPoll = async (
return { state, pollCount } return { state, pollCount }
} }
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

View File

@@ -1,11 +1,16 @@
import { ServerType } from '@sasjs/utils/types' import { ServerType } from '@sasjs/utils/types'
import { RequestClient } from '../request/RequestClient' import { RequestClient } from '../request/RequestClient'
import { LoginOptions, LoginResult } from '../types/Login'
import { serialize } from '../utils' import { serialize } from '../utils'
import { openWebPage } from './openWebPage'
import { verifySas9Login } from './verifySas9Login'
import { verifySasViyaLogin } from './verifySasViyaLogin'
export class AuthManager { export class AuthManager {
public userName = '' public userName = ''
private loginUrl: string private loginUrl: string
private logoutUrl: string private logoutUrl: string
private redirectedLoginUrl = `/SASLogon/home`
constructor( constructor(
private serverUrl: string, private serverUrl: string,
private serverType: ServerType, private serverType: ServerType,
@@ -19,19 +24,68 @@ export class AuthManager {
: '/SASLogon/logout.do?' : '/SASLogon/logout.do?'
} }
/**
* Opens Pop up window to SAS Login screen.
* And checks if user has finished login process.
*/
public async redirectedLogIn({
onLoggedOut
}: LoginOptions): Promise<LoginResult> {
const { isLoggedIn: isLoggedInAlready, userName: currentSessionUsername } =
await this.fetchUserName()
if (isLoggedInAlready) {
await this.loginCallback()
return {
isLoggedIn: true,
userName: currentSessionUsername
}
}
const loginPopup = await openWebPage(
this.redirectedLoginUrl,
'SASLogon',
{
width: 500,
height: 600
},
onLoggedOut
)
if (!loginPopup) {
return { isLoggedIn: false, userName: '' }
}
const { isLoggedIn } =
this.serverType === ServerType.SasViya
? await verifySasViyaLogin(loginPopup)
: await verifySas9Login(loginPopup)
loginPopup.close()
if (isLoggedIn) {
if (this.serverType === ServerType.Sas9) {
await this.performCASSecurityCheck()
}
const { userName } = await this.fetchUserName()
await this.loginCallback()
return { isLoggedIn: true, userName }
}
return { isLoggedIn: false, userName: '' }
}
/** /**
* Logs into the SAS server with the supplied credentials. * Logs into the SAS server with the supplied credentials.
* @param username - a string representing the username. * @param username - a string representing the username.
* @param password - a string representing the password. * @param password - a string representing the password.
* @returns - a boolean `isLoggedin` and a string `username` * @returns - a boolean `isLoggedin` and a string `username`
*/ */
public async logIn( public async logIn(username: string, password: string): Promise<LoginResult> {
username: string,
password: string
): Promise<{
isLoggedIn: boolean
userName: string
}> {
const loginParams = { const loginParams = {
_service: 'default', _service: 'default',
username, username,
@@ -54,7 +108,8 @@ export class AuthManager {
userName: this.userName userName: this.userName
} }
} else { } else {
this.logOut() await this.logOut()
loginForm = await this.getNewLoginForm()
} }
} else this.userName = '' } else this.userName = ''
@@ -72,19 +127,14 @@ export class AuthManager {
const res = await this.checkSession() const res = await this.checkSession()
isLoggedIn = res.isLoggedIn isLoggedIn = res.isLoggedIn
if (isLoggedIn) this.userName = res.userName! if (isLoggedIn) this.userName = res.userName
} else { } else {
this.userName = loginParams.username this.userName = loginParams.username
} }
if (isLoggedIn) { if (isLoggedIn) {
if (this.serverType === ServerType.Sas9) { if (this.serverType === ServerType.Sas9) {
const casAuthenticationUrl = `${this.serverUrl}/SASStoredProcess/j_spring_cas_security_check` await this.performCASSecurityCheck()
await this.requestClient.get<string>(
`/SASLogon/login?service=${casAuthenticationUrl}`,
undefined
)
} }
this.loginCallback() this.loginCallback()
@@ -96,6 +146,15 @@ export class AuthManager {
} }
} }
private async performCASSecurityCheck() {
const casAuthenticationUrl = `${this.serverUrl}/SASStoredProcess/j_spring_cas_security_check`
await this.requestClient.get<string>(
`/SASLogon/login?service=${casAuthenticationUrl}`,
undefined
)
}
private async sendLoginRequest( private async sendLoginRequest(
loginForm: { [key: string]: any }, loginForm: { [key: string]: any },
loginParams: { [key: string]: any } loginParams: { [key: string]: any }
@@ -128,13 +187,45 @@ export class AuthManager {
*/ */
public async checkSession(): Promise<{ public async checkSession(): Promise<{
isLoggedIn: boolean isLoggedIn: boolean
userName?: string userName: string
loginForm?: any loginForm?: any
}> {
const { isLoggedIn, userName } = await this.fetchUserName()
let loginForm = null
if (!isLoggedIn) {
//We will logout to make sure cookies are removed and login form is presented
//Residue can happen in case of session expiration
await this.logOut()
loginForm = await this.getNewLoginForm()
}
return Promise.resolve({
isLoggedIn,
userName: userName.toLowerCase(),
loginForm
})
}
private async getNewLoginForm() {
const { result: formResponse } = await this.requestClient.get<string>(
this.loginUrl.replace('.do', ''),
undefined,
'text/plain'
)
return await this.getLoginForm(formResponse)
}
private async fetchUserName(): Promise<{
isLoggedIn: boolean
userName: string
}> { }> {
//For VIYA we will send request on API endpoint. Which is faster then pinging SASJobExecution. //For VIYA we will send request on API endpoint. Which is faster then pinging SASJobExecution.
//For SAS9 we will send request on SASStoredProcess //For SAS9 we will send request on SASStoredProcess
const url = const url =
this.serverType === 'SASVIYA' this.serverType === ServerType.SasViya
? `${this.serverUrl}/identities/users/@currentUser` ? `${this.serverUrl}/identities/users/@currentUser`
: `${this.serverUrl}/SASStoredProcess` : `${this.serverUrl}/SASStoredProcess`
@@ -145,31 +236,9 @@ export class AuthManager {
}) })
const isLoggedIn = loginResponse !== 'authErr' const isLoggedIn = loginResponse !== 'authErr'
const userName = isLoggedIn const userName = isLoggedIn ? this.extractUserName(loginResponse) : ''
? this.extractUserName(loginResponse)
: undefined
let loginForm = null return { isLoggedIn, userName }
if (!isLoggedIn) {
//We will logout to make sure cookies are removed and login form is presented
//Residue can happen in case of session expiration
await this.logOut()
const { result: formResponse } = await this.requestClient.get<string>(
this.loginUrl.replace('.do', ''),
undefined,
'text/plain'
)
loginForm = await this.getLoginForm(formResponse)
}
return Promise.resolve({
isLoggedIn,
userName: userName?.toLowerCase(),
loginForm
})
} }
private extractUserName = (response: any): string => { private extractUserName = (response: any): string => {

40
src/auth/openWebPage.ts Normal file
View File

@@ -0,0 +1,40 @@
import { openLoginPrompt } from '../utils/loginPrompt'
interface WindowFeatures {
width: number
height: number
}
const defaultWindowFeatures: WindowFeatures = { width: 500, height: 600 }
export async function openWebPage(
url: string,
windowName: string = '',
WindowFeatures: WindowFeatures = defaultWindowFeatures,
onLoggedOut?: () => Promise<Boolean>
): Promise<Window | null> {
const { width, height } = WindowFeatures
const left = screen.width / 2 - width / 2
const top = screen.height / 2 - height / 2
const loginPopup = window.open(
url,
windowName,
`toolbar=0,location=0,menubar=0,width=${width},height=${height},left=${left},top=${top}`
)
if (!loginPopup) {
const getUserAction: () => Promise<Boolean> = onLoggedOut ?? openLoginPrompt
const doLogin = await getUserAction()
return doLogin
? window.open(
url,
windowName,
`toolbar=0,location=0,menubar=0,width=${width},height=${height},left=${left},top=${top}`
)
: null
}
return loginPopup
}

View File

@@ -3,10 +3,14 @@ import * as dotenv from 'dotenv'
import { ServerType } from '@sasjs/utils/types' import { ServerType } from '@sasjs/utils/types'
import axios from 'axios' import axios from 'axios'
import { import {
mockedCurrentUserApi,
mockLoginAuthoriseRequiredResponse, mockLoginAuthoriseRequiredResponse,
mockLoginSuccessResponse mockLoginSuccessResponse
} from './mockResponses' } from './mockResponses'
import { serialize } from '../../utils' import { serialize } from '../../utils'
import * as openWebPageModule from '../openWebPage'
import * as verifySasViyaLoginModule from '../verifySasViyaLogin'
import * as verifySas9LoginModule from '../verifySas9Login'
import { RequestClient } from '../../request/RequestClient' import { RequestClient } from '../../request/RequestClient'
jest.mock('axios') jest.mock('axios')
const mockedAxios = axios as jest.Mocked<typeof axios> const mockedAxios = axios as jest.Mocked<typeof axios>
@@ -57,133 +61,614 @@ describe('AuthManager', () => {
expect((authManager as any).logoutUrl).toEqual('/SASLogon/logout?') expect((authManager as any).logoutUrl).toEqual('/SASLogon/logout?')
}) })
it('should call the auth callback and return when already logged in', async () => { describe('login - default mechanism', () => {
const authManager = new AuthManager( it('should call the auth callback and return when already logged in', async () => {
serverUrl, const authManager = new AuthManager(
serverType, serverUrl,
requestClient, serverType,
authCallback requestClient,
) authCallback
jest.spyOn(authManager, 'checkSession').mockImplementation(() => )
Promise.resolve({ jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
isLoggedIn: true, Promise.resolve({
userName, isLoggedIn: true,
loginForm: 'test' userName,
}) loginForm: 'test'
) })
)
const loginResponse = await authManager.logIn(userName, password) const loginResponse = await authManager.logIn(userName, password)
expect(loginResponse.isLoggedIn).toBeTruthy() expect(loginResponse.isLoggedIn).toBeTruthy()
expect(loginResponse.userName).toEqual(userName) expect(loginResponse.userName).toEqual(userName)
expect(authCallback).toHaveBeenCalledTimes(1) expect(authCallback).toHaveBeenCalledTimes(1)
})
it('should post a login request to the server if not logged in', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
Promise.resolve({
isLoggedIn: false,
loginForm: { name: 'test' }
})
)
mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: mockLoginSuccessResponse })
)
const loginResponse = await authManager.logIn(userName, password)
expect(loginResponse.isLoggedIn).toBeTruthy()
expect(loginResponse.userName).toEqual(userName)
const loginParams = serialize({
_service: 'default',
username: userName,
password,
name: 'test'
}) })
expect(mockedAxios.post).toHaveBeenCalledWith(
`/SASLogon/login`, it('should post a login request to the server when already logged in with other username', async () => {
loginParams, const authManager = new AuthManager(
{ serverUrl,
withCredentials: true, serverType,
headers: { requestClient,
'Content-Type': 'application/x-www-form-urlencoded', authCallback
Accept: '*/*' )
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
Promise.resolve({
isLoggedIn: true,
userName: 'someOtherUsername',
loginForm: null
})
)
jest
.spyOn(authManager, 'logOut')
.mockImplementation(() => Promise.resolve(true))
jest
.spyOn<any, any>(authManager, 'getNewLoginForm')
.mockImplementation(() =>
Promise.resolve({
name: 'test'
})
)
mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: mockLoginSuccessResponse })
)
const loginResponse = await authManager.logIn(userName, password)
expect(loginResponse.isLoggedIn).toBeTruthy()
expect(loginResponse.userName).toEqual(userName)
const loginParams = serialize({
_service: 'default',
username: userName,
password,
name: 'test'
})
expect(authCallback).toHaveBeenCalledTimes(1)
expect(authManager.logOut).toHaveBeenCalledTimes(1)
expect(authManager['getNewLoginForm']).toHaveBeenCalledTimes(1)
expect(mockedAxios.post).toHaveBeenCalledWith(
`/SASLogon/login`,
loginParams,
{
withCredentials: true,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: '*/*'
}
} }
} )
) expect(authCallback).toHaveBeenCalledTimes(1)
expect(authCallback).toHaveBeenCalledTimes(1) })
it('should post a login request to the server when not logged in', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
Promise.resolve({
isLoggedIn: false,
userName: '',
loginForm: { name: 'test' }
})
)
mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: mockLoginSuccessResponse })
)
const loginResponse = await authManager.logIn(userName, password)
expect(loginResponse.isLoggedIn).toBeTruthy()
expect(loginResponse.userName).toEqual(userName)
const loginParams = serialize({
_service: 'default',
username: userName,
password,
name: 'test'
})
expect(mockedAxios.post).toHaveBeenCalledWith(
`/SASLogon/login`,
loginParams,
{
withCredentials: true,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: '*/*'
}
}
)
expect(authCallback).toHaveBeenCalledTimes(1)
})
it('should post a login & a cas_security request to the SAS9 server when not logged in', async () => {
const serverType = ServerType.Sas9
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
Promise.resolve({
isLoggedIn: false,
userName: '',
loginForm: { name: 'test' }
})
)
mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: mockLoginSuccessResponse })
)
mockedAxios.get.mockImplementation(() => Promise.resolve({ status: 200 }))
const loginResponse = await authManager.logIn(userName, password)
expect(loginResponse.isLoggedIn).toBeTruthy()
expect(loginResponse.userName).toEqual(userName)
const loginParams = serialize({
_service: 'default',
username: userName,
password,
name: 'test'
})
expect(mockedAxios.post).toHaveBeenCalledWith(
`/SASLogon/login`,
loginParams,
{
withCredentials: true,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: '*/*'
}
}
)
const casAuthenticationUrl = `${serverUrl}/SASStoredProcess/j_spring_cas_security_check`
expect(mockedAxios.get).toHaveBeenCalledWith(
`/SASLogon/login?service=${casAuthenticationUrl}`,
getHeadersJson
)
expect(authCallback).toHaveBeenCalledTimes(1)
})
it('should return empty username if unable to logged in', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
Promise.resolve({
isLoggedIn: false,
userName: '',
loginForm: { name: 'test' }
})
)
mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: 'Not Signed in' })
)
const loginResponse = await authManager.logIn(userName, password)
expect(loginResponse.isLoggedIn).toBeFalsy()
expect(loginResponse.userName).toEqual('')
const loginParams = serialize({
_service: 'default',
username: userName,
password,
name: 'test'
})
expect(mockedAxios.post).toHaveBeenCalledWith(
`/SASLogon/login`,
loginParams,
{
withCredentials: true,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: '*/*'
}
}
)
})
it('should parse and submit the authorisation form when necessary', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
jest
.spyOn(requestClient, 'authorize')
.mockImplementation(() => Promise.resolve())
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
Promise.resolve({
isLoggedIn: false,
userName: 'test',
loginForm: { name: 'test' }
})
)
mockedAxios.post.mockImplementationOnce(() =>
Promise.resolve({
data: mockLoginAuthoriseRequiredResponse,
config: { url: 'https://test.com/SASLogon/login' },
request: { responseURL: 'https://test.com/OAuth/authorize' }
})
)
mockedAxios.get.mockImplementationOnce(() =>
Promise.resolve({
data: mockLoginAuthoriseRequiredResponse
})
)
await authManager.logIn(userName, password)
expect(requestClient.authorize).toHaveBeenCalledWith(
mockLoginAuthoriseRequiredResponse
)
})
}) })
it('should parse and submit the authorisation form when necessary', async () => { describe('login - redirect mechanism', () => {
const authManager = new AuthManager( beforeAll(() => {
serverUrl, jest.mock('../openWebPage')
serverType, jest
requestClient, .spyOn(openWebPageModule, 'openWebPage')
authCallback .mockImplementation(() =>
) Promise.resolve({ close: jest.fn() } as unknown as Window)
jest )
.spyOn(requestClient, 'authorize') jest.mock('../verifySasViyaLogin')
.mockImplementation(() => Promise.resolve()) jest
jest.spyOn(authManager, 'checkSession').mockImplementation(() => .spyOn(verifySasViyaLoginModule, 'verifySasViyaLogin')
Promise.resolve({ .mockImplementation(() => Promise.resolve({ isLoggedIn: true }))
isLoggedIn: false, jest.mock('../verifySas9Login')
userName: 'test', jest
loginForm: { name: 'test' } .spyOn(verifySas9LoginModule, 'verifySas9Login')
}) .mockImplementation(() => Promise.resolve({ isLoggedIn: true }))
) })
mockedAxios.post.mockImplementationOnce(() =>
Promise.resolve({
data: mockLoginAuthoriseRequiredResponse,
config: { url: 'https://test.com/SASLogon/login' },
request: { responseURL: 'https://test.com/OAuth/authorize' }
})
)
mockedAxios.get.mockImplementationOnce(() => it('should call the auth callback and return when already logged in', async () => {
Promise.resolve({ const authManager = new AuthManager(
data: mockLoginAuthoriseRequiredResponse serverUrl,
}) serverType,
) requestClient,
authCallback
)
jest
.spyOn<any, any>(authManager, 'fetchUserName')
.mockImplementation(() =>
Promise.resolve({
isLoggedIn: true,
userName
})
)
await authManager.logIn(userName, password) const loginResponse = await authManager.redirectedLogIn({})
expect(requestClient.authorize).toHaveBeenCalledWith( expect(loginResponse.isLoggedIn).toBeTruthy()
mockLoginAuthoriseRequiredResponse expect(loginResponse.userName).toEqual(userName)
) expect(authCallback).toHaveBeenCalledTimes(1)
})
it('should perform login via pop up if not logged in', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
jest
.spyOn<any, any>(authManager, 'fetchUserName')
.mockImplementationOnce(() =>
Promise.resolve({
isLoggedIn: false,
userName: ''
})
)
.mockImplementationOnce(() =>
Promise.resolve({
isLoggedIn: true,
userName
})
)
const loginResponse = await authManager.redirectedLogIn({})
expect(loginResponse.isLoggedIn).toBeTruthy()
expect(loginResponse.userName).toEqual(userName)
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
`/SASLogon/home`,
'SASLogon',
{
width: 500,
height: 600
},
undefined
)
expect(authManager['fetchUserName']).toHaveBeenCalledTimes(2)
expect(verifySasViyaLoginModule.verifySasViyaLogin).toHaveBeenCalledTimes(
1
)
expect(authCallback).toHaveBeenCalledTimes(1)
})
it('should perform login via pop up if not logged in with server sas9', async () => {
const serverType = ServerType.Sas9
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
jest
.spyOn<any, any>(authManager, 'fetchUserName')
.mockImplementationOnce(() =>
Promise.resolve({
isLoggedIn: false,
userName: ''
})
)
.mockImplementationOnce(() =>
Promise.resolve({
isLoggedIn: true,
userName
})
)
const loginResponse = await authManager.redirectedLogIn({})
expect(loginResponse.isLoggedIn).toBeTruthy()
expect(loginResponse.userName).toEqual(userName)
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
`/SASLogon/home`,
'SASLogon',
{
width: 500,
height: 600
},
undefined
)
expect(authManager['fetchUserName']).toHaveBeenCalledTimes(2)
expect(verifySas9LoginModule.verifySas9Login).toHaveBeenCalledTimes(1)
expect(authCallback).toHaveBeenCalledTimes(1)
})
it('should return empty username if user unable to re-login via pop up', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
jest
.spyOn<any, any>(authManager, 'fetchUserName')
.mockImplementationOnce(() =>
Promise.resolve({
isLoggedIn: false,
userName: ''
})
)
.mockImplementationOnce(() =>
Promise.resolve({
isLoggedIn: true,
userName
})
)
jest
.spyOn(verifySasViyaLoginModule, 'verifySasViyaLogin')
.mockImplementation(() => Promise.resolve({ isLoggedIn: false }))
const loginResponse = await authManager.redirectedLogIn({})
expect(loginResponse.isLoggedIn).toBeFalsy()
expect(loginResponse.userName).toEqual('')
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
`/SASLogon/home`,
'SASLogon',
{
width: 500,
height: 600
},
undefined
)
expect(authManager['fetchUserName']).toHaveBeenCalledTimes(1)
expect(authCallback).toHaveBeenCalledTimes(0)
})
it('should return empty username if user rejects to re-login', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
jest
.spyOn<any, any>(authManager, 'fetchUserName')
.mockImplementationOnce(() =>
Promise.resolve({
isLoggedIn: false,
userName: ''
})
)
.mockImplementationOnce(() =>
Promise.resolve({
isLoggedIn: true,
userName
})
)
jest
.spyOn(openWebPageModule, 'openWebPage')
.mockImplementation(() => Promise.resolve(null))
const loginResponse = await authManager.redirectedLogIn({})
expect(loginResponse.isLoggedIn).toBeFalsy()
expect(loginResponse.userName).toEqual('')
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
`/SASLogon/home`,
'SASLogon',
{
width: 500,
height: 600
},
undefined
)
expect(authManager['fetchUserName']).toHaveBeenCalledTimes(1)
expect(authCallback).toHaveBeenCalledTimes(0)
})
}) })
it('should check and return session information if logged in', async () => { describe('checkSession', () => {
const authManager = new AuthManager( it('return session information when logged in', async () => {
serverUrl, const authManager = new AuthManager(
serverType, serverUrl,
requestClient, serverType,
authCallback requestClient,
) authCallback
mockedAxios.get.mockImplementation(() => )
Promise.resolve({ data: '<button onClick="logout">' }) mockedAxios.get.mockImplementation(() =>
) Promise.resolve({ data: mockedCurrentUserApi(userName) })
)
const response = await authManager.checkSession() const response = await authManager.checkSession()
expect(response.isLoggedIn).toBeTruthy() expect(response.isLoggedIn).toBeTruthy()
expect(mockedAxios.get).toHaveBeenNthCalledWith( expect(response.userName).toEqual(userName)
1, expect(mockedAxios.get).toHaveBeenNthCalledWith(
`http://test-server.com/identities/users/@currentUser`, 1,
{ `http://test-server.com/identities/users/@currentUser`,
withCredentials: true, {
responseType: 'text', withCredentials: true,
transformResponse: undefined, responseType: 'text',
headers: { transformResponse: undefined,
Accept: '*/*', headers: {
'Content-Type': 'text/plain' Accept: '*/*',
'Content-Type': 'text/plain'
}
} }
} )
) })
it('return session information when logged in - SAS9', async () => {
// username cannot have `-` and cannot be uppercased
const username = 'testusername'
const serverType = ServerType.Sas9
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
mockedAxios.get.mockImplementation(() =>
Promise.resolve({
data: `"title":"Log Off ${username}","url":"javascript: clearFrame(\"/SASStoredProcess/do?_action=logoff\")"' })`
})
)
const response = await authManager.checkSession()
expect(response.isLoggedIn).toBeTruthy()
expect(response.userName).toEqual(username)
expect(mockedAxios.get).toHaveBeenNthCalledWith(
1,
`http://test-server.com/SASStoredProcess`,
{
withCredentials: true,
responseType: 'text',
transformResponse: undefined,
headers: {
Accept: '*/*',
'Content-Type': 'text/plain'
}
}
)
})
it('return session information when logged in - SAS9 - having full name in html', async () => {
const fullname = 'FirstName LastName'
const username = 'firlas'
const serverType = ServerType.Sas9
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
mockedAxios.get.mockImplementation(() =>
Promise.resolve({
data: `"title":"Log Off ${fullname}","url":"javascript: clearFrame(\"/SASStoredProcess/do?_action=logoff\")"' })`
})
)
const response = await authManager.checkSession()
expect(response.isLoggedIn).toBeTruthy()
expect(response.userName).toEqual(username)
expect(mockedAxios.get).toHaveBeenNthCalledWith(
1,
`http://test-server.com/SASStoredProcess`,
{
withCredentials: true,
responseType: 'text',
transformResponse: undefined,
headers: {
Accept: '*/*',
'Content-Type': 'text/plain'
}
}
)
})
it('perform logout when not logged in', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
requestClient,
authCallback
)
mockedAxios.get
.mockImplementationOnce(() => Promise.resolve({ status: 401 }))
.mockImplementation(() => Promise.resolve({}))
const response = await authManager.checkSession()
expect(response.isLoggedIn).toBeFalsy()
expect(response.userName).toEqual('')
expect(mockedAxios.get).toHaveBeenNthCalledWith(
1,
`http://test-server.com/identities/users/@currentUser`,
{
withCredentials: true,
responseType: 'text',
transformResponse: undefined,
headers: {
Accept: '*/*',
'Content-Type': 'text/plain'
}
}
)
expect(mockedAxios.get).toHaveBeenNthCalledWith(
2,
`/SASLogon/logout.do?`,
getHeadersJson
)
})
}) })
}) })
const getHeadersJson = {
withCredentials: true,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
},
responseType: 'json'
}

View File

@@ -22,3 +22,28 @@ export const generateToken = (timeToLiveSeconds: number): string => {
const token = `${header}.${payload}.${signature}` const token = `${header}.${payload}.${signature}`
return token return token
} }
export const mockedCurrentUserApi = (username: string) => ({
creationTimeStamp: '2021-04-17T14:13:14.000Z',
modifiedTimeStamp: '2021-08-31T22:08:07.000Z',
id: username,
type: 'user',
name: 'Full User Name',
links: [
{
method: 'GET',
rel: 'self',
href: `/identities/users/${username}`,
uri: `/identities/users/${username}`,
type: 'user'
},
{
method: 'GET',
rel: 'alternate',
href: `/identities/users/${username}`,
uri: `/identities/users/${username}`,
type: 'application/vnd.sas.summary'
}
],
version: 2
})

View File

@@ -0,0 +1,64 @@
/**
* @jest-environment jsdom
*/
import { openWebPage } from '../openWebPage'
import * as loginPromptModule from '../../utils/loginPrompt'
describe('openWebPage', () => {
const serverUrl = 'http://test-server.com'
describe('window.open is not blocked', () => {
const mockedOpen = jest
.fn()
.mockImplementation(() => ({} as unknown as Window))
const originalOpen = window.open
beforeAll(() => {
window.open = mockedOpen
})
afterAll(() => {
window.open = originalOpen
})
it(`should return new Window popup - using default adapter's dialog`, async () => {
await expect(openWebPage(serverUrl)).resolves.toBeDefined()
expect(mockedOpen).toBeCalled()
})
})
describe('window.open is blocked', () => {
const mockedOpen = jest.fn().mockImplementation(() => null)
const originalOpen = window.open
beforeAll(() => {
window.open = mockedOpen
})
afterAll(() => {
window.open = originalOpen
})
it(`should return new Window popup - using default adapter's dialog`, async () => {
jest.mock('../../utils/loginPrompt')
jest
.spyOn(loginPromptModule, 'openLoginPrompt')
.mockImplementation(() => Promise.resolve(true))
await expect(openWebPage(serverUrl)).resolves.toBeDefined()
expect(loginPromptModule.openLoginPrompt).toBeCalled()
expect(mockedOpen).toBeCalled()
})
it(`should return new Window popup - using frontend's provided onloggedOut`, async () => {
const onLoggedOut = jest
.fn()
.mockImplementation(() => Promise.resolve(true))
await expect(
openWebPage(serverUrl, undefined, undefined, onLoggedOut)
).resolves.toBeDefined()
expect(onLoggedOut).toBeCalled()
expect(mockedOpen).toBeCalled()
})
})
})

View File

@@ -0,0 +1,37 @@
/**
* @jest-environment jsdom
*/
import { verifySas9Login } from '../verifySas9Login'
import * as delayModule from '../../utils/delay'
describe('verifySas9Login', () => {
const serverUrl = 'http://test-server.com'
beforeAll(() => {
jest.mock('../../utils')
jest
.spyOn(delayModule, 'delay')
.mockImplementation(() => Promise.resolve({}))
})
it('should return isLoggedIn true by checking state of popup', async () => {
const popup = {
window: {
location: { href: serverUrl + `/SASLogon/home` },
document: { body: { innerText: '<h3>You have signed in.</h3>' } }
}
} as unknown as Window
await expect(verifySas9Login(popup)).resolves.toEqual({
isLoggedIn: true
})
})
it('should return isLoggedIn false if user closed popup, already', async () => {
const popup: Window = { closed: true } as unknown as Window
await expect(verifySas9Login(popup)).resolves.toEqual({
isLoggedIn: false
})
})
})

View File

@@ -0,0 +1,38 @@
/**
* @jest-environment jsdom
*/
import { verifySasViyaLogin } from '../verifySasViyaLogin'
import * as delayModule from '../../utils/delay'
describe('verifySasViyaLogin', () => {
const serverUrl = 'http://test-server.com'
beforeAll(() => {
jest.mock('../../utils')
jest
.spyOn(delayModule, 'delay')
.mockImplementation(() => Promise.resolve({}))
document.cookie = encodeURIComponent('Current-User={"userId":"user-hash"}')
})
it('should return isLoggedIn true by checking state of popup', async () => {
const popup = {
window: {
location: { href: serverUrl + `/SASLogon/home` },
document: { body: { innerText: '<h3>You have signed in.</h3>' } }
}
} as unknown as Window
await expect(verifySasViyaLogin(popup)).resolves.toEqual({
isLoggedIn: true
})
})
it('should return isLoggedIn false if user closed popup, already', async () => {
const popup: Window = { closed: true } as unknown as Window
await expect(verifySasViyaLogin(popup)).resolves.toEqual({
isLoggedIn: false
})
})
})

View File

@@ -0,0 +1,20 @@
import { delay } from '../utils'
export async function verifySas9Login(loginPopup: Window): Promise<{
isLoggedIn: boolean
}> {
let isLoggedIn = false
let startTime = new Date()
let elapsedSeconds = 0
do {
await delay(1000)
if (loginPopup.closed) break
isLoggedIn =
loginPopup.window.location.href.includes('SASLogon') &&
loginPopup.window.document.body.innerText.includes('You have signed in.')
elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000
} while (!isLoggedIn && elapsedSeconds < 5 * 60)
return { isLoggedIn }
}

View File

@@ -0,0 +1,33 @@
import { delay } from '../utils'
export async function verifySasViyaLogin(loginPopup: Window): Promise<{
isLoggedIn: boolean
}> {
let isLoggedIn = false
let startTime = new Date()
let elapsedSeconds = 0
do {
await delay(1000)
if (loginPopup.closed) break
isLoggedIn = isLoggedInSASVIYA()
elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000
} while (!isLoggedIn && elapsedSeconds < 5 * 60)
let isAuthorized = false
startTime = new Date()
do {
await delay(1000)
if (loginPopup.closed) break
isAuthorized =
loginPopup.window.location.href.includes('SASLogon') ||
loginPopup.window.document.body?.innerText?.includes(
'You have signed in.'
)
elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000
} while (!isAuthorized && elapsedSeconds < 5 * 60)
return { isLoggedIn: isLoggedIn && isAuthorized }
}
export const isLoggedInSASVIYA = () =>
document.cookie.includes('Current-User') && document.cookie.includes('userId')

View File

@@ -35,14 +35,12 @@ export class ComputeJobExecutor extends BaseJobExecutor {
expectWebout expectWebout
) )
.then((response) => { .then((response) => {
this.appendRequest(response, sasJob, config.debug) this.sasViyaApiClient.appendRequest(response, sasJob, config.debug)
resolve(response.result) resolve(response.result)
}) })
.catch(async (e: Error) => { .catch(async (e: Error) => {
if (e instanceof ComputeJobExecutionError) { if (e instanceof ComputeJobExecutionError) {
this.appendRequest(e, sasJob, config.debug) this.sasViyaApiClient.appendRequest(e, sasJob, config.debug)
reject(new ErrorResponse(e?.message, e)) reject(new ErrorResponse(e?.message, e))
} }

View File

@@ -28,7 +28,7 @@ export class JesJobExecutor extends BaseJobExecutor {
this.sasViyaApiClient this.sasViyaApiClient
?.executeJob(sasJob, config.contextName, config.debug, data, authConfig) ?.executeJob(sasJob, config.contextName, config.debug, data, authConfig)
.then((response: any) => { .then((response: any) => {
this.appendRequest(response, sasJob, config.debug) this.sasViyaApiClient.appendRequest(response, sasJob, config.debug)
const responseObject = appendExtraResponseAttributes( const responseObject = appendExtraResponseAttributes(
response, response,
@@ -39,7 +39,7 @@ export class JesJobExecutor extends BaseJobExecutor {
}) })
.catch(async (e: Error) => { .catch(async (e: Error) => {
if (e instanceof JobExecutionError) { if (e instanceof JobExecutionError) {
this.appendRequest(e, sasJob, config.debug) this.sasViyaApiClient.appendRequest(e, sasJob, config.debug)
reject(new ErrorResponse(e?.message, e)) reject(new ErrorResponse(e?.message, e))
} }

View File

@@ -15,8 +15,6 @@ export interface JobExecutor {
extraResponseAttributes?: ExtraResponseAttributes[] extraResponseAttributes?: ExtraResponseAttributes[]
) => Promise<any> ) => Promise<any>
resendWaitingRequests: () => Promise<void> resendWaitingRequests: () => Promise<void>
getRequests: () => SASjsRequest[]
clearRequests: () => void
} }
export abstract class BaseJobExecutor implements JobExecutor { export abstract class BaseJobExecutor implements JobExecutor {
@@ -46,54 +44,7 @@ export abstract class BaseJobExecutor implements JobExecutor {
return return
} }
getRequests = () => this.requests
clearRequests = () => {
this.requests = []
}
protected appendWaitingRequest(request: ExecuteFunction) { protected appendWaitingRequest(request: ExecuteFunction) {
this.waitingRequests.push(request) this.waitingRequests.push(request)
} }
protected appendRequest(response: any, program: string, debug: boolean) {
let sourceCode = ''
let generatedCode = ''
let sasWork = null
if (debug) {
if (response?.log) {
sourceCode = parseSourceCode(response.log)
generatedCode = parseGeneratedCode(response.log)
if (response?.result) {
sasWork = response.result.WORK
} else {
sasWork = response.log
}
} else if (response?.result) {
sourceCode = parseSourceCode(response.result)
generatedCode = parseGeneratedCode(response.result)
sasWork = response.result.WORK
}
}
const stringifiedResult =
typeof response?.result === 'string'
? response?.result
: JSON.stringify(response?.result, null, 2)
this.requests.push({
logFile: response?.log || stringifiedResult || response,
serviceLink: program,
timestamp: new Date(),
sourceCode,
generatedCode,
SASWORK: sasWork
})
if (this.requests.length > 20) {
this.requests.splice(0, 1)
}
}
} }

View File

@@ -6,8 +6,7 @@ import {
import { import {
ErrorResponse, ErrorResponse,
JobExecutionError, JobExecutionError,
LoginRequiredError, LoginRequiredError
WeboutResponseError
} from '../types/errors' } from '../types/errors'
import { generateFileUploadForm } from '../file/generateFileUploadForm' import { generateFileUploadForm } from '../file/generateFileUploadForm'
import { generateTableUploadForm } from '../file/generateTableUploadForm' import { generateTableUploadForm } from '../file/generateTableUploadForm'
@@ -15,7 +14,6 @@ import { RequestClient } from '../request/RequestClient'
import { SASViyaApiClient } from '../SASViyaApiClient' import { SASViyaApiClient } from '../SASViyaApiClient'
import { import {
isRelativePath, isRelativePath,
getValidJson,
parseSasViyaDebugResponse, parseSasViyaDebugResponse,
appendExtraResponseAttributes appendExtraResponseAttributes
} from '../utils' } from '../utils'
@@ -55,10 +53,7 @@ export class WebJobExecutor extends BaseJobExecutor {
let apiUrl = `${config.serverUrl}${this.jobsPath}/?${'_program=' + program}` let apiUrl = `${config.serverUrl}${this.jobsPath}/?${'_program=' + program}`
if (config.serverType === ServerType.SasViya) { if (config.serverType === ServerType.SasViya) {
const jobUri = const jobUri = await this.getJobUri(sasJob)
config.serverType === ServerType.SasViya
? await this.getJobUri(sasJob)
: ''
apiUrl += jobUri.length > 0 ? '&_job=' + jobUri : '' apiUrl += jobUri.length > 0 ? '&_job=' + jobUri : ''
@@ -120,6 +115,8 @@ export class WebJobExecutor extends BaseJobExecutor {
const requestPromise = new Promise((resolve, reject) => { const requestPromise = new Promise((resolve, reject) => {
this.requestClient!.post(apiUrl, formData, undefined) this.requestClient!.post(apiUrl, formData, undefined)
.then(async (res: any) => { .then(async (res: any) => {
this.requestClient!.appendRequest(res, sasJob, config.debug)
let jsonResponse = res.result let jsonResponse = res.result
if (config.debug) { if (config.debug) {
@@ -140,8 +137,6 @@ export class WebJobExecutor extends BaseJobExecutor {
} }
} }
this.appendRequest(res, sasJob, config.debug)
const responseObject = appendExtraResponseAttributes( const responseObject = appendExtraResponseAttributes(
{ result: jsonResponse }, { result: jsonResponse },
extraResponseAttributes extraResponseAttributes
@@ -150,14 +145,11 @@ export class WebJobExecutor extends BaseJobExecutor {
}) })
.catch(async (e: Error) => { .catch(async (e: Error) => {
if (e instanceof JobExecutionError) { if (e instanceof JobExecutionError) {
this.appendRequest(e, sasJob, config.debug) this.requestClient!.appendRequest(e, sasJob, config.debug)
reject(new ErrorResponse(e?.message, e)) reject(new ErrorResponse(e?.message, e))
} }
if (e instanceof LoginRequiredError) { if (e instanceof LoginRequiredError) {
await loginCallback()
this.appendWaitingRequest(() => { this.appendWaitingRequest(() => {
return this.execute( return this.execute(
sasJob, sasJob,
@@ -175,6 +167,8 @@ export class WebJobExecutor extends BaseJobExecutor {
} }
) )
}) })
await loginCallback()
} else { } else {
reject(new ErrorResponse(e?.message, e)) reject(new ErrorResponse(e?.message, e))
} }

View File

@@ -8,10 +8,11 @@ import {
InternalServerError, InternalServerError,
JobExecutionError JobExecutionError
} from '../types/errors' } from '../types/errors'
import { SASjsRequest } from '../types'
import { parseWeboutResponse } from '../utils/parseWeboutResponse' import { parseWeboutResponse } from '../utils/parseWeboutResponse'
import { prefixMessage } from '@sasjs/utils/error' import { prefixMessage } from '@sasjs/utils/error'
import { SAS9AuthError } from '../types/errors/SAS9AuthError' import { SAS9AuthError } from '../types/errors/SAS9AuthError'
import { getValidJson } from '../utils' import { parseGeneratedCode, parseSourceCode } from '../utils'
export interface HttpClient { export interface HttpClient {
get<T>( get<T>(
@@ -47,27 +48,18 @@ export interface HttpClient {
} }
export class RequestClient implements HttpClient { export class RequestClient implements HttpClient {
private requests: SASjsRequest[] = []
protected csrfToken: CsrfToken = { headerName: '', value: '' } protected csrfToken: CsrfToken = { headerName: '', value: '' }
protected fileUploadCsrfToken: CsrfToken | undefined protected fileUploadCsrfToken: CsrfToken | undefined
protected httpClient: AxiosInstance protected httpClient!: AxiosInstance
constructor(protected baseUrl: string, allowInsecure = false) { constructor(protected baseUrl: string, allowInsecure = false) {
const https = require('https') this.createHttpClient(baseUrl, allowInsecure)
if (allowInsecure && https.Agent) { }
this.httpClient = axios.create({
baseURL: baseUrl,
httpsAgent: new https.Agent({
rejectUnauthorized: !allowInsecure
})
})
} else {
this.httpClient = axios.create({
baseURL: baseUrl
})
}
this.httpClient.defaults.validateStatus = (status) => public setConfig(baseUrl: string, allowInsecure = false) {
status >= 200 && status < 305 this.createHttpClient(baseUrl, allowInsecure)
} }
public getCsrfToken(type: 'general' | 'file' = 'general') { public getCsrfToken(type: 'general' | 'file' = 'general') {
@@ -83,6 +75,66 @@ export class RequestClient implements HttpClient {
return this.httpClient.defaults.baseURL || '' return this.httpClient.defaults.baseURL || ''
} }
/**
* this method returns all requests, an array of SASjsRequest type
* @returns SASjsRequest[]
*/
public getRequests = () => this.requests
/**
* this method clears the requests array, i.e set to empty
*/
public clearRequests = () => {
this.requests = []
}
/**
* this method appends the response from sasjs request to requests array
* @param response - response from sasjs request
* @param program - name of program
* @param debug - a boolean that indicates whether debug was enabled or not
*/
public appendRequest(response: any, program: string, debug: boolean) {
let sourceCode = ''
let generatedCode = ''
let sasWork = null
if (debug) {
if (response?.log) {
sourceCode = parseSourceCode(response.log)
generatedCode = parseGeneratedCode(response.log)
if (response?.result) {
sasWork = response.result.WORK
} else {
sasWork = response.log
}
} else if (response?.result) {
sourceCode = parseSourceCode(response.result)
generatedCode = parseGeneratedCode(response.result)
sasWork = response.result.WORK
}
}
const stringifiedResult =
typeof response?.result === 'string'
? response?.result
: JSON.stringify(response?.result, null, 2)
this.requests.push({
logFile: response?.log || stringifiedResult || response,
serviceLink: program,
timestamp: new Date(),
sourceCode,
generatedCode,
SASWORK: sasWork
})
if (this.requests.length > 20) {
this.requests.splice(0, 1)
}
}
public async get<T>( public async get<T>(
url: string, url: string,
accessToken: string | undefined, accessToken: string | undefined,
@@ -454,6 +506,25 @@ export class RequestClient implements HttpClient {
return responseToReturn return responseToReturn
} }
private createHttpClient(baseUrl: string, allowInsecure = false) {
const https = require('https')
if (allowInsecure && https.Agent) {
this.httpClient = axios.create({
baseURL: baseUrl,
httpsAgent: new https.Agent({
rejectUnauthorized: !allowInsecure
})
})
} else {
this.httpClient = axios.create({
baseURL: baseUrl
})
}
this.httpClient.defaults.validateStatus = (status) =>
status >= 200 && status < 305
}
} }
export const throwIfError = (response: AxiosResponse) => { export const throwIfError = (response: AxiosResponse) => {

View File

@@ -34,7 +34,8 @@ const prepareFilesAndParams = () => {
describe('FileUploader', () => { describe('FileUploader', () => {
const config: SASjsConfig = { const config: SASjsConfig = {
...new SASjsConfig(), ...new SASjsConfig(),
appLoc: '/sample/apploc' appLoc: '/sample/apploc',
debug: false
} }
const fileUploader = new FileUploader( const fileUploader = new FileUploader(

View File

@@ -33,4 +33,18 @@ describe('jsonValidator', () => {
} }
expect(test).toThrow(JsonParseArrayError) expect(test).toThrow(JsonParseArrayError)
}) })
it('should throw an error when null is passed', () => {
const test = () => {
getValidJson(null as any)
}
expect(test).toThrow(InvalidJsonError)
})
it('should throw an error when undefined is passed', () => {
const test = () => {
getValidJson(undefined as any)
}
expect(test).toThrow(InvalidJsonError)
})
}) })

8
src/types/Login.ts Normal file
View File

@@ -0,0 +1,8 @@
export interface LoginOptions {
onLoggedOut?: () => Promise<boolean>
}
export interface LoginResult {
isLoggedIn: boolean
userName: string
}

View File

@@ -59,4 +59,13 @@ export class SASjsConfig {
* Changing this setting is not recommended. * Changing this setting is not recommended.
*/ */
allowInsecureRequests = false allowInsecureRequests = false
/**
* Supported login mechanisms are - Redirected and Default
*/
loginMechanism: LoginMechanism = LoginMechanism.Default
}
export enum LoginMechanism {
Default = 'Default',
Redirected = 'Redirected'
} }

2
src/utils/delay.ts Normal file
View File

@@ -0,0 +1,2 @@
export const delay = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms))

View File

@@ -6,6 +6,8 @@ import { JsonParseArrayError, InvalidJsonError } from '../types/errors'
*/ */
export const getValidJson = (str: string | object) => { export const getValidJson = (str: string | object) => {
try { try {
if (str === null || str === undefined) throw new InvalidJsonError()
if (Array.isArray(str)) throw new JsonParseArrayError() if (Array.isArray(str)) throw new JsonParseArrayError()
if (typeof str === 'object') return str if (typeof str === 'object') return str

View File

@@ -1,6 +1,7 @@
export * from './asyncForEach' export * from './asyncForEach'
export * from './compareTimestamps' export * from './compareTimestamps'
export * from './convertToCsv' export * from './convertToCsv'
export * from './delay'
export * from './isNode' export * from './isNode'
export * from './isRelativePath' export * from './isRelativePath'
export * from './isUri' export * from './isUri'

View File

@@ -0,0 +1,167 @@
enum domIDs {
styles = 'sasjsAdapterStyles',
overlay = 'sasjsAdapterLoginPromptBG',
dialog = 'sasjsAdapterLoginPrompt'
}
export const openLoginPrompt = (): Promise<boolean> => {
return new Promise(async (resolve) => {
const style = document.createElement('style')
style.id = domIDs.styles
style.innerText = cssContent
const loginPromptBG = document.createElement('div')
loginPromptBG.id = domIDs.overlay
loginPromptBG.classList.add('popUpBG')
const loginPrompt = document.createElement('div')
loginPrompt.id = domIDs.dialog
loginPrompt.classList.add('popUp')
const title = document.createElement('h1')
title.innerText = 'Session Expired!'
loginPrompt.appendChild(title)
const descHolder = document.createElement('div')
const desc = document.createElement('span')
desc.innerText = 'You need to relogin, click OK to login.'
descHolder.appendChild(desc)
loginPrompt.appendChild(descHolder)
const buttonCancel = document.createElement('button')
buttonCancel.classList.add('cancel')
buttonCancel.innerText = 'Cancel'
buttonCancel.onclick = () => {
closeLoginPrompt()
resolve(false)
}
loginPrompt.appendChild(buttonCancel)
const buttonOk = document.createElement('button')
buttonOk.classList.add('confirm')
buttonOk.innerText = 'Ok'
buttonOk.onclick = () => {
closeLoginPrompt()
resolve(true)
}
loginPrompt.appendChild(buttonOk)
document.body.style.overflow = 'hidden'
document.body.appendChild(style)
document.body.appendChild(loginPromptBG)
document.body.appendChild(loginPrompt)
})
}
const closeLoginPrompt = () => {
Object.values(domIDs).forEach((id) => {
const elem = document.getElementById(id)
elem?.parentNode?.removeChild(elem)
})
document.body.style.overflow = 'auto'
}
const cssContent = `
.popUp {
box-sizing: border-box;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
display: block;
position: fixed;
top: 40%;
left: 50%;
padding: 0;
font-size: 14px;
font-family: 'PT Sans', sans-serif;
color: #fff;
border-style: none;
z-index: 999;
overflow: hidden;
background: rgba(0, 0, 0, 0.2);
margin: 0;
width: 100%;
max-width: 300px;
height: auto;
max-height: 300px;
transform: translate(-50%, -50%);
}
.popUp > h1 {
box-sizing: border-box;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
padding: 5px;
min-height: 40px;
font-size: 1.2em;
font-weight: bold;
text-align: center;
color: #fff;
background-color: transparent;
border-style: none;
border-width: 5px;
border-color: black;
}
.popUp > div {
width: 100%;
height: calc(100% -108px);
margin: 0;
display: block;
box-sizing: border-box;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
padding: 5%;
text-align: center;
border-width: 1px;
border-color: #ccc;
border-style: none none solid none;
overflow: auto;
}
.popUp > div > span {
display: table-cell;
box-sizing: border-box;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
margin: 0;
padding: 0;
width: 300px;
height: 108px;
vertical-align: middle;
border-style: none;
}
.popUp .cancel {
float: left;
}
.popUp .confirm {
float: right;
}
.popUp > button {
box-sizing: border-box;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
margin: 0;
padding: 10px;
width: 50%;
border: 1px none #ccc;
color: #fff;
font-family: inherit;
cursor: pointer;
height: 50px;
background: rgba(1, 1, 1, 0.2);
}
.popUp > button:hover {
background: rgba(0, 0, 0, 0.2);
}
.popUpBG {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
opacity: 0.95;
z-index: 50;
background-image: radial-gradient(#0378cd, #012036);
}
`