@sasjs/adapter 3.1.0 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/SASViyaApiClient.d.ts +2 -8
  2. package/SASjs.d.ts +26 -12
  3. package/SASjsApiClient.d.ts +22 -0
  4. package/auth/AuthManager.d.ts +12 -4
  5. package/auth/getAccessTokenForSasjs.d.ts +11 -0
  6. package/auth/{getAccessToken.d.ts → getAccessTokenForViya.d.ts} +1 -1
  7. package/auth/getAuthCodeForSasjs.d.ts +9 -0
  8. package/auth/getTokens.d.ts +3 -2
  9. package/auth/refreshTokensForSasjs.d.ts +10 -0
  10. package/auth/{refreshTokens.d.ts → refreshTokensForViya.d.ts} +2 -2
  11. package/auth/spec/{getAccessToken.spec.d.ts → getAccessTokenForSasjs.spec.d.ts} +0 -0
  12. package/auth/spec/{refreshTokens.spec.d.ts → getAccessTokenForViya.spec.d.ts} +0 -0
  13. package/auth/spec/mockResponses.d.ts +4 -0
  14. package/auth/spec/refreshTokensForSasjs.spec.d.ts +1 -0
  15. package/auth/spec/refreshTokensForViya.spec.d.ts +1 -0
  16. package/index.js +2 -2
  17. package/node/SASViyaApiClient.d.ts +2 -8
  18. package/node/SASjs.d.ts +25 -11
  19. package/node/SASjsApiClient.d.ts +22 -0
  20. package/node/auth/AuthManager.d.ts +11 -3
  21. package/node/auth/getAccessTokenForSasjs.d.ts +11 -0
  22. package/node/auth/{getAccessToken.d.ts → getAccessTokenForViya.d.ts} +1 -1
  23. package/node/auth/getAuthCodeForSasjs.d.ts +9 -0
  24. package/node/auth/getTokens.d.ts +3 -2
  25. package/node/auth/refreshTokensForSasjs.d.ts +10 -0
  26. package/node/auth/{refreshTokens.d.ts → refreshTokensForViya.d.ts} +2 -2
  27. package/node/index.js +2 -2
  28. package/node/request/RequestClient.d.ts +4 -0
  29. package/node/request/SasjsRequestClient.d.ts +8 -0
  30. package/package.json +1 -1
  31. package/request/RequestClient.d.ts +4 -0
  32. package/request/SasjsRequestClient.d.ts +8 -0
  33. package/src/SASViyaApiClient.ts +10 -5
  34. package/src/SASjs.ts +83 -47
  35. package/src/SASjsApiClient.ts +43 -0
  36. package/src/SessionManager.ts +1 -1
  37. package/src/api/viya/pollJobState.ts +4 -3
  38. package/src/auth/AuthManager.ts +65 -10
  39. package/src/auth/getAccessTokenForSasjs.ts +36 -0
  40. package/src/auth/{getAccessToken.ts → getAccessTokenForViya.ts} +1 -1
  41. package/src/auth/getAuthCodeForSasjs.ts +31 -0
  42. package/src/auth/getTokens.ts +16 -9
  43. package/src/auth/refreshTokensForSasjs.ts +35 -0
  44. package/src/auth/{refreshTokens.ts → refreshTokensForViya.ts} +2 -2
  45. package/src/auth/spec/mockResponses.ts +5 -0
  46. package/src/job-execution/WebJobExecutor.ts +6 -6
  47. package/src/request/RequestClient.ts +20 -6
  48. package/src/request/Sas9RequestClient.ts +1 -1
  49. package/src/request/SasjsRequestClient.ts +23 -0
  50. package/src/test/SAS_server_app.ts +1 -0
@@ -2,6 +2,8 @@ import { ServerType } from '@sasjs/utils/types'
2
2
  import { RequestClient } from '../request/RequestClient'
3
3
  import { LoginOptions, LoginResult } from '../types/Login'
4
4
  import { serialize } from '../utils'
5
+ import { getAccessTokenForSasjs } from './getAccessTokenForSasjs'
6
+ import { getAuthCodeForSasjs } from './getAuthCodeForSasjs'
5
7
  import { openWebPage } from './openWebPage'
6
8
  import { verifySas9Login } from './verifySas9Login'
7
9
  import { verifySasViyaLogin } from './verifySasViyaLogin'
@@ -81,6 +83,39 @@ export class AuthManager {
81
83
  return { isLoggedIn: false, userName: '' }
82
84
  }
83
85
 
86
+ /**
87
+ * Logs into the SAS server with the supplied credentials.
88
+ * @param userName - a string representing the username.
89
+ * @param password - a string representing the password.
90
+ * @param clientId - a string representing the client ID.
91
+ * @returns - a boolean `isLoggedin` and a string `username`
92
+ */
93
+ public async logInSasjs(
94
+ username: string,
95
+ password: string,
96
+ clientId: string
97
+ ): Promise<LoginResult> {
98
+ const isLoggedIn = await this.sendLoginRequestSasjs(
99
+ username,
100
+ password,
101
+ clientId
102
+ )
103
+ .then((res) => {
104
+ this.userName = username
105
+ this.requestClient.saveLocalStorageToken(
106
+ res.access_token,
107
+ res.refresh_token
108
+ )
109
+ return true
110
+ })
111
+ .catch(() => false)
112
+
113
+ return {
114
+ isLoggedIn,
115
+ userName: this.userName
116
+ }
117
+ }
118
+
84
119
  /**
85
120
  * Logs into the SAS server with the supplied credentials.
86
121
  * @param username - a string representing the username.
@@ -180,28 +215,41 @@ export class AuthManager {
180
215
  return loginResponse
181
216
  }
182
217
 
218
+ private async sendLoginRequestSasjs(
219
+ username: string,
220
+ password: string,
221
+ clientId: string
222
+ ) {
223
+ const authCode = await getAuthCodeForSasjs(
224
+ this.requestClient,
225
+ username,
226
+ password,
227
+ clientId
228
+ )
229
+ return getAccessTokenForSasjs(this.requestClient, clientId, authCode)
230
+ }
183
231
  /**
184
232
  * Checks whether a session is active, or login is required.
185
- * @param accessToken - an optional access token is required for SASjs server type.
186
233
  * @returns - a promise which resolves with an object containing three values
187
234
  * - a boolean `isLoggedIn`
188
235
  * - a string `userName` and
189
236
  * - a form `loginForm` if not loggedin.
190
237
  */
191
- public async checkSession(accessToken?: string): Promise<{
238
+ public async checkSession(): Promise<{
192
239
  isLoggedIn: boolean
193
240
  userName: string
194
241
  loginForm?: any
195
242
  }> {
196
- const { isLoggedIn, userName } = await this.fetchUserName(accessToken)
243
+ const { isLoggedIn, userName } = await this.fetchUserName()
197
244
  let loginForm = null
198
245
 
199
- if (!isLoggedIn && this.serverType !== ServerType.Sasjs) {
246
+ if (!isLoggedIn) {
200
247
  //We will logout to make sure cookies are removed and login form is presented
201
248
  //Residue can happen in case of session expiration
202
249
  await this.logOut()
203
250
 
204
- loginForm = await this.getNewLoginForm()
251
+ if (this.serverType !== ServerType.Sasjs)
252
+ loginForm = await this.getNewLoginForm()
205
253
  }
206
254
 
207
255
  return Promise.resolve({
@@ -221,7 +269,7 @@ export class AuthManager {
221
269
  return await this.getLoginForm(formResponse)
222
270
  }
223
271
 
224
- private async fetchUserName(accessToken?: string): Promise<{
272
+ private async fetchUserName(): Promise<{
225
273
  isLoggedIn: boolean
226
274
  userName: string
227
275
  }> {
@@ -232,9 +280,8 @@ export class AuthManager {
232
280
  ? `${this.serverUrl}/SASStoredProcess`
233
281
  : `${this.serverUrl}/SASjsApi/session`
234
282
 
235
- // Access token is required for server type `SASjs`
236
283
  const { result: loginResponse } = await this.requestClient
237
- .get<string>(url, accessToken, 'text/plain')
284
+ .get<string>(url, undefined, 'text/plain')
238
285
  .catch((err: any) => {
239
286
  return { result: 'authErr' }
240
287
  })
@@ -315,11 +362,19 @@ export class AuthManager {
315
362
  * Logs out of the configured SAS server.
316
363
  * @param accessToken - an optional access token is required for SASjs server type.
317
364
  */
318
- public logOut(accessToken?: string) {
365
+ public async logOut() {
319
366
  if (this.serverType === ServerType.Sasjs) {
320
- return this.requestClient.post(this.logoutUrl, undefined, accessToken)
367
+ return this.requestClient
368
+ .delete(this.logoutUrl)
369
+ .catch(() => true)
370
+ .finally(() => {
371
+ this.requestClient.clearLocalStorageTokens()
372
+ return true
373
+ })
321
374
  }
375
+
322
376
  this.requestClient.clearCsrfTokens()
377
+
323
378
  return this.requestClient.get(this.logoutUrl, undefined).then(() => true)
324
379
  }
325
380
  }
@@ -0,0 +1,36 @@
1
+ import { prefixMessage } from '@sasjs/utils/error'
2
+ import { RequestClient } from '../request/RequestClient'
3
+
4
+ /**
5
+ * Exchanges the auth code for an access token for the given client.
6
+ * @param requestClient - the pre-configured HTTP request client
7
+ * @param clientId - the client ID to authenticate with.
8
+ * @param authCode - the auth code received from the server.
9
+ */
10
+ export async function getAccessTokenForSasjs(
11
+ requestClient: RequestClient,
12
+ clientId: string,
13
+ authCode: string
14
+ ) {
15
+ const url = '/SASjsApi/auth/token'
16
+ const data = {
17
+ clientId,
18
+ code: authCode
19
+ }
20
+
21
+ return await requestClient
22
+ .post(url, data, undefined)
23
+ .then((res) => {
24
+ const sasAuth = res.result as {
25
+ accessToken: string
26
+ refreshToken: string
27
+ }
28
+ return {
29
+ access_token: sasAuth.accessToken,
30
+ refresh_token: sasAuth.refreshToken
31
+ }
32
+ })
33
+ .catch((err) => {
34
+ throw prefixMessage(err, 'Error while getting access token. ')
35
+ })
36
+ }
@@ -10,7 +10,7 @@ import { RequestClient } from '../request/RequestClient'
10
10
  * @param clientSecret - the client secret to authenticate with.
11
11
  * @param authCode - the auth code received from the server.
12
12
  */
13
- export async function getAccessToken(
13
+ export async function getAccessTokenForViya(
14
14
  requestClient: RequestClient,
15
15
  clientId: string,
16
16
  clientSecret: string,
@@ -0,0 +1,31 @@
1
+ import { prefixMessage } from '@sasjs/utils/error'
2
+ import { RequestClient } from '../request/RequestClient'
3
+
4
+ /**
5
+ * Performs a login authenticate and returns an auth code for the given client.
6
+ * @param requestClient - the pre-configured HTTP request client
7
+ * @param username - a string representing the username.
8
+ * @param password - a string representing the password.
9
+ * @param clientId - the client ID to authenticate with.
10
+ */
11
+ export const getAuthCodeForSasjs = async (
12
+ requestClient: RequestClient,
13
+ username: string,
14
+ password: string,
15
+ clientId: string
16
+ ) => {
17
+ const url = '/SASjsApi/auth/authorize'
18
+ const data = { username, password, clientId }
19
+
20
+ const { code: authCode } = await requestClient
21
+ .post<{ code: string }>(url, data, undefined)
22
+ .then((res) => res.result)
23
+ .catch((err) => {
24
+ throw prefixMessage(
25
+ err,
26
+ 'Error while authenticating with provided username, password and clientId. '
27
+ )
28
+ })
29
+
30
+ return authCode
31
+ }
@@ -3,18 +3,21 @@ import {
3
3
  isRefreshTokenExpiring,
4
4
  hasTokenExpired
5
5
  } from '@sasjs/utils/auth'
6
- import { AuthConfig } from '@sasjs/utils/types'
6
+ import { AuthConfig, ServerType } from '@sasjs/utils/types'
7
7
  import { RequestClient } from '../request/RequestClient'
8
- import { refreshTokens } from './refreshTokens'
8
+ import { refreshTokensForViya } from './refreshTokensForViya'
9
+ import { refreshTokensForSasjs } from './refreshTokensForSasjs'
9
10
 
10
11
  /**
11
12
  * Returns the auth configuration, refreshing the tokens if necessary.
12
13
  * @param requestClient - the pre-configured HTTP request client
13
14
  * @param authConfig - an object containing a client ID, secret, access token and refresh token
15
+ * @param serverType - server type for which refreshing the tokens, defaults to SASVIYA
14
16
  */
15
17
  export async function getTokens(
16
18
  requestClient: RequestClient,
17
- authConfig: AuthConfig
19
+ authConfig: AuthConfig,
20
+ serverType: ServerType = ServerType.SasViya
18
21
  ): Promise<AuthConfig> {
19
22
  const logger = process.logger || console
20
23
  let { access_token, refresh_token, client, secret } = authConfig
@@ -29,12 +32,16 @@ export async function getTokens(
29
32
  throw new Error(error)
30
33
  }
31
34
  logger.info('Refreshing access and refresh tokens.')
32
- ;({ access_token, refresh_token } = await refreshTokens(
33
- requestClient,
34
- client,
35
- secret,
36
- refresh_token
37
- ))
35
+ const tokens =
36
+ serverType === ServerType.SasViya
37
+ ? await refreshTokensForViya(
38
+ requestClient,
39
+ client,
40
+ secret,
41
+ refresh_token
42
+ )
43
+ : await refreshTokensForSasjs(requestClient, refresh_token)
44
+ ;({ access_token, refresh_token } = tokens)
38
45
  }
39
46
  return { access_token, refresh_token, client, secret }
40
47
  }
@@ -0,0 +1,35 @@
1
+ import { prefixMessage } from '@sasjs/utils/error'
2
+ import { RequestClient } from '../request/RequestClient'
3
+
4
+ /**
5
+ * Exchanges the refresh token for an access token for the given client.
6
+ * @param requestClient - the pre-configured HTTP request client
7
+ * @param refreshToken - the refresh token received from the server.
8
+ */
9
+ export async function refreshTokensForSasjs(
10
+ requestClient: RequestClient,
11
+ refreshToken: string
12
+ ) {
13
+ const url = '/SASjsApi/auth/refresh'
14
+ const headers = {
15
+ Authorization: 'Bearer ' + refreshToken
16
+ }
17
+
18
+ const authResponse = await requestClient
19
+ .post(url, undefined, undefined, undefined, headers)
20
+ .then((res) => {
21
+ const sasAuth = res.result as {
22
+ accessToken: string
23
+ refreshToken: string
24
+ }
25
+ return {
26
+ access_token: sasAuth.accessToken,
27
+ refresh_token: sasAuth.refreshToken
28
+ }
29
+ })
30
+ .catch((err) => {
31
+ throw prefixMessage(err, 'Error while refreshing tokens')
32
+ })
33
+
34
+ return authResponse
35
+ }
@@ -8,9 +8,9 @@ import { RequestClient } from '../request/RequestClient'
8
8
  * @param requestClient - the pre-configured HTTP request client
9
9
  * @param clientId - the client ID to authenticate with.
10
10
  * @param clientSecret - the client secret to authenticate with.
11
- * @param authCode - the refresh token received from the server.
11
+ * @param refreshToken - the refresh token received from the server.
12
12
  */
13
- export async function refreshTokens(
13
+ export async function refreshTokensForViya(
14
14
  requestClient: RequestClient,
15
15
  clientId: string,
16
16
  clientSecret: string,
@@ -13,6 +13,11 @@ export const mockAuthResponse: SasAuthResponse = {
13
13
  jti: 'test'
14
14
  }
15
15
 
16
+ export const mockSasjsAuthResponse = {
17
+ access_token: 'acc355',
18
+ refresh_token: 'r3fr35h'
19
+ }
20
+
16
21
  export const generateToken = (timeToLiveSeconds: number): string => {
17
22
  const exp =
18
23
  new Date(new Date().getTime() + timeToLiveSeconds * 1000).getTime() / 1000
@@ -144,7 +144,6 @@ export class WebJobExecutor extends BaseJobExecutor {
144
144
  }
145
145
 
146
146
  const requestPromise = new Promise((resolve, reject) => {
147
- // Access token is required for server type `SASjs`
148
147
  this.requestClient!.post(apiUrl, formData, authConfig?.access_token)
149
148
  .then(async (res: any) => {
150
149
  const resObj =
@@ -158,11 +157,6 @@ export class WebJobExecutor extends BaseJobExecutor {
158
157
 
159
158
  let jsonResponse = res.result
160
159
 
161
- if (this.serverType === ServerType.Sasjs) {
162
- const webout = parseWeboutResponse(res.result._webout, apiUrl)
163
- jsonResponse = getValidJson(webout)
164
- }
165
-
166
160
  if (config.debug) {
167
161
  switch (this.serverType) {
168
162
  case ServerType.SasViya:
@@ -178,7 +172,13 @@ export class WebJobExecutor extends BaseJobExecutor {
178
172
  ? parseWeboutResponse(res.result, apiUrl)
179
173
  : res.result
180
174
  break
175
+ case ServerType.Sasjs:
176
+ const webout = parseWeboutResponse(res.result._webout, apiUrl)
177
+ jsonResponse = getValidJson(webout)
178
+ break
181
179
  }
180
+ } else if (this.serverType === ServerType.Sasjs) {
181
+ jsonResponse = getValidJson(res.result._webout)
182
182
  }
183
183
 
184
184
  const responseObject = appendExtraResponseAttributes(
@@ -48,7 +48,9 @@ export interface HttpClient {
48
48
  ): Promise<{ result: T; etag: string }>
49
49
 
50
50
  getCsrfToken(type: 'general' | 'file'): CsrfToken | undefined
51
+ saveLocalStorageToken(accessToken: string, refreshToken: string): void
51
52
  clearCsrfTokens(): void
53
+ clearLocalStorageTokens(): void
52
54
  getBaseUrl(): string
53
55
  }
54
56
 
@@ -70,6 +72,11 @@ export class RequestClient implements HttpClient {
70
72
  this.createHttpClient(baseUrl, httpsAgentOptions)
71
73
  }
72
74
 
75
+ public saveLocalStorageToken(accessToken: string, refreshToken: string) {
76
+ localStorage.setItem('accessToken', accessToken)
77
+ localStorage.setItem('refreshToken', refreshToken)
78
+ }
79
+
73
80
  public getCsrfToken(type: 'general' | 'file' = 'general') {
74
81
  return type === 'file' ? this.fileUploadCsrfToken : this.csrfToken
75
82
  }
@@ -78,6 +85,10 @@ export class RequestClient implements HttpClient {
78
85
  this.csrfToken = { headerName: '', value: '' }
79
86
  this.fileUploadCsrfToken = { headerName: '', value: '' }
80
87
  }
88
+ public clearLocalStorageTokens() {
89
+ localStorage.setItem('accessToken', '')
90
+ localStorage.setItem('refreshToken', '')
91
+ }
81
92
 
82
93
  public getBaseUrl() {
83
94
  return this.httpClient.defaults.baseURL || ''
@@ -184,9 +195,7 @@ export class RequestClient implements HttpClient {
184
195
  }
185
196
  ),
186
197
  debug
187
- ).catch((err) => {
188
- throw prefixMessage(err, 'Error while handling error. ')
189
- })
198
+ )
190
199
  })
191
200
  }
192
201
 
@@ -206,6 +215,7 @@ export class RequestClient implements HttpClient {
206
215
  .post<T>(url, data, { headers, withCredentials: true })
207
216
  .then((response) => {
208
217
  throwIfError(response)
218
+
209
219
  return this.parseResponse<T>(response)
210
220
  })
211
221
  .catch(async (e) => {
@@ -455,6 +465,8 @@ export class RequestClient implements HttpClient {
455
465
 
456
466
  if (e instanceof LoginRequiredError) {
457
467
  this.clearCsrfTokens()
468
+
469
+ throw e
458
470
  }
459
471
 
460
472
  if (response?.status === 403 || response?.status === 449) {
@@ -477,7 +489,8 @@ export class RequestClient implements HttpClient {
477
489
  else return
478
490
  }
479
491
 
480
- throw e
492
+ if (e.message) throw e
493
+ else throw prefixMessage(e, 'Error while handling error. ')
481
494
  }
482
495
 
483
496
  protected parseResponse<T>(response: AxiosResponse<any>) {
@@ -529,8 +542,9 @@ export class RequestClient implements HttpClient {
529
542
 
530
543
  this.httpClient = createAxiosInstance(baseUrl, httpsAgent)
531
544
 
532
- this.httpClient.defaults.validateStatus = (status) =>
533
- status >= 200 && status < 401
545
+ this.httpClient.defaults.validateStatus = (status) => {
546
+ return status >= 200 && status <= 401
547
+ }
534
548
  }
535
549
  }
536
550
 
@@ -87,7 +87,7 @@ export class Sas9RequestClient extends RequestClient {
87
87
  })
88
88
  }
89
89
 
90
- public post<T>(
90
+ public async post<T>(
91
91
  url: string,
92
92
  data: any,
93
93
  accessToken: string | undefined,
@@ -0,0 +1,23 @@
1
+ import { RequestClient } from './RequestClient'
2
+
3
+ /**
4
+ * Specific request client for SASJS.
5
+ * Append tokens in headers.
6
+ */
7
+ export class SasjsRequestClient extends RequestClient {
8
+ getHeaders = (accessToken: string | undefined, contentType: string) => {
9
+ const headers: any = {}
10
+
11
+ if (contentType !== 'application/x-www-form-urlencoded')
12
+ headers['Content-Type'] = contentType
13
+
14
+ headers.Accept = contentType === 'application/json' ? contentType : '*/*'
15
+
16
+ if (!accessToken && typeof window !== 'undefined')
17
+ accessToken = localStorage.getItem('accessToken') ?? undefined
18
+
19
+ if (accessToken) headers.Authorization = `Bearer ${accessToken}`
20
+
21
+ return headers
22
+ }
23
+ }
@@ -18,6 +18,7 @@ app.get('/', function (req: any, res: any) {
18
18
 
19
19
  app.post('/SASLogon/oauth/token', function (req: any, res: any) {
20
20
  let valid = true
21
+
21
22
  // capture the encoded form data
22
23
  req.on('data', (data: any) => {
23
24
  const resData = data.toString()