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

Compare commits

..

14 Commits

Author SHA1 Message Date
Yury Shkoda
0f881fba72 chore(deps): regenerate package-lock.json 2021-12-23 11:49:42 +03:00
Yury Shkoda
4e125ce38f Merge pull request #605 from sasjs/issue-604
Improve error handling and job/session state polling
2021-12-22 19:11:37 +03:00
Yury Shkoda
4a963ffbf5 feat(polling-state): improve logging 2021-12-22 18:46:06 +03:00
Yury Shkoda
f25d9ec09d test(RequestClient): cover handleError method 2021-12-21 16:41:08 +03:00
Yury Shkoda
4197ad5aa8 test(RequestClient): fix error handling 2021-12-21 11:40:59 +03:00
Yury Shkoda
2ebd6e11ba test(RequestClient): fix error handling 2021-12-21 11:40:27 +03:00
Yury Shkoda
098e7f8590 fix(errors): fixed error handling function 2021-12-20 10:57:53 +03:00
Yury Shkoda
42aec96410 feat(pollJobState): improved loggging 2021-12-20 10:56:52 +03:00
Allan Bowe
f2905ee169 Merge pull request #602 from sasjs/hot-fix-sasjs-server
fix: response from SASJS Server with/without debug on
2021-12-15 17:42:29 +00:00
Saad Jutt
1ba9291746 fix: response from SASJS Server with/without debug on 2021-12-15 21:32:02 +05:00
Muhammad Saad
c44766ea14 Merge pull request #600 from sasjs/sasjs-server-access-refresh-tokens
feat: get access token & refresh tokens for server type SASJS
2021-12-15 12:56:57 +05:00
Saad Jutt
2c10b9c65c chore: typedoc updated 2021-12-13 17:01:59 +05:00
Saad Jutt
c56874fe00 feat: login for web with server type SASJS 2021-12-09 17:13:47 +05:00
Saad Jutt
ebe9c2ffeb feat: get access token & refresh tokens for server type SASJS 2021-12-09 11:43:50 +05:00
105 changed files with 25450 additions and 9288 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -22,8 +22,8 @@ import { pollJobState } from './api/viya/pollJobState'
import { getTokens } from './auth/getTokens'
import { uploadTables } from './api/viya/uploadTables'
import { executeScript } from './api/viya/executeScript'
import { getAccessToken } from './auth/getAccessToken'
import { refreshTokens } from './auth/refreshTokens'
import { getAccessTokenForViya } from './auth/getAccessTokenForViya'
import { refreshTokensForViya } from './auth/refreshTokensForViya'
/**
* A client for interfacing with the SAS Viya REST API.
@@ -534,21 +534,26 @@ export class SASViyaApiClient {
clientSecret: string,
authCode: string
): Promise<SasAuthResponse> {
return getAccessToken(this.requestClient, clientId, clientSecret, authCode)
return getAccessTokenForViya(
this.requestClient,
clientId,
clientSecret,
authCode
)
}
/**
* Exchanges the refresh token for an access token for the given client.
* @param clientId - the client ID to authenticate with.
* @param clientSecret - the client secret to authenticate with.
* @param authCode - the refresh token received from the server.
* @param refreshToken - the refresh token received from the server.
*/
public async refreshTokens(
clientId: string,
clientSecret: string,
refreshToken: string
) {
return refreshTokens(
return refreshTokensForViya(
this.requestClient,
clientId,
clientSecret,

View File

@@ -11,15 +11,17 @@ import {
} from './types'
import { SASViyaApiClient } from './SASViyaApiClient'
import { SAS9ApiClient } from './SAS9ApiClient'
import { SASjsApiClient } from './SASjsApiClient'
import { SASjsApiClient, SASjsAuthResponse } from './SASjsApiClient'
import { AuthManager } from './auth'
import {
ServerType,
MacroVar,
AuthConfig,
ExtraResponseAttributes
ExtraResponseAttributes,
SasAuthResponse
} from '@sasjs/utils/types'
import { RequestClient } from './request/RequestClient'
import { SasjsRequestClient } from './request/SasjsRequestClient'
import {
JobExecutor,
WebJobExecutor,
@@ -53,7 +55,7 @@ export default class SASjs {
private jobsPath: string = ''
private sasViyaApiClient: SASViyaApiClient | null = null
private sas9ApiClient: SAS9ApiClient | null = null
private SASjsApiClient: SASjsApiClient | null = null
private sasJSApiClient: SASjsApiClient | null = null
private fileUploader: FileUploader | null = null
private authManager: AuthManager | null = null
private requestClient: RequestClient | null = null
@@ -80,7 +82,7 @@ export default class SASjs {
userName: string,
password: string
) {
this.isMethodSupported('executeScriptSAS9', ServerType.Sas9)
this.isMethodSupported('executeScriptSAS9', [ServerType.Sas9])
return await this.sas9ApiClient?.executeScript(
linesOfCode,
@@ -94,7 +96,7 @@ export default class SASjs {
* @param accessToken - an access token for an authorized user.
*/
public async getComputeContexts(accessToken: string) {
this.isMethodSupported('getComputeContexts', ServerType.SasViya)
this.isMethodSupported('getComputeContexts', [ServerType.SasViya])
return await this.sasViyaApiClient!.getComputeContexts(accessToken)
}
@@ -104,7 +106,7 @@ export default class SASjs {
* @param accessToken - an access token for an authorized user.
*/
public async getLauncherContexts(accessToken: string) {
this.isMethodSupported('getLauncherContexts', ServerType.SasViya)
this.isMethodSupported('getLauncherContexts', [ServerType.SasViya])
return await this.sasViyaApiClient!.getLauncherContexts(accessToken)
}
@@ -113,7 +115,7 @@ export default class SASjs {
* Gets default(system) launcher contexts.
*/
public getDefaultComputeContexts() {
this.isMethodSupported('getDefaultComputeContexts', ServerType.SasViya)
this.isMethodSupported('getDefaultComputeContexts', [ServerType.SasViya])
return this.sasViyaApiClient!.getDefaultComputeContexts()
}
@@ -123,7 +125,7 @@ export default class SASjs {
* @param authConfig - an access token, refresh token, client and secret for an authorized user.
*/
public async getExecutableContexts(authConfig: AuthConfig) {
this.isMethodSupported('getExecutableContexts', ServerType.SasViya)
this.isMethodSupported('getExecutableContexts', [ServerType.SasViya])
return await this.sasViyaApiClient!.getExecutableContexts(authConfig)
}
@@ -145,7 +147,7 @@ export default class SASjs {
accessToken: string,
authorizedUsers?: string[]
) {
this.isMethodSupported('createComputeContext', ServerType.SasViya)
this.isMethodSupported('createComputeContext', [ServerType.SasViya])
return await this.sasViyaApiClient!.createComputeContext(
contextName,
@@ -170,7 +172,7 @@ export default class SASjs {
launchType: string,
accessToken: string
) {
this.isMethodSupported('createLauncherContext', ServerType.SasViya)
this.isMethodSupported('createLauncherContext', [ServerType.SasViya])
return await this.sasViyaApiClient!.createLauncherContext(
contextName,
@@ -191,7 +193,7 @@ export default class SASjs {
editedContext: EditContextInput,
accessToken?: string
) {
this.isMethodSupported('editComputeContext', ServerType.SasViya)
this.isMethodSupported('editComputeContext', [ServerType.SasViya])
return await this.sasViyaApiClient!.editComputeContext(
contextName,
@@ -206,7 +208,7 @@ export default class SASjs {
* @param accessToken - an access token for an authorized user.
*/
public async deleteComputeContext(contextName: string, accessToken?: string) {
this.isMethodSupported('deleteComputeContext', ServerType.SasViya)
this.isMethodSupported('deleteComputeContext', [ServerType.SasViya])
return await this.sasViyaApiClient!.deleteComputeContext(
contextName,
@@ -224,7 +226,7 @@ export default class SASjs {
contextName: string,
accessToken?: string
) {
this.isMethodSupported('getComputeContextByName', ServerType.SasViya)
this.isMethodSupported('getComputeContextByName', [ServerType.SasViya])
return await this.sasViyaApiClient!.getComputeContextByName(
contextName,
@@ -238,7 +240,7 @@ export default class SASjs {
* @param accessToken - an access token for an authorized user.
*/
public async getComputeContextById(contextId: string, accessToken?: string) {
this.isMethodSupported('getComputeContextById', ServerType.SasViya)
this.isMethodSupported('getComputeContextById', [ServerType.SasViya])
return await this.sasViyaApiClient!.getComputeContextById(
contextId,
@@ -247,7 +249,7 @@ export default class SASjs {
}
public async createSession(contextName: string, accessToken: string) {
this.isMethodSupported('createSession', ServerType.SasViya)
this.isMethodSupported('createSession', [ServerType.SasViya])
return await this.sasViyaApiClient!.createSession(contextName, accessToken)
}
@@ -267,7 +269,7 @@ export default class SASjs {
authConfig?: AuthConfig,
debug?: boolean
) {
this.isMethodSupported('executeScriptSASViya', ServerType.SasViya)
this.isMethodSupported('executeScriptSASViya', [ServerType.SasViya])
if (!contextName) {
throw new Error(
'Context name is undefined. Please set a `contextName` in your SASjs or override config.'
@@ -357,7 +359,7 @@ export default class SASjs {
* @param accessToken - the access token to authorize the request.
*/
public async getFolder(folderPath: string, accessToken?: string) {
this.isMethodSupported('getFolder', ServerType.SasViya)
this.isMethodSupported('getFolder', [ServerType.SasViya])
return await this.sasViyaApiClient!.getFolder(folderPath, accessToken)
}
@@ -367,7 +369,7 @@ export default class SASjs {
* @param accessToken - an access token for authorizing the request.
*/
public async deleteFolder(folderPath: string, accessToken: string) {
this.isMethodSupported('deleteFolder', ServerType.SasViya)
this.isMethodSupported('deleteFolder', [ServerType.SasViya])
return await this.sasViyaApiClient?.deleteFolder(folderPath, accessToken)
}
@@ -382,7 +384,7 @@ export default class SASjs {
accessToken?: string,
limit?: number
) {
this.isMethodSupported('listFolder', ServerType.SasViya)
this.isMethodSupported('listFolder', [ServerType.SasViya])
return await this.sasViyaApiClient?.listFolder(
sourceFolder,
@@ -404,7 +406,7 @@ export default class SASjs {
targetFolderName: string,
accessToken: string
) {
this.isMethodSupported('moveFolder', ServerType.SasViya)
this.isMethodSupported('moveFolder', [ServerType.SasViya])
return await this.sasViyaApiClient?.moveFolder(
sourceFolder,
@@ -422,7 +424,7 @@ export default class SASjs {
accessToken?: string,
sasApiClient?: SASViyaApiClient
) {
this.isMethodSupported('createJobDefinition', ServerType.SasViya)
this.isMethodSupported('createJobDefinition', [ServerType.SasViya])
if (sasApiClient)
return await sasApiClient!.createJobDefinition(
@@ -442,7 +444,7 @@ export default class SASjs {
}
public async getAuthCode(clientId: string) {
this.isMethodSupported('getAuthCode', ServerType.SasViya)
this.isMethodSupported('getAuthCode', [ServerType.SasViya])
return await this.sasViyaApiClient!.getAuthCode(clientId)
}
@@ -457,8 +459,14 @@ export default class SASjs {
clientId: string,
clientSecret: string,
authCode: string
) {
this.isMethodSupported('getAccessToken', ServerType.SasViya)
): Promise<SasAuthResponse | SASjsAuthResponse> {
this.isMethodSupported('getAccessToken', [
ServerType.SasViya,
ServerType.Sasjs
])
if (this.sasjsConfig.serverType === ServerType.Sasjs)
return await this.sasJSApiClient!.getAccessToken(clientId, authCode)
return await this.sasViyaApiClient!.getAccessToken(
clientId,
@@ -467,12 +475,24 @@ export default class SASjs {
)
}
/**
* Exchanges the refresh token for an access token for the given client.
* @param clientId - the client ID to authenticate with.
* @param clientSecret - the client secret to authenticate with.
* @param refreshToken - the refresh token received from the server.
*/
public async refreshTokens(
clientId: string,
clientSecret: string,
refreshToken: string
) {
this.isMethodSupported('refreshTokens', ServerType.SasViya)
): Promise<SasAuthResponse | SASjsAuthResponse> {
this.isMethodSupported('refreshTokens', [
ServerType.SasViya,
ServerType.Sasjs
])
if (this.sasjsConfig.serverType === ServerType.Sasjs)
return await this.sasJSApiClient!.refreshTokens(refreshToken)
return await this.sasViyaApiClient!.refreshTokens(
clientId,
@@ -482,7 +502,7 @@ export default class SASjs {
}
public async deleteClient(clientId: string, accessToken: string) {
this.isMethodSupported('deleteClient', ServerType.SasViya)
this.isMethodSupported('deleteClient', [ServerType.SasViya])
return await this.sasViyaApiClient!.deleteClient(clientId, accessToken)
}
@@ -528,29 +548,39 @@ export default class SASjs {
/**
* Checks whether a session is active, or login is required.
* @param accessToken - an optional access token is required for SASjs server type.
* @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`.
*/
public async checkSession(accessToken?: string) {
return this.authManager!.checkSession(accessToken)
public async checkSession() {
return this.authManager!.checkSession()
}
/**
* Logs into the SAS server with the supplied credentials.
* @param username - a string representing the username.
* @param password - a string representing the password.
* @param clientId - a string representing the client ID.
*/
public async logIn(
username?: string,
password?: string,
clientId?: string,
options: LoginOptions = {}
): Promise<LoginResult> {
if (this.sasjsConfig.loginMechanism === LoginMechanism.Default) {
if (!username || !password) {
if (!username || !password)
throw new Error(
'A username and password are required when using the default login mechanism.'
)
if (this.sasjsConfig.serverType === ServerType.Sasjs) {
if (!clientId)
throw new Error(
'A username, password and clientId are required when using the default login mechanism with server type SASJS.'
)
return this.authManager!.logInSasjs(username, password, clientId)
}
return this.authManager!.logIn(username, password)
}
@@ -565,10 +595,9 @@ export default class SASjs {
/**
* Logs out of the configured SAS server.
* @param accessToken - an optional access token is required for SASjs server type.
*/
public logOut(accessToken?: string) {
return this.authManager!.logOut(accessToken)
public logOut() {
return this.authManager!.logOut()
}
/**
@@ -777,7 +806,7 @@ export default class SASjs {
accessToken?: string,
isForced = false
) {
this.isMethodSupported('deployServicePack', ServerType.SasViya)
this.isMethodSupported('deployServicePack', [ServerType.SasViya])
let sasApiClient: any = null
if (serverUrl || appLoc) {
@@ -832,11 +861,11 @@ export default class SASjs {
}
public async deployToSASjs(members: [FolderMember, ServiceMember]) {
return await this.SASjsApiClient?.deploy(members, this.sasjsConfig.appLoc)
return await this.sasJSApiClient?.deploy(members, this.sasjsConfig.appLoc)
}
public async executeJobSASjs(query: ExecutionQuery) {
return await this.SASjsApiClient?.executeJob(query)
return await this.sasJSApiClient?.executeJob(query)
}
/**
@@ -872,7 +901,7 @@ export default class SASjs {
...config
}
this.isMethodSupported('startComputeJob', ServerType.SasViya)
this.isMethodSupported('startComputeJob', [ServerType.SasViya])
if (!config.contextName) {
throw new Error(
'Context name is undefined. Please set a `contextName` in your SASjs or override config.'
@@ -964,7 +993,11 @@ export default class SASjs {
}
if (!this.requestClient) {
this.requestClient = new RequestClient(
const RequestClientClass =
this.sasjsConfig.serverType === ServerType.Sasjs
? SasjsRequestClient
: RequestClient
this.requestClient = new RequestClientClass(
this.sasjsConfig.serverUrl,
this.sasjsConfig.httpsAgentOptions
)
@@ -1020,10 +1053,10 @@ export default class SASjs {
}
if (this.sasjsConfig.serverType === ServerType.Sasjs) {
if (this.SASjsApiClient) {
this.SASjsApiClient.setConfig(this.sasjsConfig.serverUrl)
if (this.sasJSApiClient) {
this.sasJSApiClient.setConfig(this.sasjsConfig.serverUrl)
} else {
this.SASjsApiClient = new SASjsApiClient(
this.sasJSApiClient = new SASjsApiClient(
this.sasjsConfig.serverUrl,
this.requestClient
)
@@ -1117,12 +1150,15 @@ export default class SASjs {
})
}
private isMethodSupported(method: string, serverType: string) {
if (this.sasjsConfig.serverType !== serverType) {
private isMethodSupported(method: string, serverTypes: ServerType[]) {
if (
!this.sasjsConfig.serverType ||
!serverTypes.includes(this.sasjsConfig.serverType)
) {
throw new Error(
`Method '${method}' is only supported on ${
serverType === ServerType.Sas9 ? 'SAS9' : 'SAS Viya'
} servers.`
`Method '${method}' is only supported on ${serverTypes.join(
', '
)} servers.`
)
}
}

View File

@@ -1,5 +1,8 @@
import { FolderMember, ServiceMember, ExecutionQuery } from './types'
import { RequestClient } from './request/RequestClient'
import { getAccessTokenForSasjs } from './auth/getAccessTokenForSasjs'
import { refreshTokensForSasjs } from './auth/refreshTokensForSasjs'
import { getAuthCodeForSasjs } from './auth/getAuthCodeForSasjs'
export class SASjsApiClient {
constructor(
@@ -36,4 +39,44 @@ export class SASjsApiClient {
return Promise.resolve(result)
}
/**
* Exchanges the auth code for an access token for the given client.
* @param clientId - the client ID to authenticate with.
* @param authCode - the auth code received from the server.
*/
public async getAccessToken(
clientId: string,
authCode: string
): Promise<SASjsAuthResponse> {
return getAccessTokenForSasjs(this.requestClient, clientId, authCode)
}
/**
* Exchanges the refresh token for an access token.
* @param refreshToken - the refresh token received from the server.
*/
public async refreshTokens(refreshToken: string): Promise<SASjsAuthResponse> {
return refreshTokensForSasjs(this.requestClient, refreshToken)
}
/**
* Performs a login authenticate and returns an auth code for the given client.
* @param username - a string representing the username.
* @param password - a string representing the password.
* @param clientId - the client ID to authenticate with.
*/
public async getAuthCode(
username: string,
password: string,
clientId: string
) {
return getAuthCodeForSasjs(this.requestClient, username, password, clientId)
}
}
// todo move to sasjs/utils
export interface SASjsAuthResponse {
access_token: string
refresh_token: string
}

View File

@@ -168,7 +168,7 @@ export class SessionManager {
) {
if (stateLink) {
if (this.debug && !this.printedSessionState.printed) {
logger.info('Polling session status...')
logger.info(`Polling: ${this.serverUrl + stateLink.href}`)
this.printedSessionState.printed = true
}

View File

@@ -206,10 +206,11 @@ const doPoll = async (
pollCount++
const jobHref = postedJob.links.find((l: Link) => l.rel === 'self')!.href
if (pollOptions?.streamLog) {
const jobUrl = postedJob.links.find((l: Link) => l.rel === 'self')
const { result: job } = await requestClient.get<Job>(
jobUrl!.href,
jobHref,
authConfig?.access_token
)
@@ -231,7 +232,7 @@ const doPoll = async (
}
if (debug && printedState !== state) {
logger.info('Polling job status...')
logger.info(`Polling: ${requestClient.getBaseUrl() + jobHref}/state`)
logger.info(`Current job state: ${state}`)
printedState = state

View File

@@ -9,7 +9,10 @@ import * as isNodeModule from '../../../utils/isNode'
import { PollOptions } from '../../../types'
import { WriteStream } from 'fs'
const baseUrl = 'http://localhost'
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
requestClient['httpClient'].defaults.baseURL = baseUrl
const defaultPollOptions: PollOptions = {
maxPollCount: 100,
pollInterval: 500,
@@ -195,7 +198,7 @@ describe('pollJobState', () => {
expect((process as any).logger.info).toHaveBeenCalledTimes(4)
expect((process as any).logger.info).toHaveBeenNthCalledWith(
1,
'Polling job status...'
`Polling: ${baseUrl}/job/state`
)
expect((process as any).logger.info).toHaveBeenNthCalledWith(
2,
@@ -203,7 +206,7 @@ describe('pollJobState', () => {
)
expect((process as any).logger.info).toHaveBeenNthCalledWith(
3,
'Polling job status...'
`Polling: ${baseUrl}/job/state`
)
expect((process as any).logger.info).toHaveBeenNthCalledWith(
4,

View File

@@ -2,6 +2,8 @@ import { ServerType } from '@sasjs/utils/types'
import { RequestClient } from '../request/RequestClient'
import { LoginOptions, LoginResult } from '../types/Login'
import { serialize } from '../utils'
import { getAccessTokenForSasjs } from './getAccessTokenForSasjs'
import { getAuthCodeForSasjs } from './getAuthCodeForSasjs'
import { openWebPage } from './openWebPage'
import { verifySas9Login } from './verifySas9Login'
import { verifySasViyaLogin } from './verifySasViyaLogin'
@@ -81,6 +83,39 @@ export class AuthManager {
return { isLoggedIn: false, userName: '' }
}
/**
* Logs into the SAS server with the supplied credentials.
* @param userName - a string representing the username.
* @param password - a string representing the password.
* @param clientId - a string representing the client ID.
* @returns - a boolean `isLoggedin` and a string `username`
*/
public async logInSasjs(
username: string,
password: string,
clientId: string
): Promise<LoginResult> {
const isLoggedIn = await this.sendLoginRequestSasjs(
username,
password,
clientId
)
.then((res) => {
this.userName = username
this.requestClient.saveLocalStorageToken(
res.access_token,
res.refresh_token
)
return true
})
.catch(() => false)
return {
isLoggedIn,
userName: this.userName
}
}
/**
* Logs into the SAS server with the supplied credentials.
* @param username - a string representing the username.
@@ -180,28 +215,41 @@ export class AuthManager {
return loginResponse
}
private async sendLoginRequestSasjs(
username: string,
password: string,
clientId: string
) {
const authCode = await getAuthCodeForSasjs(
this.requestClient,
username,
password,
clientId
)
return getAccessTokenForSasjs(this.requestClient, clientId, authCode)
}
/**
* Checks whether a session is active, or login is required.
* @param accessToken - an optional access token is required for SASjs server type.
* @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(accessToken?: string): Promise<{
public async checkSession(): Promise<{
isLoggedIn: boolean
userName: string
loginForm?: any
}> {
const { isLoggedIn, userName } = await this.fetchUserName(accessToken)
const { isLoggedIn, userName } = await this.fetchUserName()
let loginForm = null
if (!isLoggedIn && this.serverType !== ServerType.Sasjs) {
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()
if (this.serverType !== ServerType.Sasjs)
loginForm = await this.getNewLoginForm()
}
return Promise.resolve({
@@ -221,7 +269,7 @@ export class AuthManager {
return await this.getLoginForm(formResponse)
}
private async fetchUserName(accessToken?: string): Promise<{
private async fetchUserName(): Promise<{
isLoggedIn: boolean
userName: string
}> {
@@ -232,9 +280,8 @@ export class AuthManager {
? `${this.serverUrl}/SASStoredProcess`
: `${this.serverUrl}/SASjsApi/session`
// Access token is required for server type `SASjs`
const { result: loginResponse } = await this.requestClient
.get<string>(url, accessToken, 'text/plain')
.get<string>(url, undefined, 'text/plain')
.catch((err: any) => {
return { result: 'authErr' }
})
@@ -315,11 +362,19 @@ export class AuthManager {
* Logs out of the configured SAS server.
* @param accessToken - an optional access token is required for SASjs server type.
*/
public logOut(accessToken?: string) {
public async logOut() {
if (this.serverType === ServerType.Sasjs) {
return this.requestClient.post(this.logoutUrl, undefined, accessToken)
return this.requestClient
.delete(this.logoutUrl)
.catch(() => true)
.finally(() => {
this.requestClient.clearLocalStorageTokens()
return true
})
}
this.requestClient.clearCsrfTokens()
return this.requestClient.get(this.logoutUrl, undefined).then(() => true)
}
}

View File

@@ -0,0 +1,36 @@
import { prefixMessage } from '@sasjs/utils/error'
import { RequestClient } from '../request/RequestClient'
/**
* Exchanges the auth code for an access token for the given client.
* @param requestClient - the pre-configured HTTP request client
* @param clientId - the client ID to authenticate with.
* @param authCode - the auth code received from the server.
*/
export async function getAccessTokenForSasjs(
requestClient: RequestClient,
clientId: string,
authCode: string
) {
const url = '/SASjsApi/auth/token'
const data = {
clientId,
code: authCode
}
return await requestClient
.post(url, data, undefined)
.then((res) => {
const sasAuth = res.result as {
accessToken: string
refreshToken: string
}
return {
access_token: sasAuth.accessToken,
refresh_token: sasAuth.refreshToken
}
})
.catch((err) => {
throw prefixMessage(err, 'Error while getting access token. ')
})
}

View File

@@ -10,7 +10,7 @@ import { RequestClient } from '../request/RequestClient'
* @param clientSecret - the client secret to authenticate with.
* @param authCode - the auth code received from the server.
*/
export async function getAccessToken(
export async function getAccessTokenForViya(
requestClient: RequestClient,
clientId: string,
clientSecret: string,

View File

@@ -0,0 +1,31 @@
import { prefixMessage } from '@sasjs/utils/error'
import { RequestClient } from '../request/RequestClient'
/**
* Performs a login authenticate and returns an auth code for the given client.
* @param requestClient - the pre-configured HTTP request client
* @param username - a string representing the username.
* @param password - a string representing the password.
* @param clientId - the client ID to authenticate with.
*/
export const getAuthCodeForSasjs = async (
requestClient: RequestClient,
username: string,
password: string,
clientId: string
) => {
const url = '/SASjsApi/auth/authorize'
const data = { username, password, clientId }
const { code: authCode } = await requestClient
.post<{ code: string }>(url, data, undefined)
.then((res) => res.result)
.catch((err) => {
throw prefixMessage(
err,
'Error while authenticating with provided username, password and clientId. '
)
})
return authCode
}

View File

@@ -3,18 +3,21 @@ import {
isRefreshTokenExpiring,
hasTokenExpired
} from '@sasjs/utils/auth'
import { AuthConfig } from '@sasjs/utils/types'
import { AuthConfig, ServerType } from '@sasjs/utils/types'
import { RequestClient } from '../request/RequestClient'
import { refreshTokens } from './refreshTokens'
import { refreshTokensForViya } from './refreshTokensForViya'
import { refreshTokensForSasjs } from './refreshTokensForSasjs'
/**
* Returns the auth configuration, refreshing the tokens if necessary.
* @param requestClient - the pre-configured HTTP request client
* @param authConfig - an object containing a client ID, secret, access token and refresh token
* @param serverType - server type for which refreshing the tokens, defaults to SASVIYA
*/
export async function getTokens(
requestClient: RequestClient,
authConfig: AuthConfig
authConfig: AuthConfig,
serverType: ServerType = ServerType.SasViya
): Promise<AuthConfig> {
const logger = process.logger || console
let { access_token, refresh_token, client, secret } = authConfig
@@ -29,12 +32,16 @@ export async function getTokens(
throw new Error(error)
}
logger.info('Refreshing access and refresh tokens.')
;({ access_token, refresh_token } = await refreshTokens(
requestClient,
client,
secret,
refresh_token
))
const tokens =
serverType === ServerType.SasViya
? await refreshTokensForViya(
requestClient,
client,
secret,
refresh_token
)
: await refreshTokensForSasjs(requestClient, refresh_token)
;({ access_token, refresh_token } = tokens)
}
return { access_token, refresh_token, client, secret }
}

View File

@@ -0,0 +1,35 @@
import { prefixMessage } from '@sasjs/utils/error'
import { RequestClient } from '../request/RequestClient'
/**
* Exchanges the refresh token for an access token for the given client.
* @param requestClient - the pre-configured HTTP request client
* @param refreshToken - the refresh token received from the server.
*/
export async function refreshTokensForSasjs(
requestClient: RequestClient,
refreshToken: string
) {
const url = '/SASjsApi/auth/refresh'
const headers = {
Authorization: 'Bearer ' + refreshToken
}
const authResponse = await requestClient
.post(url, undefined, undefined, undefined, headers)
.then((res) => {
const sasAuth = res.result as {
accessToken: string
refreshToken: string
}
return {
access_token: sasAuth.accessToken,
refresh_token: sasAuth.refreshToken
}
})
.catch((err) => {
throw prefixMessage(err, 'Error while refreshing tokens')
})
return authResponse
}

View File

@@ -8,9 +8,9 @@ import { RequestClient } from '../request/RequestClient'
* @param requestClient - the pre-configured HTTP request client
* @param clientId - the client ID to authenticate with.
* @param clientSecret - the client secret to authenticate with.
* @param authCode - the refresh token received from the server.
* @param refreshToken - the refresh token received from the server.
*/
export async function refreshTokens(
export async function refreshTokensForViya(
requestClient: RequestClient,
clientId: string,
clientSecret: string,

View File

@@ -0,0 +1,65 @@
import { AuthConfig } from '@sasjs/utils'
import { generateToken, mockSasjsAuthResponse } from './mockResponses'
import { RequestClient } from '../../request/RequestClient'
import { getAccessTokenForSasjs } from '../getAccessTokenForSasjs'
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
describe('getAccessTokenForSasjs', () => {
it('should attempt to refresh tokens', async () => {
setupMocks()
const access_token = generateToken(30)
const refresh_token = generateToken(30)
const authConfig: AuthConfig = {
access_token,
refresh_token,
client: 'cl13nt',
secret: 's3cr3t'
}
jest
.spyOn(requestClient, 'post')
.mockImplementation(() =>
Promise.resolve({ result: mockSasjsAuthResponse, etag: '' })
)
await getAccessTokenForSasjs(
requestClient,
authConfig.client,
authConfig.refresh_token
)
expect(requestClient.post).toHaveBeenCalledWith(
'/SASjsApi/auth/token',
{ clientId: authConfig.client, code: authConfig.refresh_token },
undefined
)
})
it('should handle errors while refreshing tokens', async () => {
setupMocks()
const access_token = generateToken(30)
const refresh_token = generateToken(30)
const authConfig: AuthConfig = {
access_token,
refresh_token,
client: 'cl13nt',
secret: 's3cr3t'
}
jest
.spyOn(requestClient, 'post')
.mockImplementation(() => Promise.reject('Token Error'))
const error = await getAccessTokenForSasjs(
requestClient,
authConfig.client,
authConfig.refresh_token
).catch((e) => e)
expect(error).toContain('Error while getting access token')
})
})
const setupMocks = () => {
jest.restoreAllMocks()
jest.mock('../../request/RequestClient')
}

View File

@@ -2,11 +2,11 @@ import { AuthConfig } from '@sasjs/utils'
import * as NodeFormData from 'form-data'
import { generateToken, mockAuthResponse } from './mockResponses'
import { RequestClient } from '../../request/RequestClient'
import { getAccessToken } from '../getAccessToken'
import { getAccessTokenForViya } from '../getAccessTokenForViya'
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
describe('getAccessToken', () => {
describe('getAccessTokenForViya', () => {
it('should attempt to refresh tokens', async () => {
setupMocks()
const access_token = generateToken(30)
@@ -26,7 +26,7 @@ describe('getAccessToken', () => {
authConfig.client + ':' + authConfig.secret
).toString('base64')
await getAccessToken(
await getAccessTokenForViya(
requestClient,
authConfig.client,
authConfig.secret,
@@ -58,7 +58,7 @@ describe('getAccessToken', () => {
.spyOn(requestClient, 'post')
.mockImplementation(() => Promise.reject('Token Error'))
const error = await getAccessToken(
const error = await getAccessTokenForViya(
requestClient,
authConfig.client,
authConfig.secret,

View File

@@ -1,5 +1,5 @@
import { AuthConfig } from '@sasjs/utils'
import * as refreshTokensModule from '../refreshTokens'
import * as refreshTokensModule from '../refreshTokensForViya'
import { generateToken, mockAuthResponse } from './mockResponses'
import { getTokens } from '../getTokens'
import { RequestClient } from '../../request/RequestClient'
@@ -20,7 +20,7 @@ describe('getTokens', () => {
await getTokens(requestClient, authConfig)
expect(refreshTokensModule.refreshTokens).toHaveBeenCalledWith(
expect(refreshTokensModule.refreshTokensForViya).toHaveBeenCalledWith(
requestClient,
authConfig.client,
authConfig.secret,
@@ -41,7 +41,7 @@ describe('getTokens', () => {
await getTokens(requestClient, authConfig)
expect(refreshTokensModule.refreshTokens).toHaveBeenCalledWith(
expect(refreshTokensModule.refreshTokensForViya).toHaveBeenCalledWith(
requestClient,
authConfig.client,
authConfig.secret,
@@ -71,9 +71,9 @@ describe('getTokens', () => {
const setupMocks = () => {
jest.restoreAllMocks()
jest.mock('../../request/RequestClient')
jest.mock('../refreshTokens')
jest.mock('../refreshTokensForViya')
jest
.spyOn(refreshTokensModule, 'refreshTokens')
.spyOn(refreshTokensModule, 'refreshTokensForViya')
.mockImplementation(() => Promise.resolve(mockAuthResponse))
}

View File

@@ -13,6 +13,11 @@ export const mockAuthResponse: SasAuthResponse = {
jti: 'test'
}
export const mockSasjsAuthResponse = {
access_token: 'acc355',
refresh_token: 'r3fr35h'
}
export const generateToken = (timeToLiveSeconds: number): string => {
const exp =
new Date(new Date().getTime() + timeToLiveSeconds * 1000).getTime() / 1000

View File

@@ -0,0 +1,47 @@
import { generateToken, mockAuthResponse } from './mockResponses'
import { RequestClient } from '../../request/RequestClient'
import { refreshTokensForSasjs } from '../refreshTokensForSasjs'
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
describe('refreshTokensForSasjs', () => {
it('should attempt to refresh tokens', async () => {
setupMocks()
const refresh_token = generateToken(30)
jest
.spyOn(requestClient, 'post')
.mockImplementation(() =>
Promise.resolve({ result: mockAuthResponse, etag: '' })
)
await refreshTokensForSasjs(requestClient, refresh_token)
expect(requestClient.post).toHaveBeenCalledWith(
'/SASjsApi/auth/refresh',
undefined,
undefined,
undefined,
{ Authorization: `Bearer ${refresh_token}` }
)
})
it('should handle errors while refreshing tokens', async () => {
setupMocks()
const refresh_token = generateToken(30)
jest
.spyOn(requestClient, 'post')
.mockImplementation(() => Promise.reject('Token Error'))
const error = await refreshTokensForSasjs(
requestClient,
refresh_token
).catch((e) => e)
expect(error).toContain('Error while refreshing tokens')
})
})
const setupMocks = () => {
jest.restoreAllMocks()
jest.mock('../../request/RequestClient')
}

View File

@@ -2,11 +2,11 @@ import { AuthConfig } from '@sasjs/utils'
import * as NodeFormData from 'form-data'
import { generateToken, mockAuthResponse } from './mockResponses'
import { RequestClient } from '../../request/RequestClient'
import { refreshTokens } from '../refreshTokens'
import { refreshTokensForViya } from '../refreshTokensForViya'
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
describe('refreshTokens', () => {
describe('refreshTokensForViya', () => {
it('should attempt to refresh tokens', async () => {
setupMocks()
const access_token = generateToken(30)
@@ -26,7 +26,7 @@ describe('refreshTokens', () => {
authConfig.client + ':' + authConfig.secret
).toString('base64')
await refreshTokens(
await refreshTokensForViya(
requestClient,
authConfig.client,
authConfig.secret,
@@ -58,7 +58,7 @@ describe('refreshTokens', () => {
.spyOn(requestClient, 'post')
.mockImplementation(() => Promise.reject('Token Error'))
const error = await refreshTokens(
const error = await refreshTokensForViya(
requestClient,
authConfig.client,
authConfig.secret,

View File

@@ -144,7 +144,6 @@ export class WebJobExecutor extends BaseJobExecutor {
}
const requestPromise = new Promise((resolve, reject) => {
// Access token is required for server type `SASjs`
this.requestClient!.post(apiUrl, formData, authConfig?.access_token)
.then(async (res: any) => {
const resObj =
@@ -158,11 +157,6 @@ export class WebJobExecutor extends BaseJobExecutor {
let jsonResponse = res.result
if (this.serverType === ServerType.Sasjs) {
const webout = parseWeboutResponse(res.result._webout, apiUrl)
jsonResponse = getValidJson(webout)
}
if (config.debug) {
switch (this.serverType) {
case ServerType.SasViya:
@@ -178,7 +172,13 @@ export class WebJobExecutor extends BaseJobExecutor {
? parseWeboutResponse(res.result, apiUrl)
: res.result
break
case ServerType.Sasjs:
const webout = parseWeboutResponse(res.result._webout, apiUrl)
jsonResponse = getValidJson(webout)
break
}
} else if (this.serverType === ServerType.Sasjs) {
jsonResponse = getValidJson(res.result._webout)
}
const responseObject = appendExtraResponseAttributes(

View File

@@ -48,7 +48,9 @@ export interface HttpClient {
): Promise<{ result: T; etag: string }>
getCsrfToken(type: 'general' | 'file'): CsrfToken | undefined
saveLocalStorageToken(accessToken: string, refreshToken: string): void
clearCsrfTokens(): void
clearLocalStorageTokens(): void
getBaseUrl(): string
}
@@ -70,6 +72,11 @@ export class RequestClient implements HttpClient {
this.createHttpClient(baseUrl, httpsAgentOptions)
}
public saveLocalStorageToken(accessToken: string, refreshToken: string) {
localStorage.setItem('accessToken', accessToken)
localStorage.setItem('refreshToken', refreshToken)
}
public getCsrfToken(type: 'general' | 'file' = 'general') {
return type === 'file' ? this.fileUploadCsrfToken : this.csrfToken
}
@@ -78,6 +85,10 @@ export class RequestClient implements HttpClient {
this.csrfToken = { headerName: '', value: '' }
this.fileUploadCsrfToken = { headerName: '', value: '' }
}
public clearLocalStorageTokens() {
localStorage.setItem('accessToken', '')
localStorage.setItem('refreshToken', '')
}
public getBaseUrl() {
return this.httpClient.defaults.baseURL || ''
@@ -184,9 +195,7 @@ export class RequestClient implements HttpClient {
}
),
debug
).catch((err) => {
throw prefixMessage(err, 'Error while handling error. ')
})
)
})
}
@@ -206,6 +215,7 @@ export class RequestClient implements HttpClient {
.post<T>(url, data, { headers, withCredentials: true })
.then((response) => {
throwIfError(response)
return this.parseResponse<T>(response)
})
.catch(async (e) => {
@@ -455,6 +465,8 @@ export class RequestClient implements HttpClient {
if (e instanceof LoginRequiredError) {
this.clearCsrfTokens()
throw e
}
if (response?.status === 403 || response?.status === 449) {
@@ -477,7 +489,8 @@ export class RequestClient implements HttpClient {
else return
}
throw e
if (e.message) throw e
else throw prefixMessage(e, 'Error while handling error. ')
}
protected parseResponse<T>(response: AxiosResponse<any>) {
@@ -529,8 +542,9 @@ export class RequestClient implements HttpClient {
this.httpClient = createAxiosInstance(baseUrl, httpsAgent)
this.httpClient.defaults.validateStatus = (status) =>
status >= 200 && status < 401
this.httpClient.defaults.validateStatus = (status) => {
return status >= 200 && status <= 401
}
}
}
@@ -627,7 +641,7 @@ const parseError = (data: string) => {
if (parts.length > 1) {
const log = parts[1].split('<pre>')[1].split('</pre>')[0]
const message = `This request completed with errors.`
return new JobExecutionError(500, message, log)
return new JobExecutionError(404, message, log)
}
}
} catch (_) {}

Some files were not shown because too many files have changed in this diff Show More