mirror of
https://github.com/sasjs/adapter.git
synced 2026-01-08 13:00:05 +00:00
chore(*): refactor common functionality into JobExecutor, handle all auth-related scenarios
This commit is contained in:
@@ -1,19 +1,18 @@
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import axios, { AxiosInstance } from 'axios'
|
||||
import { isAuthorizeFormRequired, parseAndSubmitAuthorizeForm } from '.'
|
||||
import { isAuthorizeFormRequired } from '.'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import { serialize } from '../utils'
|
||||
|
||||
export class AuthManager {
|
||||
public userName = ''
|
||||
private loginUrl: string
|
||||
private logoutUrl: string
|
||||
private httpClient: AxiosInstance
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
private serverType: ServerType,
|
||||
private requestClient: RequestClient,
|
||||
private loginCallback: () => Promise<void>
|
||||
) {
|
||||
this.httpClient = axios.create({ baseURL: this.serverUrl })
|
||||
this.loginUrl = `/SASLogon/login`
|
||||
this.logoutUrl =
|
||||
this.serverType === ServerType.Sas9
|
||||
@@ -50,21 +49,21 @@ export class AuthManager {
|
||||
}
|
||||
const loginParamsStr = serialize(loginParams)
|
||||
|
||||
const loginResponse = await this.httpClient
|
||||
.post<string>(this.loginUrl, loginParamsStr, {
|
||||
withCredentials: true,
|
||||
responseType: 'text',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: '*/*'
|
||||
}
|
||||
})
|
||||
.then((response) => response.data)
|
||||
const { result: loginResponse } = await this.requestClient.post<string>(
|
||||
this.loginUrl,
|
||||
loginParamsStr,
|
||||
undefined,
|
||||
'text/plain',
|
||||
{
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: '*/*'
|
||||
}
|
||||
)
|
||||
|
||||
let loggedIn
|
||||
|
||||
if (isAuthorizeFormRequired(loginResponse)) {
|
||||
await parseAndSubmitAuthorizeForm(loginResponse, this.serverUrl)
|
||||
await this.requestClient.authorize(loginResponse)
|
||||
} else {
|
||||
loggedIn = isLogInSuccess(loginResponse)
|
||||
}
|
||||
@@ -89,16 +88,12 @@ export class AuthManager {
|
||||
* @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`.
|
||||
*/
|
||||
public async checkSession() {
|
||||
const loginResponse = await this.httpClient.get(
|
||||
const { result: loginResponse } = await this.requestClient.get<string>(
|
||||
this.loginUrl.replace('.do', ''),
|
||||
{
|
||||
responseType: 'text',
|
||||
headers: {
|
||||
Accept: '*/*'
|
||||
}
|
||||
}
|
||||
undefined,
|
||||
'text/plain'
|
||||
)
|
||||
const responseText = loginResponse?.data
|
||||
const responseText = loginResponse
|
||||
const isLoggedIn = /<button.+onClick.+logout/gm.test(responseText)
|
||||
let loginForm: any = null
|
||||
|
||||
@@ -158,7 +153,8 @@ export class AuthManager {
|
||||
* Logs out of the configured SAS server.
|
||||
*/
|
||||
public logOut() {
|
||||
return this.httpClient.get(this.logoutUrl).then(() => true)
|
||||
this.requestClient.clearCsrfTokens()
|
||||
return this.requestClient.get(this.logoutUrl, undefined).then(() => true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { AuthManager } from './AuthManager'
|
||||
|
||||
export * from './AuthManager'
|
||||
export * from './isAuthorizeFormRequired'
|
||||
export * from './isLoginRequired'
|
||||
export * from './parseAndSubmitAuthorizeForm'
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export const parseAndSubmitAuthorizeForm = async (
|
||||
response: string,
|
||||
serverUrl: string
|
||||
) => {
|
||||
let authUrl: string | null = null
|
||||
const params: any = {}
|
||||
|
||||
const responseBody = response.split('<body>')[1].split('</body>')[0]
|
||||
const bodyElement = document.createElement('div')
|
||||
bodyElement.innerHTML = responseBody
|
||||
|
||||
const form = bodyElement.querySelector('#application_authorization')
|
||||
authUrl = form ? serverUrl + form.getAttribute('action') : null
|
||||
|
||||
const inputs: any = form?.querySelectorAll('input')
|
||||
|
||||
for (const input of inputs) {
|
||||
if (input.name === 'user_oauth_approval') {
|
||||
input.value = 'true'
|
||||
}
|
||||
|
||||
params[input.name] = input.value
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
for (const key in params) {
|
||||
if (params.hasOwnProperty(key)) {
|
||||
formData.append(key, params[key])
|
||||
}
|
||||
}
|
||||
|
||||
if (!authUrl) {
|
||||
throw new Error('Auth Form URL is null or undefined.')
|
||||
}
|
||||
|
||||
return await axios
|
||||
.post(authUrl, formData, {
|
||||
withCredentials: true,
|
||||
responseType: 'text',
|
||||
headers: {
|
||||
Accept: '*/*'
|
||||
}
|
||||
})
|
||||
.then((res) => res.data)
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { AuthManager } from '../AuthManager'
|
||||
import * as dotenv from 'dotenv'
|
||||
import * as authModule from '..'
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import axios from 'axios'
|
||||
import {
|
||||
@@ -8,8 +7,8 @@ import {
|
||||
mockLoginSuccessResponse
|
||||
} from './mockResponses'
|
||||
import { serialize } from '../../utils'
|
||||
import { RequestClient } from '../../request/RequestClient'
|
||||
jest.mock('axios')
|
||||
jest.mock('../parseAndSubmitAuthorizeForm')
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||
|
||||
describe('AuthManager', () => {
|
||||
@@ -18,6 +17,7 @@ describe('AuthManager', () => {
|
||||
const serverType = ServerType.SasViya
|
||||
const userName = 'test-username'
|
||||
const password = 'test-password'
|
||||
const requestClient = new RequestClient(serverUrl)
|
||||
|
||||
beforeAll(() => {
|
||||
dotenv.config()
|
||||
@@ -25,12 +25,16 @@ describe('AuthManager', () => {
|
||||
})
|
||||
|
||||
it('should instantiate and set the correct URLs for a Viya server', () => {
|
||||
const authManager = new AuthManager(serverUrl, serverType, authCallback)
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
|
||||
expect(authManager).toBeTruthy()
|
||||
expect((authManager as any).serverUrl).toEqual(serverUrl)
|
||||
expect((authManager as any).serverType).toEqual(serverType)
|
||||
expect((authManager as any).httpClient).toBeTruthy()
|
||||
expect((authManager as any).loginUrl).toEqual(`/SASLogon/login`)
|
||||
expect((authManager as any).logoutUrl).toEqual('/SASLogon/logout.do?')
|
||||
})
|
||||
@@ -39,18 +43,27 @@ describe('AuthManager', () => {
|
||||
const authCallback = () => Promise.resolve()
|
||||
const serverType = ServerType.Sas9
|
||||
|
||||
const authManager = new AuthManager(serverUrl, serverType, authCallback)
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
|
||||
expect(authManager).toBeTruthy()
|
||||
expect((authManager as any).serverUrl).toEqual(serverUrl)
|
||||
expect((authManager as any).serverType).toEqual(serverType)
|
||||
expect((authManager as any).httpClient).toBeTruthy()
|
||||
expect((authManager as any).loginUrl).toEqual(`/SASLogon/login`)
|
||||
expect((authManager as any).logoutUrl).toEqual('/SASLogon/logout?')
|
||||
})
|
||||
|
||||
it('should call the auth callback and return when already logged in', async (done) => {
|
||||
const authManager = new AuthManager(serverUrl, serverType, authCallback)
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: true,
|
||||
@@ -68,7 +81,12 @@ describe('AuthManager', () => {
|
||||
})
|
||||
|
||||
it('should post a login request to the server if not logged in', async (done) => {
|
||||
const authManager = new AuthManager(serverUrl, serverType, authCallback)
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: false,
|
||||
@@ -96,7 +114,6 @@ describe('AuthManager', () => {
|
||||
loginParams,
|
||||
{
|
||||
withCredentials: true,
|
||||
responseType: 'text',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: '*/*'
|
||||
@@ -108,9 +125,14 @@ describe('AuthManager', () => {
|
||||
})
|
||||
|
||||
it('should parse and submit the authorisation form when necessary', async (done) => {
|
||||
const authManager = new AuthManager(serverUrl, serverType, authCallback)
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
jest
|
||||
.spyOn(authModule, 'parseAndSubmitAuthorizeForm')
|
||||
.spyOn(requestClient, 'authorize')
|
||||
.mockImplementation(() => Promise.resolve())
|
||||
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
@@ -125,15 +147,19 @@ describe('AuthManager', () => {
|
||||
|
||||
await authManager.logIn(userName, password)
|
||||
|
||||
expect(authModule.parseAndSubmitAuthorizeForm).toHaveBeenCalledWith(
|
||||
mockLoginAuthoriseRequiredResponse,
|
||||
serverUrl
|
||||
expect(requestClient.authorize).toHaveBeenCalledWith(
|
||||
mockLoginAuthoriseRequiredResponse
|
||||
)
|
||||
done()
|
||||
})
|
||||
|
||||
it('should check and return session information if logged in', async (done) => {
|
||||
const authManager = new AuthManager(serverUrl, serverType, authCallback)
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: '<button onClick="logout">' })
|
||||
)
|
||||
@@ -141,9 +167,12 @@ describe('AuthManager', () => {
|
||||
const response = await authManager.checkSession()
|
||||
expect(response.isLoggedIn).toBeTruthy()
|
||||
expect(mockedAxios.get).toHaveBeenNthCalledWith(1, `/SASLogon/login`, {
|
||||
withCredentials: true,
|
||||
responseType: 'text',
|
||||
transformResponse: undefined,
|
||||
headers: {
|
||||
Accept: '*/*'
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -151,7 +180,12 @@ describe('AuthManager', () => {
|
||||
})
|
||||
|
||||
it('should check and return session information if logged in', async (done) => {
|
||||
const authManager = new AuthManager(serverUrl, serverType, authCallback)
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: '<button onClick="logout">' })
|
||||
)
|
||||
@@ -159,9 +193,12 @@ describe('AuthManager', () => {
|
||||
const response = await authManager.checkSession()
|
||||
expect(response.isLoggedIn).toBeTruthy()
|
||||
expect(mockedAxios.get).toHaveBeenNthCalledWith(1, `/SASLogon/login`, {
|
||||
withCredentials: true,
|
||||
responseType: 'text',
|
||||
transformResponse: undefined,
|
||||
headers: {
|
||||
Accept: '*/*'
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user