Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(auth): added multi-language support to logIn method #836

Merged
merged 4 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading