From 053b07769a2a1918dd24d8350828bdc0b590c38b Mon Sep 17 00:00:00 2001 From: Yury Date: Thu, 20 Jun 2024 17:15:05 +0300 Subject: [PATCH] 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'