Skip to content

Commit

Permalink
Merge pull request #836 from sasjs/issue-835
Browse files Browse the repository at this point in the history
feat(auth): added multi-language support to logIn method
  • Loading branch information
allanbowe authored Jun 21, 2024
2 parents 4c45119 + c2e64d9 commit ffd6bc5
Show file tree
Hide file tree
Showing 13 changed files with 235 additions and 17 deletions.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"cSpell.words": ["SASVIYA"]
}
11 changes: 3 additions & 8 deletions src/auth/AuthManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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 = ''
Expand Down Expand Up @@ -132,7 +133,7 @@ export class AuthManager {

let loginResponse = await this.sendLoginRequest(loginForm, loginParams)

let isLoggedIn = isLogInSuccess(this.serverType, loginResponse)
let isLoggedIn = isLogInSuccessHeaderPresent(this.serverType, loginResponse)

if (!isLoggedIn) {
if (isCredentialsVerifyError(loginResponse)) {
Expand Down Expand Up @@ -217,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<LoginResultInternal> {
const { isLoggedIn, userName, userLongName } = await this.fetchUserName()
Expand Down Expand Up @@ -384,9 +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
)

const isLogInSuccess = (serverType: ServerType, response: any): boolean => {
if (serverType === ServerType.Sasjs) return response?.loggedin

return /You have signed in/gm.test(response)
}
1 change: 1 addition & 0 deletions src/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './AuthManager'
export * from './isAuthorizeFormRequired'
export * from './isLoginRequired'
export * from './loginHeader'
97 changes: 97 additions & 0 deletions src/auth/loginHeader.ts
Original file line number Diff line number Diff line change
@@ -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)
}
17 changes: 13 additions & 4 deletions src/auth/spec/AuthManager.spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
/**
* @jest-environment jsdom
*/

import { AuthManager } from '../AuthManager'
import * as dotenv from 'dotenv'
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<typeof axios>

Expand Down Expand Up @@ -125,6 +130,7 @@ describe('AuthManager', () => {
requestClient,
authCallback
)

jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
Promise.resolve({
isLoggedIn: false,
Expand All @@ -133,8 +139,9 @@ describe('AuthManager', () => {
loginForm: { name: 'test' }
})
)

mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: mockLoginSuccessResponse })
Promise.resolve({ data: getExpectedLogInSuccessHeader() })
)

const loginResponse = await authManager.logIn(userName, password)
Expand Down Expand Up @@ -170,6 +177,7 @@ describe('AuthManager', () => {
requestClient,
authCallback
)

jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
Promise.resolve({
isLoggedIn: false,
Expand All @@ -178,8 +186,9 @@ describe('AuthManager', () => {
loginForm: { name: 'test' }
})
)

mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: mockLoginSuccessResponse })
Promise.resolve({ data: getExpectedLogInSuccessHeader() })
)
mockedAxios.get.mockImplementation(() => Promise.resolve({ status: 200 }))

Expand Down
82 changes: 82 additions & 0 deletions src/auth/spec/loginHeader.spec.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
1 change: 0 additions & 1 deletion src/auth/spec/mockResponses.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { SasAuthResponse } from '@sasjs/utils/types'

export const mockLoginAuthoriseRequiredResponse = `<form id="application_authorization" action="/SASLogon/oauth/authorize" method="POST"><input type="hidden" name="X-Uaa-Csrf" value="2nfuxIn6WaOURWL7tzTXCe"/>`
export const mockLoginSuccessResponse = `You have signed in`

export const mockAuthResponse: SasAuthResponse = {
access_token: 'acc355',
Expand Down
5 changes: 4 additions & 1 deletion src/auth/spec/verifySas9Login.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
import { verifySas9Login } from '../verifySas9Login'
import * as delayModule from '../../utils/delay'
import { getExpectedLogInSuccessHeader } from '../'

describe('verifySas9Login', () => {
const serverUrl = 'http://test-server.com'
Expand All @@ -18,7 +19,9 @@ describe('verifySas9Login', () => {
const popup = {
window: {
location: { href: serverUrl + `/SASLogon` },
document: { body: { innerText: '<h3>You have signed in.</h3>' } }
document: {
body: { innerText: `<h3>${getExpectedLogInSuccessHeader()}</h3>` }
}
}
} as unknown as Window

Expand Down
5 changes: 4 additions & 1 deletion src/auth/spec/verifySasViyaLogin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
import { verifySasViyaLogin } from '../verifySasViyaLogin'
import * as delayModule from '../../utils/delay'
import { getExpectedLogInSuccessHeader } from '../'

describe('verifySasViyaLogin', () => {
const serverUrl = 'http://test-server.com'
Expand All @@ -19,7 +20,9 @@ describe('verifySasViyaLogin', () => {
const popup = {
window: {
location: { href: serverUrl + `/SASLogon` },
document: { body: { innerText: '<h3>You have signed in.</h3>' } }
document: {
body: { innerText: `<h3>${getExpectedLogInSuccessHeader()}</h3>` }
}
}
} as unknown as Window

Expand Down
7 changes: 6 additions & 1 deletion src/auth/verifySas9Login.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import { delay } from '../utils'
import { getExpectedLogInSuccessHeader } from './'

export async function verifySas9Login(loginPopup: Window): Promise<{
isLoggedIn: boolean
}> {
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.')
loginPopup.window.document.body.innerText.includes(
getExpectedLogInSuccessHeader()
)

elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000
} while (!isLoggedIn && elapsedSeconds < 5 * 60)

Expand Down
12 changes: 11 additions & 1 deletion src/auth/verifySasViyaLogin.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,38 @@
import { delay } from '../utils'
import { getExpectedLogInSuccessHeader } from './'

export async function verifySasViyaLogin(loginPopup: Window): Promise<{
isLoggedIn: boolean
}> {
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.'
getExpectedLogInSuccessHeader()
)

elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000
} while (!isAuthorized && elapsedSeconds < 5 * 60)

Expand Down
Loading

0 comments on commit ffd6bc5

Please sign in to comment.