From 830a907bd1c6546ed74fa969546b37681cc7967f Mon Sep 17 00:00:00 2001 From: Krishna Acondy Date: Sat, 21 Aug 2021 21:36:50 +0100 Subject: [PATCH 01/16] feat(login): add redirected login mechanism --- src/SASjs.ts | 30 ++++++++++++++++++++++++++---- src/api/viya/pollJobState.ts | 4 +--- src/auth/AuthManager.ts | 33 ++++++++++++++++++++++++++++++++- src/types/SASjsConfig.ts | 9 +++++++++ src/utils/delay.ts | 2 ++ src/utils/index.ts | 1 + 6 files changed, 71 insertions(+), 8 deletions(-) create mode 100644 src/utils/delay.ts diff --git a/src/SASjs.ts b/src/SASjs.ts index 5924985..2c08f70 100644 --- a/src/SASjs.ts +++ b/src/SASjs.ts @@ -1,5 +1,11 @@ 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 { SAS9ApiClient } from './SAS9ApiClient' import { FileUploader } from './FileUploader' @@ -29,7 +35,8 @@ const defaultConfig: SASjsConfig = { debug: false, contextName: 'SAS Job Execution compute context', useComputeApi: null, - allowInsecureRequests: false + allowInsecureRequests: false, + loginMechanism: LoginMechanism.Default } /** @@ -526,8 +533,23 @@ export default class SASjs { * @param username - a string representing the username. * @param password - a string representing the password. */ - public async logIn(username: string, password: string) { - return this.authManager!.logIn(username, password) + public async logIn(username?: string, password?: string) { + 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() } /** diff --git a/src/api/viya/pollJobState.ts b/src/api/viya/pollJobState.ts index c4b05d0..6829611 100644 --- a/src/api/viya/pollJobState.ts +++ b/src/api/viya/pollJobState.ts @@ -4,7 +4,7 @@ import { getTokens } from '../../auth/getTokens' import { RequestClient } from '../../request/RequestClient' import { JobStatePollError } from '../../types/errors' import { Link, WriteStream } from '../../types' -import { isNode } from '../../utils' +import { delay, isNode } from '../../utils' export async function pollJobState( requestClient: RequestClient, @@ -246,5 +246,3 @@ const doPoll = async ( return { state, pollCount } } - -const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) diff --git a/src/auth/AuthManager.ts b/src/auth/AuthManager.ts index 7d29cf9..2747bc9 100644 --- a/src/auth/AuthManager.ts +++ b/src/auth/AuthManager.ts @@ -1,6 +1,6 @@ import { ServerType } from '@sasjs/utils/types' import { RequestClient } from '../request/RequestClient' -import { serialize } from '../utils' +import { delay, serialize } from '../utils' export class AuthManager { public userName = '' @@ -19,6 +19,37 @@ export class AuthManager { : '/SASLogon/logout.do?' } + public async redirectedLogIn() { + await this.logOut() + const loginPopup = window.open( + this.loginUrl.replace('.do', ''), + '_blank', + 'toolbar=0,location=0,menubar=0,width=500,height=500' + ) + let isLoggedIn = false + let startTime = new Date() + let elapsedSeconds = 0 + do { + await delay(1000) + isLoggedIn = + document.cookie.includes('Current-User') && + document.cookie.includes('userId') + elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000 + } while (!isLoggedIn && elapsedSeconds < 5 * 60) + + let isAuthorized = false + startTime = new Date() + do { + await delay(1000) + isAuthorized = !loginPopup?.window.location.href.includes('SASLogon') + elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000 + } while (!isAuthorized && elapsedSeconds < 5 * 60) + + loginPopup?.close() + + return { isLoggedIn: isLoggedIn && isAuthorized, userName: 'test' } + } + /** * Logs into the SAS server with the supplied credentials. * @param username - a string representing the username. diff --git a/src/types/SASjsConfig.ts b/src/types/SASjsConfig.ts index 2bdd84c..b562dcb 100644 --- a/src/types/SASjsConfig.ts +++ b/src/types/SASjsConfig.ts @@ -59,4 +59,13 @@ export class SASjsConfig { * Changing this setting is not recommended. */ allowInsecureRequests = false + /** + * Supported login mechanisms are - Redirected and Default + */ + loginMechanism: LoginMechanism = LoginMechanism.Default +} + +export enum LoginMechanism { + Default = 'Default', + Redirected = 'Redirected' } diff --git a/src/utils/delay.ts b/src/utils/delay.ts new file mode 100644 index 0000000..8e83610 --- /dev/null +++ b/src/utils/delay.ts @@ -0,0 +1,2 @@ +export const delay = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)) diff --git a/src/utils/index.ts b/src/utils/index.ts index 2a05d63..0534f25 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,6 +1,7 @@ export * from './asyncForEach' export * from './compareTimestamps' export * from './convertToCsv' +export * from './delay' export * from './isNode' export * from './isRelativePath' export * from './isUri' From 97918f301ba8552d1a920d7774ca04ca3cd3892b Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Sun, 22 Aug 2021 03:57:23 +0500 Subject: [PATCH 02/16] chore(redirectLogin): centered popup + verifying sas9 login + sasviya login fixes --- src/auth/AuthManager.ts | 49 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/src/auth/AuthManager.ts b/src/auth/AuthManager.ts index 2747bc9..42ec203 100644 --- a/src/auth/AuthManager.ts +++ b/src/auth/AuthManager.ts @@ -20,17 +20,34 @@ export class AuthManager { } public async redirectedLogIn() { - await this.logOut() + const width = 500 + const height = 600 + const left = screen.width / 2 - width / 2 + const top = screen.height / 2 - height / 2 + const loginPopup = window.open( - this.loginUrl.replace('.do', ''), + this.loginUrl, '_blank', - 'toolbar=0,location=0,menubar=0,width=500,height=500' + `toolbar=0,location=0,menubar=0,width=${width},height=${height},left=${left},top=${top}` ) + + const { isLoggedIn } = + this.serverType === ServerType.SasViya + ? await this.verifyingPopUpLoginSASVIYA(loginPopup!) + : await this.verifyingPopUpLoginSAS9(loginPopup!) + + loginPopup?.close() + + return { isLoggedIn, userName: 'test' } + } + + async verifyingPopUpLoginSASVIYA(loginPopup: Window) { let isLoggedIn = false let startTime = new Date() let elapsedSeconds = 0 do { await delay(1000) + if (loginPopup.closed) break isLoggedIn = document.cookie.includes('Current-User') && document.cookie.includes('userId') @@ -41,13 +58,33 @@ export class AuthManager { startTime = new Date() do { await delay(1000) - isAuthorized = !loginPopup?.window.location.href.includes('SASLogon') + 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) - loginPopup?.close() + return { isLoggedIn: isLoggedIn && isAuthorized } + } - return { isLoggedIn: isLoggedIn && isAuthorized, userName: 'test' } + async verifyingPopUpLoginSAS9(loginPopup: Window) { + let isLoggedIn = false + let startTime = new Date() + let elapsedSeconds = 0 + do { + await delay(1000) + if (loginPopup.closed) break + + isLoggedIn = loginPopup.window.document.body.innerText.includes( + 'You have signed in.' + ) + elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000 + } while (!isLoggedIn && elapsedSeconds < 5 * 60) + + return { isLoggedIn } } /** From 1a59f95be74197e25af03a0ef4f617c65feb44bd Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Wed, 25 Aug 2021 07:55:04 +0500 Subject: [PATCH 03/16] fix: split code to files + corrected usage of loginCallback --- src/auth/AuthManager.ts | 80 ++++++++------------------ src/auth/openWebPage.ts | 21 +++++++ src/auth/verifyingPopUpLoginSAS9.ts | 18 ++++++ src/auth/verifyingPopUpLoginSASVIYA.ts | 29 ++++++++++ src/job-execution/WebJobExecutor.ts | 13 +++-- 5 files changed, 102 insertions(+), 59 deletions(-) create mode 100644 src/auth/openWebPage.ts create mode 100644 src/auth/verifyingPopUpLoginSAS9.ts create mode 100644 src/auth/verifyingPopUpLoginSASVIYA.ts diff --git a/src/auth/AuthManager.ts b/src/auth/AuthManager.ts index 42ec203..0915650 100644 --- a/src/auth/AuthManager.ts +++ b/src/auth/AuthManager.ts @@ -1,11 +1,15 @@ import { ServerType } from '@sasjs/utils/types' import { RequestClient } from '../request/RequestClient' -import { delay, serialize } from '../utils' +import { serialize } from '../utils' +import { openWebPage } from './openWebPage' +import { verifyingPopUpLoginSAS9 } from './verifyingPopUpLoginSAS9' +import { verifyingPopUpLoginSASVIYA } from './verifyingPopUpLoginSASVIYA' export class AuthManager { public userName = '' private loginUrl: string private logoutUrl: string + private loginPreventRedirectUrl = `/SASLogon/home` constructor( private serverUrl: string, private serverType: ServerType, @@ -20,69 +24,35 @@ export class AuthManager { } public async redirectedLogIn() { - const width = 500 - const height = 600 - const left = screen.width / 2 - width / 2 - const top = screen.height / 2 - height / 2 + const loginPopup = openWebPage(this.loginPreventRedirectUrl, 'SASLogon', { + width: 500, + height: 600 + }) - const loginPopup = window.open( - this.loginUrl, - '_blank', - `toolbar=0,location=0,menubar=0,width=${width},height=${height},left=${left},top=${top}` - ) + if (!loginPopup) { + alert('Unable to open popup for login. Please try with other browser.') + return { isLoggedIn: false } + } const { isLoggedIn } = this.serverType === ServerType.SasViya - ? await this.verifyingPopUpLoginSASVIYA(loginPopup!) - : await this.verifyingPopUpLoginSAS9(loginPopup!) + ? await verifyingPopUpLoginSASVIYA(loginPopup) + : await verifyingPopUpLoginSAS9(loginPopup) - loginPopup?.close() + loginPopup.close() - return { isLoggedIn, userName: 'test' } - } + if (isLoggedIn) { + if (this.serverType === ServerType.Sas9) { + const casAuthenticationUrl = `${this.serverUrl}/SASStoredProcess/j_spring_cas_security_check` - async verifyingPopUpLoginSASVIYA(loginPopup: Window) { - let isLoggedIn = false - let startTime = new Date() - let elapsedSeconds = 0 - do { - await delay(1000) - if (loginPopup.closed) break - isLoggedIn = - document.cookie.includes('Current-User') && - document.cookie.includes('userId') - 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.' + await this.requestClient.get( + `/SASLogon/login?service=${casAuthenticationUrl}`, + undefined ) - elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000 - } while (!isAuthorized && elapsedSeconds < 5 * 60) + } - return { isLoggedIn: isLoggedIn && isAuthorized } - } - - async verifyingPopUpLoginSAS9(loginPopup: Window) { - let isLoggedIn = false - let startTime = new Date() - let elapsedSeconds = 0 - do { - await delay(1000) - if (loginPopup.closed) break - - isLoggedIn = loginPopup.window.document.body.innerText.includes( - 'You have signed in.' - ) - elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000 - } while (!isLoggedIn && elapsedSeconds < 5 * 60) + await this.loginCallback() + } return { isLoggedIn } } diff --git a/src/auth/openWebPage.ts b/src/auth/openWebPage.ts new file mode 100644 index 0000000..75a3e10 --- /dev/null +++ b/src/auth/openWebPage.ts @@ -0,0 +1,21 @@ +interface windowFeatures { + width: number + height: number +} + +export function openWebPage( + url: string, + windowName: string = '', + { width, height }: windowFeatures +): Window | null { + 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}` + ) + + return loginPopup +} diff --git a/src/auth/verifyingPopUpLoginSAS9.ts b/src/auth/verifyingPopUpLoginSAS9.ts new file mode 100644 index 0000000..8eae30d --- /dev/null +++ b/src/auth/verifyingPopUpLoginSAS9.ts @@ -0,0 +1,18 @@ +import { delay } from '../utils' + +export async function verifyingPopUpLoginSAS9(loginPopup: Window) { + 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 } +} diff --git a/src/auth/verifyingPopUpLoginSASVIYA.ts b/src/auth/verifyingPopUpLoginSASVIYA.ts new file mode 100644 index 0000000..bff5ad4 --- /dev/null +++ b/src/auth/verifyingPopUpLoginSASVIYA.ts @@ -0,0 +1,29 @@ +import { delay } from '../utils' + +export async function verifyingPopUpLoginSASVIYA(loginPopup: Window) { + 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') diff --git a/src/job-execution/WebJobExecutor.ts b/src/job-execution/WebJobExecutor.ts index d3b6faf..7e5fe89 100644 --- a/src/job-execution/WebJobExecutor.ts +++ b/src/job-execution/WebJobExecutor.ts @@ -2,8 +2,7 @@ import { ServerType } from '@sasjs/utils/types' import { ErrorResponse, JobExecutionError, - LoginRequiredError, - WeboutResponseError + LoginRequiredError } from '../types/errors' import { generateFileUploadForm } from '../file/generateFileUploadForm' import { generateTableUploadForm } from '../file/generateTableUploadForm' @@ -16,6 +15,7 @@ import { } from '../utils' import { BaseJobExecutor } from './JobExecutor' import { parseWeboutResponse } from '../utils/parseWeboutResponse' +import { isLoggedInSASVIYA } from '../auth/verifyingPopUpLoginSASVIYA' export interface WaitingRequstPromise { promise: Promise | null @@ -40,6 +40,11 @@ export class WebJobExecutor extends BaseJobExecutor { loginRequiredCallback?: any ) { const loginCallback = loginRequiredCallback || (() => Promise.resolve()) + + if (this.serverType === ServerType.SasViya && !isLoggedInSASVIYA()) { + await loginCallback() + } + const program = isRelativePath(sasJob) ? config.appLoc ? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '') @@ -143,8 +148,6 @@ export class WebJobExecutor extends BaseJobExecutor { } if (e instanceof LoginRequiredError) { - await loginCallback() - this.appendWaitingRequest(() => { return this.execute( sasJob, @@ -160,6 +163,8 @@ export class WebJobExecutor extends BaseJobExecutor { } ) }) + + await loginCallback() } else { reject(new ErrorResponse(e?.message, e)) } From 389ef94cd508f1a6a10bd5eed95249964c90a73a Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Sat, 28 Aug 2021 10:01:20 +0500 Subject: [PATCH 04/16] feat(login): redirect mechanism - in page link to open popup --- src/auth/AuthManager.ts | 14 ++- src/auth/openWebPage.ts | 17 +++- src/utils/loginPrompt/index.ts | 166 ++++++++++++++++++++++++++++++++ src/utils/loginPrompt/style.css | 101 +++++++++++++++++++ 4 files changed, 291 insertions(+), 7 deletions(-) create mode 100644 src/utils/loginPrompt/index.ts create mode 100644 src/utils/loginPrompt/style.css diff --git a/src/auth/AuthManager.ts b/src/auth/AuthManager.ts index 0915650..c97f5ea 100644 --- a/src/auth/AuthManager.ts +++ b/src/auth/AuthManager.ts @@ -23,14 +23,18 @@ export class AuthManager { : '/SASLogon/logout.do?' } + /** + * Opens Pop up window to SAS Login screen. + * And checks if user has finished login process. + */ public async redirectedLogIn() { - const loginPopup = openWebPage(this.loginPreventRedirectUrl, 'SASLogon', { - width: 500, - height: 600 - }) + const loginPopup = await openWebPage( + this.loginPreventRedirectUrl, + 'SASLogon', + { width: 500, height: 600 } + ) if (!loginPopup) { - alert('Unable to open popup for login. Please try with other browser.') return { isLoggedIn: false } } diff --git a/src/auth/openWebPage.ts b/src/auth/openWebPage.ts index 75a3e10..79f3a4f 100644 --- a/src/auth/openWebPage.ts +++ b/src/auth/openWebPage.ts @@ -1,13 +1,15 @@ +import { openLoginPrompt } from '../utils/loginPrompt' + interface windowFeatures { width: number height: number } -export function openWebPage( +export async function openWebPage( url: string, windowName: string = '', { width, height }: windowFeatures -): Window | null { +): Promise { const left = screen.width / 2 - width / 2 const top = screen.height / 2 - height / 2 @@ -17,5 +19,16 @@ export function openWebPage( `toolbar=0,location=0,menubar=0,width=${width},height=${height},left=${left},top=${top}` ) + if (!loginPopup) { + const doLogin = await openLoginPrompt() + return doLogin + ? window.open( + url, + windowName, + `toolbar=0,location=0,menubar=0,width=${width},height=${height},left=${left},top=${top}` + ) + : null + } + return loginPopup } diff --git a/src/utils/loginPrompt/index.ts b/src/utils/loginPrompt/index.ts new file mode 100644 index 0000000..4924e9d --- /dev/null +++ b/src/utils/loginPrompt/index.ts @@ -0,0 +1,166 @@ +export const openLoginPrompt = (): Promise => { + return new Promise(async (resolve) => { + // const cssContent = await readFile(path.join(__dirname, 'style.css')) + const style = document.createElement('style') + style.id = 'stylesBySASjsAdapter' + style.innerText = cssContent + + const loginPromptBG = document.createElement('div') + loginPromptBG.id = 'loginPromptBG' + loginPromptBG.classList.add('popUpBG') + + const loginPrompt = document.createElement('div') + loginPrompt.id = 'loginPrompt' + 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 = () => { + let elem = document.querySelector('#stylesBySASjsAdapter') + elem?.parentNode?.removeChild(elem) + + elem = document.querySelector('#loginPrompt') + elem?.parentNode?.removeChild(elem) + + elem = document.querySelector('#loginPromptBG') + 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); +} +` diff --git a/src/utils/loginPrompt/style.css b/src/utils/loginPrompt/style.css new file mode 100644 index 0000000..ab6df54 --- /dev/null +++ b/src/utils/loginPrompt/style.css @@ -0,0 +1,101 @@ +.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); +} From f231edb4a6780988644f74db9483363c65a8d9f9 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Tue, 31 Aug 2021 12:36:20 +0500 Subject: [PATCH 05/16] chore: redirect login with onLoggedOut callback --- src/SASjs.ts | 9 +++++++-- src/auth/AuthManager.ts | 9 +++++++-- src/auth/openWebPage.ts | 8 +++++++- src/types/LoginOptions.ts | 3 +++ 4 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 src/types/LoginOptions.ts diff --git a/src/SASjs.ts b/src/SASjs.ts index 2c08f70..1458a69 100644 --- a/src/SASjs.ts +++ b/src/SASjs.ts @@ -25,6 +25,7 @@ import { Sas9JobExecutor } from './job-execution' import { ErrorResponse } from './types/errors' +import { LoginOptions } from './types/LoginOptions' const defaultConfig: SASjsConfig = { serverUrl: '', @@ -533,7 +534,11 @@ export default class SASjs { * @param username - a string representing the username. * @param password - a string representing the password. */ - public async logIn(username?: string, password?: string) { + public async logIn( + username?: string, + password?: string, + options: LoginOptions = {} + ) { if (this.sasjsConfig.loginMechanism === LoginMechanism.Default) { if (!username || !password) { throw new Error( @@ -549,7 +554,7 @@ export default class SASjs { ) } - return this.authManager!.redirectedLogIn() + return this.authManager!.redirectedLogIn(options) } /** diff --git a/src/auth/AuthManager.ts b/src/auth/AuthManager.ts index c97f5ea..ed2728c 100644 --- a/src/auth/AuthManager.ts +++ b/src/auth/AuthManager.ts @@ -1,5 +1,6 @@ import { ServerType } from '@sasjs/utils/types' import { RequestClient } from '../request/RequestClient' +import { LoginOptions } from '../types/LoginOptions' import { serialize } from '../utils' import { openWebPage } from './openWebPage' import { verifyingPopUpLoginSAS9 } from './verifyingPopUpLoginSAS9' @@ -27,11 +28,15 @@ export class AuthManager { * Opens Pop up window to SAS Login screen. * And checks if user has finished login process. */ - public async redirectedLogIn() { + public async redirectedLogIn({ onLoggedOut }: LoginOptions) { const loginPopup = await openWebPage( this.loginPreventRedirectUrl, 'SASLogon', - { width: 500, height: 600 } + { + width: 500, + height: 600 + }, + onLoggedOut ) if (!loginPopup) { diff --git a/src/auth/openWebPage.ts b/src/auth/openWebPage.ts index 79f3a4f..f50167d 100644 --- a/src/auth/openWebPage.ts +++ b/src/auth/openWebPage.ts @@ -8,7 +8,8 @@ interface windowFeatures { export async function openWebPage( url: string, windowName: string = '', - { width, height }: windowFeatures + { width, height }: windowFeatures, + onLoggedOut?: Function ): Promise { const left = screen.width / 2 - width / 2 const top = screen.height / 2 - height / 2 @@ -20,6 +21,11 @@ export async function openWebPage( ) if (!loginPopup) { + if (onLoggedOut) { + onLoggedOut() + return null + } + const doLogin = await openLoginPrompt() return doLogin ? window.open( diff --git a/src/types/LoginOptions.ts b/src/types/LoginOptions.ts new file mode 100644 index 0000000..e4d1809 --- /dev/null +++ b/src/types/LoginOptions.ts @@ -0,0 +1,3 @@ +export interface LoginOptions { + onLoggedOut?: Function +} From f40a86f0f6304f12005c05460ada3d6e39ca270d Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Thu, 2 Sep 2021 13:43:07 +0500 Subject: [PATCH 06/16] chore(redirectLogin): onLoggedOut callback should be an async --- src/auth/openWebPage.ts | 9 +-- src/types/LoginOptions.ts | 2 +- src/utils/loginPrompt/index.ts | 25 ++++---- src/utils/loginPrompt/style.css | 101 -------------------------------- 4 files changed, 17 insertions(+), 120 deletions(-) delete mode 100644 src/utils/loginPrompt/style.css diff --git a/src/auth/openWebPage.ts b/src/auth/openWebPage.ts index f50167d..d8cd0b0 100644 --- a/src/auth/openWebPage.ts +++ b/src/auth/openWebPage.ts @@ -9,7 +9,7 @@ export async function openWebPage( url: string, windowName: string = '', { width, height }: windowFeatures, - onLoggedOut?: Function + onLoggedOut?: () => Promise ): Promise { const left = screen.width / 2 - width / 2 const top = screen.height / 2 - height / 2 @@ -21,12 +21,9 @@ export async function openWebPage( ) if (!loginPopup) { - if (onLoggedOut) { - onLoggedOut() - return null - } + const getUserAction: () => Promise = onLoggedOut ?? openLoginPrompt - const doLogin = await openLoginPrompt() + const doLogin = await getUserAction() return doLogin ? window.open( url, diff --git a/src/types/LoginOptions.ts b/src/types/LoginOptions.ts index e4d1809..c8e9329 100644 --- a/src/types/LoginOptions.ts +++ b/src/types/LoginOptions.ts @@ -1,3 +1,3 @@ export interface LoginOptions { - onLoggedOut?: Function + onLoggedOut?: () => Promise } diff --git a/src/utils/loginPrompt/index.ts b/src/utils/loginPrompt/index.ts index 4924e9d..4ad8354 100644 --- a/src/utils/loginPrompt/index.ts +++ b/src/utils/loginPrompt/index.ts @@ -1,16 +1,21 @@ +enum domIDs { + styles = 'sasjsAdapterStyles', + overlay = 'sasjsAdapterLoginPromptBG', + dialog = 'sasjsAdapterLoginPrompt' +} + export const openLoginPrompt = (): Promise => { return new Promise(async (resolve) => { - // const cssContent = await readFile(path.join(__dirname, 'style.css')) const style = document.createElement('style') - style.id = 'stylesBySASjsAdapter' + style.id = domIDs.styles style.innerText = cssContent const loginPromptBG = document.createElement('div') - loginPromptBG.id = 'loginPromptBG' + loginPromptBG.id = domIDs.overlay loginPromptBG.classList.add('popUpBG') const loginPrompt = document.createElement('div') - loginPrompt.id = 'loginPrompt' + loginPrompt.id = domIDs.dialog loginPrompt.classList.add('popUp') const title = document.createElement('h1') @@ -49,14 +54,10 @@ export const openLoginPrompt = (): Promise => { }) } const closeLoginPrompt = () => { - let elem = document.querySelector('#stylesBySASjsAdapter') - elem?.parentNode?.removeChild(elem) - - elem = document.querySelector('#loginPrompt') - elem?.parentNode?.removeChild(elem) - - elem = document.querySelector('#loginPromptBG') - elem?.parentNode?.removeChild(elem) + Object.keys(domIDs).forEach((id) => { + const elem = document.getElementById(id) + elem?.parentNode?.removeChild(elem) + }) document.body.style.overflow = 'auto' } diff --git a/src/utils/loginPrompt/style.css b/src/utils/loginPrompt/style.css deleted file mode 100644 index ab6df54..0000000 --- a/src/utils/loginPrompt/style.css +++ /dev/null @@ -1,101 +0,0 @@ -.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); -} From a1f5355d6a14d11e20c63cc6540c6f268dd9fb05 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Tue, 7 Sep 2021 05:26:42 +0500 Subject: [PATCH 07/16] chore: fetch username for Redirected-Login and return --- src/SASjs.ts | 4 +-- src/auth/AuthManager.ts | 70 +++++++++++++++++++++------------------ src/types/Login.ts | 8 +++++ src/types/LoginOptions.ts | 3 -- 4 files changed, 48 insertions(+), 37 deletions(-) create mode 100644 src/types/Login.ts delete mode 100644 src/types/LoginOptions.ts diff --git a/src/SASjs.ts b/src/SASjs.ts index 1458a69..9323fa5 100644 --- a/src/SASjs.ts +++ b/src/SASjs.ts @@ -25,7 +25,7 @@ import { Sas9JobExecutor } from './job-execution' import { ErrorResponse } from './types/errors' -import { LoginOptions } from './types/LoginOptions' +import { LoginOptions, LoginReturn } from './types/Login' const defaultConfig: SASjsConfig = { serverUrl: '', @@ -538,7 +538,7 @@ export default class SASjs { username?: string, password?: string, options: LoginOptions = {} - ) { + ): Promise { if (this.sasjsConfig.loginMechanism === LoginMechanism.Default) { if (!username || !password) { throw new Error( diff --git a/src/auth/AuthManager.ts b/src/auth/AuthManager.ts index 914cdbb..441a455 100644 --- a/src/auth/AuthManager.ts +++ b/src/auth/AuthManager.ts @@ -1,6 +1,6 @@ import { ServerType } from '@sasjs/utils/types' import { RequestClient } from '../request/RequestClient' -import { LoginOptions } from '../types/LoginOptions' +import { LoginOptions, LoginReturn } from '../types/Login' import { serialize } from '../utils' import { openWebPage } from './openWebPage' import { verifyingPopUpLoginSAS9 } from './verifyingPopUpLoginSAS9' @@ -28,7 +28,9 @@ export class AuthManager { * Opens Pop up window to SAS Login screen. * And checks if user has finished login process. */ - public async redirectedLogIn({ onLoggedOut }: LoginOptions) { + public async redirectedLogIn({ + onLoggedOut + }: LoginOptions): Promise { const loginPopup = await openWebPage( this.loginPreventRedirectUrl, 'SASLogon', @@ -40,7 +42,7 @@ export class AuthManager { ) if (!loginPopup) { - return { isLoggedIn: false } + return { isLoggedIn: false, userName: '' } } const { isLoggedIn } = @@ -60,10 +62,14 @@ export class AuthManager { ) } + const { userName } = await this.fetchUserName() + await this.loginCallback() + + return { isLoggedIn: true, userName } } - return { isLoggedIn } + return { isLoggedIn: false, userName: '' } } /** @@ -72,13 +78,7 @@ export class AuthManager { * @param password - a string representing the password. * @returns - a boolean `isLoggedin` and a string `username` */ - public async logIn( - username: string, - password: string - ): Promise<{ - isLoggedIn: boolean - userName: string - }> { + public async logIn(username: string, password: string): Promise { const loginParams = { _service: 'default', username, @@ -119,7 +119,7 @@ export class AuthManager { const res = await this.checkSession() isLoggedIn = res.isLoggedIn - if (isLoggedIn) this.userName = res.userName! + if (isLoggedIn) this.userName = res.userName } else { this.userName = loginParams.username } @@ -175,27 +175,10 @@ export class AuthManager { */ public async checkSession(): Promise<{ isLoggedIn: boolean - userName?: string + userName: string loginForm?: any }> { - //For VIYA we will send request on API endpoint. Which is faster then pinging SASJobExecution. - //For SAS9 we will send request on SASStoredProcess - const url = - this.serverType === 'SASVIYA' - ? `${this.serverUrl}/identities/users/@currentUser` - : `${this.serverUrl}/SASStoredProcess` - - const { result: loginResponse } = await this.requestClient - .get(url, undefined, 'text/plain') - .catch((err: any) => { - return { result: 'authErr' } - }) - - const isLoggedIn = loginResponse !== 'authErr' - const userName = isLoggedIn - ? this.extractUserName(loginResponse) - : undefined - + const { isLoggedIn, userName } = await this.fetchUserName() let loginForm = null if (!isLoggedIn) { @@ -214,11 +197,34 @@ export class AuthManager { return Promise.resolve({ isLoggedIn, - userName: userName?.toLowerCase(), + userName: userName.toLowerCase(), loginForm }) } + private async fetchUserName(): Promise<{ + isLoggedIn: boolean + userName: string + }> { + //For VIYA we will send request on API endpoint. Which is faster then pinging SASJobExecution. + //For SAS9 we will send request on SASStoredProcess + const url = + this.serverType === 'SASVIYA' + ? `${this.serverUrl}/identities/users/@currentUser` + : `${this.serverUrl}/SASStoredProcess` + + const { result: loginResponse } = await this.requestClient + .get(url, undefined, 'text/plain') + .catch((err: any) => { + return { result: 'authErr' } + }) + + const isLoggedIn = loginResponse !== 'authErr' + const userName = isLoggedIn ? this.extractUserName(loginResponse) : '' + + return { isLoggedIn, userName } + } + private extractUserName = (response: any): string => { switch (this.serverType) { case ServerType.SasViya: diff --git a/src/types/Login.ts b/src/types/Login.ts new file mode 100644 index 0000000..60d21cb --- /dev/null +++ b/src/types/Login.ts @@ -0,0 +1,8 @@ +export interface LoginOptions { + onLoggedOut?: () => Promise +} + +export interface LoginReturn { + isLoggedIn: boolean + userName: string +} diff --git a/src/types/LoginOptions.ts b/src/types/LoginOptions.ts deleted file mode 100644 index c8e9329..0000000 --- a/src/types/LoginOptions.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface LoginOptions { - onLoggedOut?: () => Promise -} From cd2b32f2f43a4fe110f92141b97afd1de7e32474 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Tue, 7 Sep 2021 06:08:17 +0500 Subject: [PATCH 08/16] test(checkSession): extract username from server response --- src/auth/spec/AuthManager.spec.ts | 5 ++++- src/auth/spec/mockResponses.ts | 25 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/auth/spec/AuthManager.spec.ts b/src/auth/spec/AuthManager.spec.ts index 3d730c4..5b2e5fc 100644 --- a/src/auth/spec/AuthManager.spec.ts +++ b/src/auth/spec/AuthManager.spec.ts @@ -3,6 +3,7 @@ import * as dotenv from 'dotenv' import { ServerType } from '@sasjs/utils/types' import axios from 'axios' import { + mockedCurrentUserApi, mockLoginAuthoriseRequiredResponse, mockLoginSuccessResponse } from './mockResponses' @@ -89,6 +90,7 @@ describe('AuthManager', () => { jest.spyOn(authManager, 'checkSession').mockImplementation(() => Promise.resolve({ isLoggedIn: false, + userName: '', loginForm: { name: 'test' } }) ) @@ -167,11 +169,12 @@ describe('AuthManager', () => { authCallback ) mockedAxios.get.mockImplementation(() => - Promise.resolve({ data: '