From 053b07769a2a1918dd24d8350828bdc0b590c38b Mon Sep 17 00:00:00 2001 From: Yury Date: Thu, 20 Jun 2024 17:15:05 +0300 Subject: [PATCH 1/4] feat(auth): added multi-language support to logIn method --- src/auth/AuthManager.ts | 95 ++++++++++++++++++++++-- src/auth/spec/AuthManager.spec.ts | 60 +++++++++++++++ src/auth/spec/mockResponses.ts | 3 +- src/auth/spec/verifySas9Login.spec.ts | 3 +- src/auth/spec/verifySasViyaLogin.spec.ts | 3 +- src/auth/verifySas9Login.ts | 3 +- src/auth/verifySasViyaLogin.ts | 5 +- src/utils/getUserLanguage.ts | 10 +++ src/utils/index.ts | 1 + 9 files changed, 169 insertions(+), 14 deletions(-) create mode 100644 src/utils/getUserLanguage.ts diff --git a/src/auth/AuthManager.ts b/src/auth/AuthManager.ts index 0aa3b639..62f07eb4 100644 --- a/src/auth/AuthManager.ts +++ b/src/auth/AuthManager.ts @@ -2,7 +2,7 @@ import { ServerType } from '@sasjs/utils/types' import { RequestClient } from '../request/RequestClient' import { NotFoundError } from '../types/errors' import { LoginOptions, LoginResult, LoginResultInternal } from '../types/Login' -import { serialize } from '../utils' +import { serialize, getUserLanguage } from '../utils' import { extractUserLongNameSas9 } from '../utils/sas9/extractUserLongNameSas9' import { openWebPage } from './openWebPage' import { verifySas9Login } from './verifySas9Login' @@ -14,6 +14,44 @@ export class AuthManager { private loginUrl: string private logoutUrl: string private redirectedLoginUrl = `/SASLogon` //SAS 9 M8 no longer redirects from `/SASLogon/home` to the login page. `/SASLogon` seems to be stable enough across SAS versions + private defaultSuccessHeaderKey = 'default' + private successHeaders: { [key: string]: string } = { + es: `Ya se ha iniciado la sesi\u00f3n.`, + th: `\u0e04\u0e38\u0e13\u0e25\u0e07\u0e0a\u0e37\u0e48\u0e2d\u0e40\u0e02\u0e49\u0e32\u0e43\u0e0a\u0e49\u0e41\u0e25\u0e49\u0e27`, + ja: `\u30b5\u30a4\u30f3\u30a4\u30f3\u3057\u307e\u3057\u305f\u3002`, + nb: `Du har logget deg p\u00e5.`, + sl: `Prijavili ste se.`, + ar: `\u0644\u0642\u062f \u0642\u0645\u062a `, + sk: `Prihl\u00e1sili ste sa.`, + zh_HK: `\u60a8\u5df2\u767b\u5165\u3002`, + zh_CN: `\u60a8\u5df2\u767b\u5f55\u3002`, + it: `L'utente si \u00e8 connesso.`, + sv: `Du har loggat in.`, + he: `\u05e0\u05db\u05e0\u05e1\u05ea `, + nl: `U hebt zich aangemeld.`, + pl: `Zosta\u0142e\u015b zalogowany.`, + ko: `\ub85c\uadf8\uc778\ud588\uc2b5\ub2c8\ub2e4.`, + zh_TW: `\u60a8\u5df2\u767b\u5165\u3002`, + tr: `Oturum a\u00e7t\u0131n\u0131z.`, + iw: `\u05e0\u05db\u05e0\u05e1\u05ea `, + fr: `Vous \u00eates connect\u00e9.`, + uk: `\u0412\u0438 \u0432\u0432\u0456\u0439\u0448\u043b\u0438 \u0432 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441.`, + pt_BR: `Voc\u00ea se conectou.`, + no: `Du har logget deg p\u00e5.`, + cs: `Jste p\u0159ihl\u00e1\u0161eni.`, + fi: `Olet kirjautunut sis\u00e4\u00e4n.`, + ru: `\u0412\u044b \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u043b\u0438 \u0432\u0445\u043e\u0434 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443.`, + el: `\u0388\u03c7\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af.`, + hr: `Prijavili ste se.`, + da: `Du er logget p\u00e5.`, + de: `Sie sind jetzt angemeldet.`, + sh: `Prijavljeni ste.`, + pt: `Iniciou sess\u00e3o.`, + hu: `Bejelentkezett.`, + sr: `Prijavljeni ste.`, + en: enLoginSuccessHeader, + [this.defaultSuccessHeaderKey]: enLoginSuccessHeader + } constructor( private serverUrl: string, @@ -132,7 +170,10 @@ export class AuthManager { let loginResponse = await this.sendLoginRequest(loginForm, loginParams) - let isLoggedIn = isLogInSuccess(this.serverType, loginResponse) + let isLoggedIn = this.isLogInSuccessHeaderPresent( + this.serverType, + loginResponse + ) if (!isLoggedIn) { if (isCredentialsVerifyError(loginResponse)) { @@ -166,6 +207,50 @@ export class AuthManager { } } + /** + * Checks if Login success header is present in the response based on language settings of the browser + * @param serverType - server type + * @param response - response object + * @returns - return boolean indicating if Login success header is present + */ + private isLogInSuccessHeaderPresent( + serverType: ServerType, + response: any + ): boolean { + if (serverType === ServerType.Sasjs) return response?.loggedin + + // get default success header + let successHeader = this.successHeaders[this.defaultSuccessHeaderKey] + + // get user language based on language settings of the browser + const userLang = getUserLanguage() + + if (userLang) { + // get success header on exact match of the language code + let userLangSuccessHeader = this.successHeaders[userLang] + + // handle case when there is no exact match of the language code + if (!userLangSuccessHeader) { + // get all supported language codes + const headerLanguages = Object.keys(this.successHeaders) + + // find language code on partial match + const headerLanguage = headerLanguages.find((language) => + new RegExp(language, 'i').test(userLang) + ) + + // reassign success header if partial match was found + if (headerLanguage) { + successHeader = this.successHeaders[headerLanguage] + } + } else { + successHeader = userLangSuccessHeader + } + } + + return new RegExp(successHeader, 'gm').test(response) + } + private async performCASSecurityCheck() { const casAuthenticationUrl = `${this.serverUrl}/SASStoredProcess/j_spring_cas_security_check` @@ -385,8 +470,4 @@ const isCredentialsVerifyError = (response: string): boolean => response ) -const isLogInSuccess = (serverType: ServerType, response: any): boolean => { - if (serverType === ServerType.Sasjs) return response?.loggedin - - return /You have signed in/gm.test(response) -} +export const enLoginSuccessHeader = 'You have signed in.' diff --git a/src/auth/spec/AuthManager.spec.ts b/src/auth/spec/AuthManager.spec.ts index 198893a3..e9dacbbd 100644 --- a/src/auth/spec/AuthManager.spec.ts +++ b/src/auth/spec/AuthManager.spec.ts @@ -1,3 +1,7 @@ +/** + * @jest-environment jsdom + */ + import { AuthManager } from '../AuthManager' import * as dotenv from 'dotenv' import { ServerType } from '@sasjs/utils/types' @@ -63,6 +67,12 @@ describe('AuthManager', () => { }) describe('login - default mechanism', () => { + let languageGetter: any + + beforeEach(() => { + languageGetter = jest.spyOn(window.navigator, 'language', 'get') + }) + it('should call the auth callback and return when already logged in', async () => { const authManager = new AuthManager( serverUrl, @@ -294,6 +304,56 @@ describe('AuthManager', () => { mockLoginAuthoriseRequiredResponse ) }) + + it('should check login success header based on language preferences of the browser', () => { + const authManager = new AuthManager( + serverUrl, + serverType, + requestClient, + authCallback + ) + + // test built in language codes + Object.keys(authManager['successHeaders']).forEach((key) => { + languageGetter.mockReturnValue(key) + + expect( + authManager['isLogInSuccessHeaderPresent']( + serverType, + authManager['successHeaders'][key] + ) + ).toBeTruthy() + }) + + // test possible longer language codes + const possibleLanguageCodes = [ + { short: 'en', long: 'en-US' }, + { short: 'fr', long: 'fr-FR' }, + { short: 'es', long: 'es-ES' } + ] + + possibleLanguageCodes.forEach((key) => { + const { short, long } = key + languageGetter.mockReturnValue(long) + + expect( + authManager['isLogInSuccessHeaderPresent']( + serverType, + authManager['successHeaders'][short] + ) + ).toBeTruthy() + }) + + // test falling back to default language code + languageGetter.mockReturnValue('WRONG-LANGUAGE') + + expect( + authManager['isLogInSuccessHeaderPresent']( + serverType, + authManager['successHeaders'][authManager['defaultSuccessHeaderKey']] + ) + ).toBeTruthy() + }) }) describe('login - redirect mechanism', () => { diff --git a/src/auth/spec/mockResponses.ts b/src/auth/spec/mockResponses.ts index 4aaad1d4..c30aa8f9 100644 --- a/src/auth/spec/mockResponses.ts +++ b/src/auth/spec/mockResponses.ts @@ -1,7 +1,8 @@ import { SasAuthResponse } from '@sasjs/utils/types' +import { enLoginSuccessHeader } from '../AuthManager' export const mockLoginAuthoriseRequiredResponse = `
` -export const mockLoginSuccessResponse = `You have signed in` +export const mockLoginSuccessResponse = enLoginSuccessHeader export const mockAuthResponse: SasAuthResponse = { access_token: 'acc355', diff --git a/src/auth/spec/verifySas9Login.spec.ts b/src/auth/spec/verifySas9Login.spec.ts index c71f595b..19696d40 100644 --- a/src/auth/spec/verifySas9Login.spec.ts +++ b/src/auth/spec/verifySas9Login.spec.ts @@ -3,6 +3,7 @@ */ import { verifySas9Login } from '../verifySas9Login' import * as delayModule from '../../utils/delay' +import { enLoginSuccessHeader } from '../AuthManager' describe('verifySas9Login', () => { const serverUrl = 'http://test-server.com' @@ -18,7 +19,7 @@ describe('verifySas9Login', () => { const popup = { window: { location: { href: serverUrl + `/SASLogon` }, - document: { body: { innerText: '

You have signed in.

' } } + document: { body: { innerText: `

${enLoginSuccessHeader}

` } } } } as unknown as Window diff --git a/src/auth/spec/verifySasViyaLogin.spec.ts b/src/auth/spec/verifySasViyaLogin.spec.ts index dc99e321..5183e07d 100644 --- a/src/auth/spec/verifySasViyaLogin.spec.ts +++ b/src/auth/spec/verifySasViyaLogin.spec.ts @@ -3,6 +3,7 @@ */ import { verifySasViyaLogin } from '../verifySasViyaLogin' import * as delayModule from '../../utils/delay' +import { enLoginSuccessHeader } from '../AuthManager' describe('verifySasViyaLogin', () => { const serverUrl = 'http://test-server.com' @@ -19,7 +20,7 @@ describe('verifySasViyaLogin', () => { const popup = { window: { location: { href: serverUrl + `/SASLogon` }, - document: { body: { innerText: '

You have signed in.

' } } + document: { body: { innerText: `

${enLoginSuccessHeader}

` } } } } as unknown as Window diff --git a/src/auth/verifySas9Login.ts b/src/auth/verifySas9Login.ts index cf350f78..831117e6 100644 --- a/src/auth/verifySas9Login.ts +++ b/src/auth/verifySas9Login.ts @@ -1,4 +1,5 @@ import { delay } from '../utils' +import { enLoginSuccessHeader } from './AuthManager' export async function verifySas9Login(loginPopup: Window): Promise<{ isLoggedIn: boolean @@ -12,7 +13,7 @@ export async function verifySas9Login(loginPopup: Window): Promise<{ isLoggedIn = loginPopup.window.location.href.includes('SASLogon') && - loginPopup.window.document.body.innerText.includes('You have signed in.') + loginPopup.window.document.body.innerText.includes(enLoginSuccessHeader) elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000 } while (!isLoggedIn && elapsedSeconds < 5 * 60) diff --git a/src/auth/verifySasViyaLogin.ts b/src/auth/verifySasViyaLogin.ts index b65cef09..91e37442 100644 --- a/src/auth/verifySasViyaLogin.ts +++ b/src/auth/verifySasViyaLogin.ts @@ -1,4 +1,5 @@ import { delay } from '../utils' +import { enLoginSuccessHeader } from './AuthManager' export async function verifySasViyaLogin(loginPopup: Window): Promise<{ isLoggedIn: boolean @@ -20,9 +21,7 @@ export async function verifySasViyaLogin(loginPopup: Window): Promise<{ if (loginPopup.closed) break isAuthorized = loginPopup.window.location.href.includes('SASLogon') || - loginPopup.window.document.body?.innerText?.includes( - 'You have signed in.' - ) + loginPopup.window.document.body?.innerText?.includes(enLoginSuccessHeader) elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000 } while (!isAuthorized && elapsedSeconds < 5 * 60) diff --git a/src/utils/getUserLanguage.ts b/src/utils/getUserLanguage.ts new file mode 100644 index 00000000..1a0be1cf --- /dev/null +++ b/src/utils/getUserLanguage.ts @@ -0,0 +1,10 @@ +interface IEnavigator { + userLanguage?: string +} + +/** + * Provides preferred language of the user. + * @returns A string representing the preferred language of the user, usually the language of the browser UI. Examples of valid language codes include "en", "en-US", "fr", "fr-FR", "es-ES". More info available https://datatracker.ietf.org/doc/html/rfc5646 + */ +export const getUserLanguage = () => + window.navigator.language || (window.navigator as IEnavigator).userLanguage diff --git a/src/utils/index.ts b/src/utils/index.ts index cbc166ac..2a2b4059 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -20,3 +20,4 @@ export * from './serialize' export * from './splitChunks' export * from './validateInput' export * from './getFormData' +export * from './getUserLanguage' From 2cca192f88e19fb5d5d5d92bbcfdb3038451b94e Mon Sep 17 00:00:00 2001 From: Yury Date: Thu, 20 Jun 2024 17:38:56 +0300 Subject: [PATCH 2/4] chore(auth-manager): added comment --- src/auth/AuthManager.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/auth/AuthManager.ts b/src/auth/AuthManager.ts index 62f07eb4..b28b0e63 100644 --- a/src/auth/AuthManager.ts +++ b/src/auth/AuthManager.ts @@ -15,7 +15,9 @@ export class AuthManager { private logoutUrl: string private redirectedLoginUrl = `/SASLogon` //SAS 9 M8 no longer redirects from `/SASLogon/home` to the login page. `/SASLogon` seems to be stable enough across SAS versions private defaultSuccessHeaderKey = 'default' - private successHeaders: { [key: string]: string } = { + + // The following headers provided by https://github.com/sasjs/adapter/issues/835#issuecomment-2177818601 + private loginSuccessHeaders: { [key: string]: string } = { es: `Ya se ha iniciado la sesi\u00f3n.`, th: `\u0e04\u0e38\u0e13\u0e25\u0e07\u0e0a\u0e37\u0e48\u0e2d\u0e40\u0e02\u0e49\u0e32\u0e43\u0e0a\u0e49\u0e41\u0e25\u0e49\u0e27`, ja: `\u30b5\u30a4\u30f3\u30a4\u30f3\u3057\u307e\u3057\u305f\u3002`, @@ -220,19 +222,19 @@ export class AuthManager { if (serverType === ServerType.Sasjs) return response?.loggedin // get default success header - let successHeader = this.successHeaders[this.defaultSuccessHeaderKey] + let successHeader = this.loginSuccessHeaders[this.defaultSuccessHeaderKey] // get user language based on language settings of the browser const userLang = getUserLanguage() if (userLang) { // get success header on exact match of the language code - let userLangSuccessHeader = this.successHeaders[userLang] + let userLangSuccessHeader = this.loginSuccessHeaders[userLang] // handle case when there is no exact match of the language code if (!userLangSuccessHeader) { // get all supported language codes - const headerLanguages = Object.keys(this.successHeaders) + const headerLanguages = Object.keys(this.loginSuccessHeaders) // find language code on partial match const headerLanguage = headerLanguages.find((language) => @@ -241,7 +243,7 @@ export class AuthManager { // reassign success header if partial match was found if (headerLanguage) { - successHeader = this.successHeaders[headerLanguage] + successHeader = this.loginSuccessHeaders[headerLanguage] } } else { successHeader = userLangSuccessHeader From a90f699abd516af40f2f054bbb879d54542d0843 Mon Sep 17 00:00:00 2001 From: Yury Date: Fri, 21 Jun 2024 11:24:06 +0300 Subject: [PATCH 3/4] feat(auth): added utils to get and check login header --- .vscode/settings.json | 3 + src/auth/AuthManager.ts | 96 +---------------------- src/auth/index.ts | 1 + src/auth/loginHeader.ts | 97 ++++++++++++++++++++++++ src/auth/spec/AuthManager.spec.ts | 63 +++------------ src/auth/spec/loginHeader.spec.ts | 82 ++++++++++++++++++++ src/auth/spec/mockResponses.ts | 2 - src/auth/spec/verifySas9Login.spec.ts | 6 +- src/auth/spec/verifySasViyaLogin.spec.ts | 6 +- src/auth/verifySas9Login.ts | 8 +- src/auth/verifySasViyaLogin.ts | 15 +++- 11 files changed, 223 insertions(+), 156 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/auth/loginHeader.ts create mode 100644 src/auth/spec/loginHeader.spec.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..12771f60 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cSpell.words": ["SASVIYA"] +} diff --git a/src/auth/AuthManager.ts b/src/auth/AuthManager.ts index b28b0e63..1c108e6f 100644 --- a/src/auth/AuthManager.ts +++ b/src/auth/AuthManager.ts @@ -2,11 +2,12 @@ import { ServerType } from '@sasjs/utils/types' import { RequestClient } from '../request/RequestClient' import { NotFoundError } from '../types/errors' import { LoginOptions, LoginResult, LoginResultInternal } from '../types/Login' -import { serialize, getUserLanguage } from '../utils' +import { serialize } from '../utils' import { extractUserLongNameSas9 } from '../utils/sas9/extractUserLongNameSas9' import { openWebPage } from './openWebPage' import { verifySas9Login } from './verifySas9Login' import { verifySasViyaLogin } from './verifySasViyaLogin' +import { isLogInSuccessHeaderPresent } from './' export class AuthManager { public userName = '' @@ -14,46 +15,6 @@ export class AuthManager { private loginUrl: string private logoutUrl: string private redirectedLoginUrl = `/SASLogon` //SAS 9 M8 no longer redirects from `/SASLogon/home` to the login page. `/SASLogon` seems to be stable enough across SAS versions - private defaultSuccessHeaderKey = 'default' - - // The following headers provided by https://github.com/sasjs/adapter/issues/835#issuecomment-2177818601 - private loginSuccessHeaders: { [key: string]: string } = { - es: `Ya se ha iniciado la sesi\u00f3n.`, - th: `\u0e04\u0e38\u0e13\u0e25\u0e07\u0e0a\u0e37\u0e48\u0e2d\u0e40\u0e02\u0e49\u0e32\u0e43\u0e0a\u0e49\u0e41\u0e25\u0e49\u0e27`, - ja: `\u30b5\u30a4\u30f3\u30a4\u30f3\u3057\u307e\u3057\u305f\u3002`, - nb: `Du har logget deg p\u00e5.`, - sl: `Prijavili ste se.`, - ar: `\u0644\u0642\u062f \u0642\u0645\u062a `, - sk: `Prihl\u00e1sili ste sa.`, - zh_HK: `\u60a8\u5df2\u767b\u5165\u3002`, - zh_CN: `\u60a8\u5df2\u767b\u5f55\u3002`, - it: `L'utente si \u00e8 connesso.`, - sv: `Du har loggat in.`, - he: `\u05e0\u05db\u05e0\u05e1\u05ea `, - nl: `U hebt zich aangemeld.`, - pl: `Zosta\u0142e\u015b zalogowany.`, - ko: `\ub85c\uadf8\uc778\ud588\uc2b5\ub2c8\ub2e4.`, - zh_TW: `\u60a8\u5df2\u767b\u5165\u3002`, - tr: `Oturum a\u00e7t\u0131n\u0131z.`, - iw: `\u05e0\u05db\u05e0\u05e1\u05ea `, - fr: `Vous \u00eates connect\u00e9.`, - uk: `\u0412\u0438 \u0432\u0432\u0456\u0439\u0448\u043b\u0438 \u0432 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441.`, - pt_BR: `Voc\u00ea se conectou.`, - no: `Du har logget deg p\u00e5.`, - cs: `Jste p\u0159ihl\u00e1\u0161eni.`, - fi: `Olet kirjautunut sis\u00e4\u00e4n.`, - ru: `\u0412\u044b \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u043b\u0438 \u0432\u0445\u043e\u0434 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443.`, - el: `\u0388\u03c7\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af.`, - hr: `Prijavili ste se.`, - da: `Du er logget p\u00e5.`, - de: `Sie sind jetzt angemeldet.`, - sh: `Prijavljeni ste.`, - pt: `Iniciou sess\u00e3o.`, - hu: `Bejelentkezett.`, - sr: `Prijavljeni ste.`, - en: enLoginSuccessHeader, - [this.defaultSuccessHeaderKey]: enLoginSuccessHeader - } constructor( private serverUrl: string, @@ -172,10 +133,7 @@ export class AuthManager { let loginResponse = await this.sendLoginRequest(loginForm, loginParams) - let isLoggedIn = this.isLogInSuccessHeaderPresent( - this.serverType, - loginResponse - ) + let isLoggedIn = isLogInSuccessHeaderPresent(this.serverType, loginResponse) if (!isLoggedIn) { if (isCredentialsVerifyError(loginResponse)) { @@ -209,50 +167,6 @@ export class AuthManager { } } - /** - * Checks if Login success header is present in the response based on language settings of the browser - * @param serverType - server type - * @param response - response object - * @returns - return boolean indicating if Login success header is present - */ - private isLogInSuccessHeaderPresent( - serverType: ServerType, - response: any - ): boolean { - if (serverType === ServerType.Sasjs) return response?.loggedin - - // get default success header - let successHeader = this.loginSuccessHeaders[this.defaultSuccessHeaderKey] - - // get user language based on language settings of the browser - const userLang = getUserLanguage() - - if (userLang) { - // get success header on exact match of the language code - let userLangSuccessHeader = this.loginSuccessHeaders[userLang] - - // handle case when there is no exact match of the language code - if (!userLangSuccessHeader) { - // get all supported language codes - const headerLanguages = Object.keys(this.loginSuccessHeaders) - - // find language code on partial match - const headerLanguage = headerLanguages.find((language) => - new RegExp(language, 'i').test(userLang) - ) - - // reassign success header if partial match was found - if (headerLanguage) { - successHeader = this.loginSuccessHeaders[headerLanguage] - } - } else { - successHeader = userLangSuccessHeader - } - } - - return new RegExp(successHeader, 'gm').test(response) - } - private async performCASSecurityCheck() { const casAuthenticationUrl = `${this.serverUrl}/SASStoredProcess/j_spring_cas_security_check` @@ -304,7 +218,7 @@ export class AuthManager { * - a boolean `isLoggedIn` * - a string `userName`, * - a string `userFullName` and - * - a form `loginForm` if not loggedin. + * - a form `loginForm` if not loggedIn. */ public async checkSession(): Promise { const { isLoggedIn, userName, userLongName } = await this.fetchUserName() @@ -471,5 +385,3 @@ const isCredentialsVerifyError = (response: string): boolean => /An error occurred while the system was verifying your credentials. Please enter your credentials again./gm.test( response ) - -export const enLoginSuccessHeader = 'You have signed in.' diff --git a/src/auth/index.ts b/src/auth/index.ts index b2d09d0d..4e472bea 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -1,3 +1,4 @@ export * from './AuthManager' export * from './isAuthorizeFormRequired' export * from './isLoginRequired' +export * from './loginHeader' diff --git a/src/auth/loginHeader.ts b/src/auth/loginHeader.ts new file mode 100644 index 00000000..6e872422 --- /dev/null +++ b/src/auth/loginHeader.ts @@ -0,0 +1,97 @@ +import { ServerType } from '@sasjs/utils/types' +import { getUserLanguage } from '../utils' + +const enLoginSuccessHeader = 'You have signed in.' + +export const defaultSuccessHeaderKey = 'default' + +// The following headers provided by https://github.com/sasjs/adapter/issues/835#issuecomment-2177818601 +export const loginSuccessHeaders: { [key: string]: string } = { + es: `Ya se ha iniciado la sesi\u00f3n.`, + th: `\u0e04\u0e38\u0e13\u0e25\u0e07\u0e0a\u0e37\u0e48\u0e2d\u0e40\u0e02\u0e49\u0e32\u0e43\u0e0a\u0e49\u0e41\u0e25\u0e49\u0e27`, + ja: `\u30b5\u30a4\u30f3\u30a4\u30f3\u3057\u307e\u3057\u305f\u3002`, + nb: `Du har logget deg p\u00e5.`, + sl: `Prijavili ste se.`, + ar: `\u0644\u0642\u062f \u0642\u0645\u062a `, + sk: `Prihl\u00e1sili ste sa.`, + zh_HK: `\u60a8\u5df2\u767b\u5165\u3002`, + zh_CN: `\u60a8\u5df2\u767b\u5f55\u3002`, + it: `L'utente si \u00e8 connesso.`, + sv: `Du har loggat in.`, + he: `\u05e0\u05db\u05e0\u05e1\u05ea `, + nl: `U hebt zich aangemeld.`, + pl: `Zosta\u0142e\u015b zalogowany.`, + ko: `\ub85c\uadf8\uc778\ud588\uc2b5\ub2c8\ub2e4.`, + zh_TW: `\u60a8\u5df2\u767b\u5165\u3002`, + tr: `Oturum a\u00e7t\u0131n\u0131z.`, + iw: `\u05e0\u05db\u05e0\u05e1\u05ea `, + fr: `Vous \u00eates connect\u00e9.`, + uk: `\u0412\u0438 \u0432\u0432\u0456\u0439\u0448\u043b\u0438 \u0432 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441.`, + pt_BR: `Voc\u00ea se conectou.`, + no: `Du har logget deg p\u00e5.`, + cs: `Jste p\u0159ihl\u00e1\u0161eni.`, + fi: `Olet kirjautunut sis\u00e4\u00e4n.`, + ru: `\u0412\u044b \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u043b\u0438 \u0432\u0445\u043e\u0434 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443.`, + el: `\u0388\u03c7\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af.`, + hr: `Prijavili ste se.`, + da: `Du er logget p\u00e5.`, + de: `Sie sind jetzt angemeldet.`, + sh: `Prijavljeni ste.`, + pt: `Iniciou sess\u00e3o.`, + hu: `Bejelentkezett.`, + sr: `Prijavljeni ste.`, + en: enLoginSuccessHeader, + [defaultSuccessHeaderKey]: enLoginSuccessHeader +} + +/** + * Provides expected login header based on language settings of the browser. + * @returns - expected header as a string. + */ +export const getExpectedLogInSuccessHeader = (): string => { + // get default success header + let successHeader = loginSuccessHeaders[defaultSuccessHeaderKey] + + // get user language based on language settings of the browser + const userLang = getUserLanguage() + + if (userLang) { + // get success header on exact match of the language code + let userLangSuccessHeader = loginSuccessHeaders[userLang] + + // handle case when there is no exact match of the language code + if (!userLangSuccessHeader) { + // get all supported language codes + const headerLanguages = Object.keys(loginSuccessHeaders) + + // find language code on partial match + const headerLanguage = headerLanguages.find((language) => + new RegExp(language, 'i').test(userLang) + ) + + // reassign success header if partial match was found + if (headerLanguage) { + successHeader = loginSuccessHeaders[headerLanguage] + } + } else { + successHeader = userLangSuccessHeader + } + } + + return successHeader +} + +/** + * Checks if Login success header is present in the response based on language settings of the browser. + * @param serverType - server type. + * @param response - response object. + * @returns - boolean indicating if Login success header is present. + */ +export const isLogInSuccessHeaderPresent = ( + serverType: ServerType, + response: any +): boolean => { + if (serverType === ServerType.Sasjs) return response?.loggedIn + + return new RegExp(getExpectedLogInSuccessHeader(), 'gm').test(response) +} diff --git a/src/auth/spec/AuthManager.spec.ts b/src/auth/spec/AuthManager.spec.ts index e9dacbbd..01e5be00 100644 --- a/src/auth/spec/AuthManager.spec.ts +++ b/src/auth/spec/AuthManager.spec.ts @@ -8,14 +8,15 @@ import { ServerType } from '@sasjs/utils/types' import axios from 'axios' import { mockedCurrentUserApi, - mockLoginAuthoriseRequiredResponse, - mockLoginSuccessResponse + mockLoginAuthoriseRequiredResponse } from './mockResponses' import { serialize } from '../../utils' import * as openWebPageModule from '../openWebPage' import * as verifySasViyaLoginModule from '../verifySasViyaLogin' import * as verifySas9LoginModule from '../verifySas9Login' import { RequestClient } from '../../request/RequestClient' +import { getExpectedLogInSuccessHeader } from '../' + jest.mock('axios') const mockedAxios = axios as jest.Mocked @@ -135,6 +136,7 @@ describe('AuthManager', () => { requestClient, authCallback ) + jest.spyOn(authManager, 'checkSession').mockImplementation(() => Promise.resolve({ isLoggedIn: false, @@ -143,8 +145,9 @@ describe('AuthManager', () => { loginForm: { name: 'test' } }) ) + mockedAxios.post.mockImplementation(() => - Promise.resolve({ data: mockLoginSuccessResponse }) + Promise.resolve({ data: getExpectedLogInSuccessHeader() }) ) const loginResponse = await authManager.logIn(userName, password) @@ -180,6 +183,7 @@ describe('AuthManager', () => { requestClient, authCallback ) + jest.spyOn(authManager, 'checkSession').mockImplementation(() => Promise.resolve({ isLoggedIn: false, @@ -188,8 +192,9 @@ describe('AuthManager', () => { loginForm: { name: 'test' } }) ) + mockedAxios.post.mockImplementation(() => - Promise.resolve({ data: mockLoginSuccessResponse }) + Promise.resolve({ data: getExpectedLogInSuccessHeader() }) ) mockedAxios.get.mockImplementation(() => Promise.resolve({ status: 200 })) @@ -304,56 +309,6 @@ describe('AuthManager', () => { mockLoginAuthoriseRequiredResponse ) }) - - it('should check login success header based on language preferences of the browser', () => { - const authManager = new AuthManager( - serverUrl, - serverType, - requestClient, - authCallback - ) - - // test built in language codes - Object.keys(authManager['successHeaders']).forEach((key) => { - languageGetter.mockReturnValue(key) - - expect( - authManager['isLogInSuccessHeaderPresent']( - serverType, - authManager['successHeaders'][key] - ) - ).toBeTruthy() - }) - - // test possible longer language codes - const possibleLanguageCodes = [ - { short: 'en', long: 'en-US' }, - { short: 'fr', long: 'fr-FR' }, - { short: 'es', long: 'es-ES' } - ] - - possibleLanguageCodes.forEach((key) => { - const { short, long } = key - languageGetter.mockReturnValue(long) - - expect( - authManager['isLogInSuccessHeaderPresent']( - serverType, - authManager['successHeaders'][short] - ) - ).toBeTruthy() - }) - - // test falling back to default language code - languageGetter.mockReturnValue('WRONG-LANGUAGE') - - expect( - authManager['isLogInSuccessHeaderPresent']( - serverType, - authManager['successHeaders'][authManager['defaultSuccessHeaderKey']] - ) - ).toBeTruthy() - }) }) describe('login - redirect mechanism', () => { diff --git a/src/auth/spec/loginHeader.spec.ts b/src/auth/spec/loginHeader.spec.ts new file mode 100644 index 00000000..33dbdf3c --- /dev/null +++ b/src/auth/spec/loginHeader.spec.ts @@ -0,0 +1,82 @@ +/** + * @jest-environment jsdom + */ + +import { ServerType } from '@sasjs/utils/types' +import { + loginSuccessHeaders, + isLogInSuccessHeaderPresent, + defaultSuccessHeaderKey +} from '../' + +describe('isLogInSuccessHeaderPresent', () => { + let languageGetter: any + + beforeEach(() => { + languageGetter = jest.spyOn(window.navigator, 'language', 'get') + }) + + it('should check SASVIYA and SAS9 login success header based on language preferences of the browser', () => { + // test SASVIYA server type + Object.keys(loginSuccessHeaders).forEach((key) => { + languageGetter.mockReturnValue(key) + + expect( + isLogInSuccessHeaderPresent( + ServerType.SasViya, + loginSuccessHeaders[key] + ) + ).toBeTruthy() + }) + + // test SAS9 server type + Object.keys(loginSuccessHeaders).forEach((key) => { + languageGetter.mockReturnValue(key) + + expect( + isLogInSuccessHeaderPresent(ServerType.Sas9, loginSuccessHeaders[key]) + ).toBeTruthy() + }) + + // test possible longer language codes + const possibleLanguageCodes = [ + { short: 'en', long: 'en-US' }, + { short: 'fr', long: 'fr-FR' }, + { short: 'es', long: 'es-ES' } + ] + + possibleLanguageCodes.forEach((key) => { + const { short, long } = key + languageGetter.mockReturnValue(long) + + expect( + isLogInSuccessHeaderPresent( + ServerType.SasViya, + loginSuccessHeaders[short] + ) + ).toBeTruthy() + }) + + // test falling back to default language code + languageGetter.mockReturnValue('WRONG-LANGUAGE') + + expect( + isLogInSuccessHeaderPresent( + ServerType.Sas9, + loginSuccessHeaders[defaultSuccessHeaderKey] + ) + ).toBeTruthy() + }) + + it('should check SASVJS login success header', () => { + expect( + isLogInSuccessHeaderPresent(ServerType.Sasjs, { loggedIn: true }) + ).toBeTruthy() + + expect( + isLogInSuccessHeaderPresent(ServerType.Sasjs, { loggedIn: false }) + ).toBeFalsy() + + expect(isLogInSuccessHeaderPresent(ServerType.Sasjs, undefined)).toBeFalsy() + }) +}) diff --git a/src/auth/spec/mockResponses.ts b/src/auth/spec/mockResponses.ts index c30aa8f9..9adf538b 100644 --- a/src/auth/spec/mockResponses.ts +++ b/src/auth/spec/mockResponses.ts @@ -1,8 +1,6 @@ import { SasAuthResponse } from '@sasjs/utils/types' -import { enLoginSuccessHeader } from '../AuthManager' export const mockLoginAuthoriseRequiredResponse = `` -export const mockLoginSuccessResponse = enLoginSuccessHeader export const mockAuthResponse: SasAuthResponse = { access_token: 'acc355', diff --git a/src/auth/spec/verifySas9Login.spec.ts b/src/auth/spec/verifySas9Login.spec.ts index 19696d40..282ad1da 100644 --- a/src/auth/spec/verifySas9Login.spec.ts +++ b/src/auth/spec/verifySas9Login.spec.ts @@ -3,7 +3,7 @@ */ import { verifySas9Login } from '../verifySas9Login' import * as delayModule from '../../utils/delay' -import { enLoginSuccessHeader } from '../AuthManager' +import { getExpectedLogInSuccessHeader } from '../' describe('verifySas9Login', () => { const serverUrl = 'http://test-server.com' @@ -19,7 +19,9 @@ describe('verifySas9Login', () => { const popup = { window: { location: { href: serverUrl + `/SASLogon` }, - document: { body: { innerText: `

${enLoginSuccessHeader}

` } } + document: { + body: { innerText: `

${getExpectedLogInSuccessHeader()}

` } + } } } as unknown as Window diff --git a/src/auth/spec/verifySasViyaLogin.spec.ts b/src/auth/spec/verifySasViyaLogin.spec.ts index 5183e07d..f1b4f52d 100644 --- a/src/auth/spec/verifySasViyaLogin.spec.ts +++ b/src/auth/spec/verifySasViyaLogin.spec.ts @@ -3,7 +3,7 @@ */ import { verifySasViyaLogin } from '../verifySasViyaLogin' import * as delayModule from '../../utils/delay' -import { enLoginSuccessHeader } from '../AuthManager' +import { getExpectedLogInSuccessHeader } from '../' describe('verifySasViyaLogin', () => { const serverUrl = 'http://test-server.com' @@ -20,7 +20,9 @@ describe('verifySasViyaLogin', () => { const popup = { window: { location: { href: serverUrl + `/SASLogon` }, - document: { body: { innerText: `

${enLoginSuccessHeader}

` } } + document: { + body: { innerText: `

${getExpectedLogInSuccessHeader()}

` } + } } } as unknown as Window diff --git a/src/auth/verifySas9Login.ts b/src/auth/verifySas9Login.ts index 831117e6..93a6adf8 100644 --- a/src/auth/verifySas9Login.ts +++ b/src/auth/verifySas9Login.ts @@ -1,5 +1,5 @@ import { delay } from '../utils' -import { enLoginSuccessHeader } from './AuthManager' +import { getExpectedLogInSuccessHeader } from './' export async function verifySas9Login(loginPopup: Window): Promise<{ isLoggedIn: boolean @@ -7,13 +7,17 @@ export async function verifySas9Login(loginPopup: Window): Promise<{ 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(enLoginSuccessHeader) + loginPopup.window.document.body.innerText.includes( + getExpectedLogInSuccessHeader() + ) + elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000 } while (!isLoggedIn && elapsedSeconds < 5 * 60) diff --git a/src/auth/verifySasViyaLogin.ts b/src/auth/verifySasViyaLogin.ts index 91e37442..46015d44 100644 --- a/src/auth/verifySasViyaLogin.ts +++ b/src/auth/verifySasViyaLogin.ts @@ -1,5 +1,5 @@ import { delay } from '../utils' -import { enLoginSuccessHeader } from './AuthManager' +import { getExpectedLogInSuccessHeader } from './' export async function verifySasViyaLogin(loginPopup: Window): Promise<{ isLoggedIn: boolean @@ -7,21 +7,32 @@ export async function verifySasViyaLogin(loginPopup: Window): Promise<{ 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(enLoginSuccessHeader) + loginPopup.window.document.body?.innerText?.includes( + getExpectedLogInSuccessHeader() + ) + elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000 } while (!isAuthorized && elapsedSeconds < 5 * 60) From c2e64d9ba6b11de7b59af83b8a14813151c6c92c Mon Sep 17 00:00:00 2001 From: Yury Date: Fri, 21 Jun 2024 11:32:58 +0300 Subject: [PATCH 4/4] chore: cleanup --- src/auth/spec/AuthManager.spec.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/auth/spec/AuthManager.spec.ts b/src/auth/spec/AuthManager.spec.ts index 01e5be00..18d7beba 100644 --- a/src/auth/spec/AuthManager.spec.ts +++ b/src/auth/spec/AuthManager.spec.ts @@ -68,12 +68,6 @@ describe('AuthManager', () => { }) describe('login - default mechanism', () => { - let languageGetter: any - - beforeEach(() => { - languageGetter = jest.spyOn(window.navigator, 'language', 'get') - }) - it('should call the auth callback and return when already logged in', async () => { const authManager = new AuthManager( serverUrl,