diff --git a/src/SASjs.ts b/src/SASjs.ts index 1d3281a..77f4680 100644 --- a/src/SASjs.ts +++ b/src/SASjs.ts @@ -11,12 +11,8 @@ require('isomorphic-fetch') import { convertToCSV, compareTimestamps, - serialize, - isAuthorizeFormRequired, - parseAndSubmitAuthorizeForm, splitChunks, isLogInRequired, - isLogInSuccess, parseSourceCode, parseGeneratedCode, parseWeboutResponse, @@ -38,6 +34,7 @@ import { import { SASViyaApiClient } from './SASViyaApiClient' import { SAS9ApiClient } from './SAS9ApiClient' import { FileUploader } from './FileUploader' +import { AuthManager } from './auth/auth' const defaultConfig: SASjsConfig = { serverUrl: '', @@ -59,8 +56,6 @@ const requestRetryLimit = 5 export default class SASjs { private sasjsConfig: SASjsConfig = new SASjsConfig() private jobsPath: string = '' - private logoutUrl: string = '' - private loginUrl: string = '' private csrfTokenApi: CsrfToken | null = null private csrfTokenWeb: CsrfToken | null = null private retryCountWeb: number = 0 @@ -68,10 +63,10 @@ export default class SASjs { private retryCountJeseApi: number = 0 private sasjsRequests: SASjsRequest[] = [] private sasjsWaitingRequests: SASjsWaitingRequest[] = [] - private userName: string = '' private sasViyaApiClient: SASViyaApiClient | null = null private sas9ApiClient: SAS9ApiClient | null = null private fileUploader: FileUploader | null = null + private authManager: AuthManager | null = null constructor(config?: any) { this.sasjsConfig = { @@ -433,7 +428,7 @@ export default class SASjs { * */ public getUserName() { - return this.userName + return this.authManager!.userName } /** @@ -475,48 +470,12 @@ export default class SASjs { } } - private async getLoginForm(response: any) { - const pattern: RegExp = // - const matches = pattern.exec(response) - const formInputs: any = {} - - if (matches && matches.length) { - this.setLoginUrl(matches) - const inputs = response.match(/]*>/g) - - if (inputs) { - inputs.forEach((inputStr: string) => { - const valueMatch = inputStr.match(/name="([^"]*)"\svalue="([^"]*)/) - - if (valueMatch && valueMatch.length) { - formInputs[valueMatch[1]] = valueMatch[2] - } - }) - } - } - - return Object.keys(formInputs).length ? formInputs : null - } - /** * Checks whether a session is active, or login is required. * @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`. */ public async checkSession() { - const loginResponse = await fetch(this.loginUrl.replace('.do', '')) - const responseText = await loginResponse.text() - const isLoggedIn = / response.text()) - .then(async (responseText) => { - let authFormRes: any - let loggedIn - - if (isAuthorizeFormRequired(responseText)) { - authFormRes = await parseAndSubmitAuthorizeForm( - responseText, - this.sasjsConfig.serverUrl - ) - } else { - loggedIn = isLogInSuccess(responseText) - } - - if (!loggedIn) { - const currentSession = await this.checkSession() - loggedIn = currentSession.isLoggedIn - } - - if (loggedIn) { - this.resendWaitingRequests() - } - - return { - isLoggedIn: loggedIn, - userName: this.userName - } - }) - .catch((e) => Promise.reject(e)) + return this.authManager!.logIn(username, password) } /** * Logs out of the configured SAS server. */ public logOut() { - return new Promise((resolve, reject) => { - const logOutURL = `${this.sasjsConfig.serverUrl}${this.logoutUrl}` - fetch(logOutURL) - .then(() => { - resolve(true) - }) - .catch((err: Error) => reject(err)) - }) + return this.authManager!.logOut() } /** @@ -1174,7 +1066,6 @@ export default class SASjs { this.sasjsWaitingRequests.push(sasjsWaitingRequest) } else { if (config.serverType === ServerType.SAS9 && config.debug) { - this.updateUsername(responseText) const jsonResponseText = parseWeboutResponse(responseText) if (jsonResponseText !== '') { @@ -1194,7 +1085,6 @@ export default class SASjs { try { this.parseSASVIYADebugResponse(responseText).then( (resText: any) => { - this.updateUsername(resText) try { resolve(JSON.parse(resText)) } catch (e) { @@ -1224,7 +1114,6 @@ export default class SASjs { ) } } else { - this.updateUsername(responseText) if ( responseText.includes( 'The requested URL /SASStoredProcess/do/ was not found on this server.' @@ -1304,19 +1193,6 @@ export default class SASjs { return requestParams } - private updateUsername(response: any) { - try { - const responseJson = JSON.parse(response) - if (this.sasjsConfig.serverType === ServerType.SAS9) { - this.userName = responseJson['_METAUSER'] - } else { - this.userName = responseJson['SYSUSERID'] - } - } catch (e) { - this.userName = '' - } - } - private parseSASVIYADebugResponse(response: string) { return new Promise((resolve, reject) => { const iframeStart = response.split( @@ -1527,11 +1403,11 @@ export default class SASjs { this.sasjsConfig.serverType === ServerType.SASViya ? this.sasjsConfig.pathSASViya : this.sasjsConfig.pathSAS9 - this.loginUrl = `${this.sasjsConfig.serverUrl}/SASLogon/login` - this.logoutUrl = - this.sasjsConfig.serverType === ServerType.SAS9 - ? '/SASLogon/logout?' - : '/SASLogon/logout.do?' + this.authManager = new AuthManager( + this.sasjsConfig.serverUrl, + this.sasjsConfig.serverType!, + this.resendWaitingRequests + ) if (this.sasjsConfig.serverType === ServerType.SASViya) { if (this.sasViyaApiClient) @@ -1563,24 +1439,6 @@ export default class SASjs { ) } - private setLoginUrl = (matches: RegExpExecArray) => { - let parsedURL = matches[1].replace(/\?.*/, '') - if (parsedURL[0] === '/') { - parsedURL = parsedURL.substr(1) - - const tempLoginLink = this.sasjsConfig.serverUrl - ? `${this.sasjsConfig.serverUrl}/${parsedURL}` - : `${parsedURL}` - - const loginUrl = tempLoginLink - - this.loginUrl = - this.sasjsConfig.serverType === ServerType.SASViya - ? tempLoginLink - : loginUrl.replace('.do', '') - } - } - private async createFoldersAndServices( parentFolder: string, membersJson: any[], diff --git a/src/auth/auth.ts b/src/auth/auth.ts new file mode 100644 index 0000000..74945fa --- /dev/null +++ b/src/auth/auth.ts @@ -0,0 +1,205 @@ +import axios, { AxiosInstance } from 'axios' +import { ServerType } from '../types' +import { + serialize, + isAuthorizeFormRequired, + parseAndSubmitAuthorizeForm, + isLogInSuccess +} from '../utils' + +export class AuthManager { + public userName = '' + private loginUrl: string + private logoutUrl: string + private httpClient: AxiosInstance + constructor( + private serverUrl: string, + private serverType: ServerType, + private loginCallback: Function + ) { + this.httpClient = axios.create({ baseURL: this.serverUrl }) + this.loginUrl = `/SASLogon/login` + this.logoutUrl = + this.serverType === ServerType.SAS9 + ? '/SASLogon/logout?' + : '/SASLogon/logout.do?' + } + + /** + * Logs into the SAS server with the supplied credentials. + * @param username - a string representing the username. + * @param password - a string representing the password. + */ + public async logIn(username: string, password: string) { + const loginParams: any = { + _service: 'default', + username, + password + } + + this.userName = loginParams.username + + const { isLoggedIn, loginForm } = await this.checkSession() + if (isLoggedIn) { + this.loginCallback() + + return { + isLoggedIn, + userName: this.userName + } + } + + for (const key in loginForm) { + loginParams[key] = loginForm[key] + } + const loginParamsStr = serialize(loginParams) + + const loginResponse = await axios + .post(this.loginUrl, loginParamsStr, { + withCredentials: true, + responseType: 'text', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' } + }) + .then((response) => response.data) + + let loggedIn + + if (isAuthorizeFormRequired(loginResponse)) { + await parseAndSubmitAuthorizeForm(loginResponse, this.serverUrl) + } else { + loggedIn = isLogInSuccess(loginResponse) + } + + if (!loggedIn) { + const currentSession = await this.checkSession() + loggedIn = currentSession.isLoggedIn + } + + if (loggedIn) { + this.loginCallback() + } + + return { + isLoggedIn: !!loggedIn, + userName: this.userName + } + + return { + isLoggedIn: isLogInSuccess(loginResponse), + userName: this.userName + } + + return fetch(this.loginUrl, { + method: 'POST', + credentials: 'include', + referrerPolicy: 'same-origin', + body: loginParamsStr, + headers: new Headers({ + 'Content-Type': 'application/x-www-form-urlencoded' + }) + }) + .then((response) => response.text()) + .then(async (responseText) => { + let loggedIn + + if (isAuthorizeFormRequired(responseText)) { + const authFormResponse = await parseAndSubmitAuthorizeForm( + responseText, + this.serverUrl + ) + } else { + loggedIn = isLogInSuccess(responseText) + } + + if (!loggedIn) { + const currentSession = await this.checkSession() + loggedIn = currentSession.isLoggedIn + } + + if (loggedIn) { + this.loginCallback() + } + + return { + isLoggedIn: loggedIn, + userName: this.userName + } + }) + .catch((e) => Promise.reject(e)) + } + + /** + * Checks whether a session is active, or login is required. + * @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`. + */ + public async checkSession() { + const loginResponse = await fetch(this.loginUrl.replace('.do', '')) + const responseText = await loginResponse.text() + const isLoggedIn = // + const matches = pattern.exec(response) + const formInputs: any = {} + + if (matches && matches.length) { + this.setLoginUrl(matches) + const inputs = response.match(/]*>/g) + + if (inputs) { + inputs.forEach((inputStr: string) => { + const valueMatch = inputStr.match(/name="([^"]*)"\svalue="([^"]*)/) + + if (valueMatch && valueMatch.length) { + formInputs[valueMatch[1]] = valueMatch[2] + } + }) + } + } + + return Object.keys(formInputs).length ? formInputs : null + } + + private setLoginUrl = (matches: RegExpExecArray) => { + let parsedURL = matches[1].replace(/\?.*/, '') + if (parsedURL[0] === '/') { + parsedURL = parsedURL.substr(1) + + const tempLoginLink = this.serverUrl + ? `${this.serverUrl}/${parsedURL}` + : `${parsedURL}` + + const loginUrl = tempLoginLink + + this.loginUrl = + this.serverType === ServerType.SASViya + ? tempLoginLink + : loginUrl.replace('.do', '') + } + } + + /** + * Logs out of the configured SAS server. + */ + public logOut() { + return new Promise((resolve, reject) => { + fetch(this.logoutUrl) + .then(() => { + resolve(true) + }) + .catch((err: Error) => reject(err)) + }) + } +}