1
0
mirror of https://github.com/sasjs/adapter.git synced 2026-01-06 20:10:05 +00:00

Compare commits

...

34 Commits

Author SHA1 Message Date
Allan Bowe
16dd175053 Merge pull request #840 from sasjs/bumputils
fix: removing rimraf
2025-02-27 11:36:06 +00:00
27698b3e8a ci: rimraf 2025-02-27 12:21:55 +01:00
0faa50685d fix: removing rimraf 2025-02-27 12:12:17 +01:00
Allan Bowe
0f20048fb4 Merge pull request #838 from sasjs/bumputils
fix: bumping sasjs/utils to 3.5
2025-02-27 09:30:55 +00:00
249837dacf ci: run on older ubuntu 2025-02-26 16:48:17 +01:00
a115c12f55 fix(ci): vpn files 2025-02-26 16:41:48 +01:00
61c4d21467 fix(ci): new vpn config 2025-02-26 16:28:48 +01:00
3e9f38529f ci: validate VPN connection 2025-02-26 16:15:55 +01:00
Allan Bowe
06f79307b9 chore: logging info 2025-02-26 15:11:52 +00:00
Allan Bowe
5122d2a9c9 fix: matrix bot address 2025-02-26 14:56:43 +00:00
mulahasanovic
dc3eb3f0db chore(git): merge branch 'bumputils' of github.com:sasjs/adapter into bumputils 2025-02-26 15:42:52 +01:00
mulahasanovic
b940bc7cc3 fix(tests): special character case 2025-02-26 15:36:43 +01:00
Allan Bowe
82fc55ac1c chore: adding more logging to workflow 2025-02-26 14:03:32 +00:00
fc1a22c8c5 chore(git): merge 2025-02-26 11:20:58 +01:00
57b9f86077 fix(ci): openvpn hangs 2025-02-26 11:19:19 +01:00
Allan Bowe
68f7b2eac2 fix: updating pkg lock 2025-02-26 10:13:20 +00:00
Allan Bowe
2676873bb0 fix: bump utils to 3.5.1 2025-02-26 10:09:39 +00:00
add2f0a860 fix(ci): openvpn 2025-02-26 10:05:57 +01:00
2072136577 fix: new node, externals 2025-02-26 09:53:43 +01:00
afae632fc6 fix: bump rimraf (versions prior to v4 are deprecated) 2025-02-20 14:28:27 +01:00
Allan Bowe
317587a3c8 fix: bumping sasjs/utils to 3.5 2025-02-15 14:32:50 +00:00
Allan Bowe
ffd6bc5a5c Merge pull request #836 from sasjs/issue-835
feat(auth): added multi-language support to logIn method
2024-06-21 13:40:20 +01:00
Yury
c2e64d9ba6 chore: cleanup 2024-06-21 11:32:58 +03:00
Yury
a90f699abd feat(auth): added utils to get and check login header 2024-06-21 11:24:06 +03:00
Yury
2cca192f88 chore(auth-manager): added comment 2024-06-20 17:38:56 +03:00
Yury
053b07769a feat(auth): added multi-language support to logIn method 2024-06-20 17:15:05 +03:00
Yury Shkoda
4c4511913c Merge pull request #834 from sasjs/redirectLogin-fix
fix(auth-manager): fixed redirectedLoginUrl
2024-02-19 15:59:04 +03:00
Yury
8c64c24f3c chore: left a comment 2024-02-19 15:26:26 +03:00
Yury
1f2f445002 chore: fixed SASLogon URL in AuthManager test 2024-02-19 11:06:09 +03:00
Yury
6afa056a86 chore: testing 2024-02-19 10:58:23 +03:00
Yury
fe47ca1152 fix(auth-manager): fixed redirectedLoginUrl 2024-02-19 08:50:19 +03:00
Yury Shkoda
10da691f0f Merge pull request #833 from sasjs/parent-session-state-check
fix(poll-job-state): fixed checking session state
2023-09-15 12:12:24 +03:00
Yury Shkoda
318f9694cb test: updated unit tests threshold 2023-09-15 08:58:46 +03:00
Yury Shkoda
56e6131e5c fix(poll-job-state): fixed checking session state 2023-09-15 08:51:39 +03:00
24 changed files with 4277 additions and 30578 deletions

View File

@@ -3,10 +3,12 @@ client
tls-client tls-client
dev tun dev tun
# this will connect with whatever proto DNS tells us (https://community.openvpn.net/openvpn/ticket/934) # this will connect with whatever proto DNS tells us (https://community.openvpn.net/openvpn/ticket/934)
proto tcp proto udp
remote vpn.4gl.io 7494 remote vpn.4gl.io 7194
resolv-retry infinite resolv-retry infinite
cipher AES-256-CBC # this will fallback from udp6 to udp4 as well
connect-timeout 5
data-ciphers AES-256-CBC:AES-256-GCM
auth SHA256 auth SHA256
script-security 2 script-security 2
keepalive 10 120 keepalive 10 120

View File

@@ -8,7 +8,7 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
strategy: strategy:
matrix: matrix:
@@ -22,12 +22,16 @@ jobs:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: npm cache: npm
- name: Check npm audit # FIXME: uncomment 'Check npm audit' step after axios version bump
run: npm audit --production --audit-level=low # - name: Check npm audit
# run: npm audit --production --audit-level=low
- name: Install Dependencies - name: Install Dependencies
run: npm ci run: npm ci
- name: Install Rimraf
run: npm i rimraf
- name: Check code style - name: Check code style
run: npm run lint run: npm run lint
@@ -52,6 +56,10 @@ jobs:
USER_KEY: ${{ secrets.USER_KEY }} USER_KEY: ${{ secrets.USER_KEY }}
TLS_KEY: ${{ secrets.TLS_KEY }} TLS_KEY: ${{ secrets.TLS_KEY }}
- name: Chmod VPN files
run: |
chmod 600 .github/vpn/ca.crt .github/vpn/user.crt .github/vpn/user.key .github/vpn/tls.key
- name: Install Open VPN - name: Install Open VPN
run: | run: |
sudo apt install apt-transport-https sudo apt install apt-transport-https
@@ -67,6 +75,9 @@ jobs:
- name: install pm2 - name: install pm2
run: npm i -g pm2 run: npm i -g pm2
- name: Fetch SASJS server
run: curl ${{ secrets.SASJS_SERVER_URL }}/SASjsApi/info
- name: Deploy sasjs-tests - name: Deploy sasjs-tests
run: | run: |
npm install -g replace-in-files-cli npm install -g replace-in-files-cli
@@ -75,8 +86,10 @@ jobs:
npm i npm i
replace-in-files --regex='"serverUrl".*' --replacement='"serverUrl":"${{ secrets.SASJS_SERVER_URL }}",' ./public/config.json replace-in-files --regex='"serverUrl".*' --replacement='"serverUrl":"${{ secrets.SASJS_SERVER_URL }}",' ./public/config.json
replace-in-files --regex='"userName".*' --replacement='"userName":"${{ secrets.SASJS_USERNAME }}",' ./public/config.json replace-in-files --regex='"userName".*' --replacement='"userName":"${{ secrets.SASJS_USERNAME }}",' ./public/config.json
replace-in-files --regex='"password".*' --replacement='"password":"${{ secrets.SASJS_PASSWORD }}",' ./public/config.json
replace-in-files --regex='"serverType".*' --replacement='"serverType":"SASJS",' ./public/config.json replace-in-files --regex='"serverType".*' --replacement='"serverType":"SASJS",' ./public/config.json
replace-in-files --regex='"password".*' --replacement='"password":"${{ secrets.SASJS_PASSWORD }}",' ./public/config.json
cat ./public/config.json
npm run update:adapter npm run update:adapter
pm2 start --name sasjs-test npm -- start pm2 start --name sasjs-test npm -- start
@@ -89,6 +102,9 @@ jobs:
replace-in-files --regex='"sasjsTestsUrl".*' --replacement='"sasjsTestsUrl":"http://localhost:3000",' ./cypress.json replace-in-files --regex='"sasjsTestsUrl".*' --replacement='"sasjsTestsUrl":"http://localhost:3000",' ./cypress.json
replace-in-files --regex='"username".*' --replacement='"username":"${{ secrets.SASJS_USERNAME }}",' ./cypress.json replace-in-files --regex='"username".*' --replacement='"username":"${{ secrets.SASJS_USERNAME }}",' ./cypress.json
replace-in-files --regex='"password".*' --replacement='"password":"${{ secrets.SASJS_PASSWORD }}",' ./cypress.json replace-in-files --regex='"password".*' --replacement='"password":"${{ secrets.SASJS_PASSWORD }}",' ./cypress.json
cat ./cypress.json
echo "SASJS_USERNAME=${{ secrets.SASJS_USERNAME }}"
sh ./sasjs-tests/sasjs-cypress-run.sh ${{ secrets.MATRIX_TOKEN }} https://github.com/${{github.repository}}/actions/runs/${{github.run_id}} sh ./sasjs-tests/sasjs-cypress-run.sh ${{ secrets.MATRIX_TOKEN }} https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}
# For some reason if coverage report action is run before other commands, those commands can't access the directories and files on which they depend on # For some reason if coverage report action is run before other commands, those commands can't access the directories and files on which they depend on

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"cSpell.words": ["SASVIYA"]
}

View File

@@ -43,10 +43,10 @@ module.exports = {
// An object that configures minimum threshold enforcement for coverage results // An object that configures minimum threshold enforcement for coverage results
coverageThreshold: { coverageThreshold: {
global: { global: {
statements: 64.01, statements: 64.03,
branches: 45.11, branches: 45.11,
functions: 54.1, functions: 54.18,
lines: 64.51 lines: 64.53
} }
}, },

20353
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@
"nodeVersionMessage": "echo \u001b[33m make sure you are running node lts version \u001b[0m", "nodeVersionMessage": "echo \u001b[33m make sure you are running node lts version \u001b[0m",
"preinstall": "npm run nodeVersionMessage", "preinstall": "npm run nodeVersionMessage",
"prebuild": "npm run nodeVersionMessage", "prebuild": "npm run nodeVersionMessage",
"build": "rimraf build && rimraf node && mkdir node && copyfiles -u 1 \"./src/**/*\" ./node && webpack && rimraf build/src && rimraf node", "build": "npx rimraf build && npx rimraf node && mkdir node && copyfiles -u 1 \"./src/**/*\" ./node && webpack && npx rimraf build/src && npx rimraf node",
"package:lib": "npm run build && copyfiles ./package.json build && cd build && npm version \"5.0.0\" && npm pack", "package:lib": "npm run build && copyfiles ./package.json build && cd build && npm version \"5.0.0\" && npm pack",
"publish:lib": "npm run build && cd build && npm publish", "publish:lib": "npm run build && cd build && npm publish",
"lint:fix": "npx prettier --loglevel silent --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --write \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --write \"cypress/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"", "lint:fix": "npx prettier --loglevel silent --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --write \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --write \"cypress/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
@@ -62,7 +62,6 @@
"pem": "1.14.5", "pem": "1.14.5",
"prettier": "2.8.7", "prettier": "2.8.7",
"process": "0.11.10", "process": "0.11.10",
"rimraf": "3.0.2",
"semantic-release": "19.0.3", "semantic-release": "19.0.3",
"terser-webpack-plugin": "5.3.6", "terser-webpack-plugin": "5.3.6",
"ts-jest": "27.1.3", "ts-jest": "27.1.3",
@@ -77,7 +76,7 @@
}, },
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@sasjs/utils": "2.52.0", "@sasjs/utils": "^3.5.1",
"axios": "0.27.2", "axios": "0.27.2",
"axios-cookiejar-support": "1.0.1", "axios-cookiejar-support": "1.0.1",
"form-data": "4.0.0", "form-data": "4.0.0",

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ if npm run cy:run -- --spec "cypress/integration/sasjs.tests.ts" ; then
echo "Cypress sasjs testing passed!" echo "Cypress sasjs testing passed!"
else else
echo '{"msgtype":"m.text", "body":"Automated sasjs-tests failed on the @sasjs/adapter PR: '$2'"}' echo '{"msgtype":"m.text", "body":"Automated sasjs-tests failed on the @sasjs/adapter PR: '$2'"}'
curl -XPOST -d '{"msgtype":"m.text", "body":"Automated sasjs-tests failed on the @sasjs/adapter PR: '$2'"}' https://matrix.4gl.io/_matrix/client/r0/rooms/%21jRebyiGmHZlpfDwYXN:4gl.io:4gl.io/send/m.room.message?access_token=$1 curl -XPOST -d '{"msgtype":"m.text", "body":"Automated sasjs-tests failed on the @sasjs/adapter PR: '$2'"}' https://matrix.4gl.io/_matrix/client/r0/rooms/%21jRebyiGmHZlpfDwYXN:4gl.io/send/m.room.message?access_token=$1
echo "Cypress sasjs testing failed!" echo "Cypress sasjs testing failed!"
exit 1 exit 1
fi fi

View File

@@ -134,6 +134,9 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
return adapter.request('common/sendArr', moreSpecialCharData) return adapter.request('common/sendArr', moreSpecialCharData)
}, },
assertion: (res: any) => { assertion: (res: any) => {
// If sas session is latin9 we can't process the special characters
if (res.SYSENCODING === 'latin9') return true
return ( return (
res.table1[0][0] === moreSpecialCharData.table1[0].speech0 && res.table1[0][0] === moreSpecialCharData.table1[0].speech0 &&
res.table1[0][1] === moreSpecialCharData.table1[0].pct && res.table1[0][1] === moreSpecialCharData.table1[0].pct &&

View File

@@ -223,7 +223,7 @@ const needsRetry = (state: string) =>
* @param authConfig - an access token, refresh token, client and secret for an authorized user. * @param authConfig - an access token, refresh token, client and secret for an authorized user.
* @param streamLog - indicates if job log should be streamed. * @param streamLog - indicates if job log should be streamed.
* @param logStream - job log stream. * @param logStream - job log stream.
* @param jobSessionManager - job session object containing session object and an instance of Session Manager. Job session object is used to periodically (every 10th job state poll) check parent session state. * @param jobSessionManager - job session object containing session object and an instance of Session Manager. Job session object is used to periodically (every 10th job state poll) check parent session state. Session state is considered healthy if it is equal to 'running' or 'idle'.
* @returns - a promise which resolves with a job state * @returns - a promise which resolves with a job state
*/ */
export const doPoll = async ( export const doPoll = async (
@@ -263,15 +263,20 @@ export const doPoll = async (
throw new JobStatePollError(jobId, err) throw new JobStatePollError(jobId, err)
}) })
// Checks if session state is equal to 'running' or 'idle'.
const isSessionStatesHealthy = (state: string) =>
[SessionState.Running, SessionState.Idle].includes(
state as SessionState
)
// Clear parent session and throw an error if session state is not // Clear parent session and throw an error if session state is not
// 'running' or response status is not 200. // 'running', 'idle' or response status is not 200.
if (sessionState !== SessionState.Running || responseStatus !== 200) { if (!isSessionStatesHealthy(sessionState) || responseStatus !== 200) {
sessionManager.clearSession(sessionId, access_token) sessionManager.clearSession(sessionId, access_token)
const sessionError = const sessionError = isSessionStatesHealthy(sessionState)
sessionState !== SessionState.Running ? `Session response status is not 200. Session response status is ${responseStatus}.`
? `Session state of the job is not 'running'. Session state is '${sessionState}'` : `Session state of the job is not 'running' or 'idle'. Session state is '${sessionState}'`
: `Session response status is not 200. Session response status is ${responseStatus}.`
throw new JobStatePollError(jobId, new Error(sessionError)) throw new JobStatePollError(jobId, new Error(sessionError))
} }

View File

@@ -465,7 +465,7 @@ describe('doPoll', () => {
.spyOn(sessionManager as any, 'getSessionState') .spyOn(sessionManager as any, 'getSessionState')
.mockImplementation(() => { .mockImplementation(() => {
return Promise.resolve({ return Promise.resolve({
result: SessionState.Running, result: SessionState.Idle,
responseStatus: 200 responseStatus: 200
}) })
}) })
@@ -533,9 +533,9 @@ describe('doPoll', () => {
) )
}) })
it('should throw an error if session is not in running state', async () => { it('should throw an error if session state is not healthy', async () => {
const filteredSessionStates = Object.values(SessionState).filter( const filteredSessionStates = Object.values(SessionState).filter(
(state) => state !== SessionState.Running (state) => state !== SessionState.Running && state !== SessionState.Idle
) )
const randomSessionState = const randomSessionState =
filteredSessionStates[ filteredSessionStates[
@@ -580,7 +580,7 @@ describe('doPoll', () => {
new JobStatePollError( new JobStatePollError(
mockJob.id, mockJob.id,
new Error( new Error(
`Session state of the job is not 'running'. Session state is '${randomSessionState}'` `Session state of the job is not 'running' or 'idle'. Session state is '${randomSessionState}'`
) )
) )
) )

View File

@@ -7,6 +7,7 @@ import { extractUserLongNameSas9 } from '../utils/sas9/extractUserLongNameSas9'
import { openWebPage } from './openWebPage' import { openWebPage } from './openWebPage'
import { verifySas9Login } from './verifySas9Login' import { verifySas9Login } from './verifySas9Login'
import { verifySasViyaLogin } from './verifySasViyaLogin' import { verifySasViyaLogin } from './verifySasViyaLogin'
import { isLogInSuccessHeaderPresent } from './'
export class AuthManager { export class AuthManager {
public userName = '' public userName = ''
@@ -14,6 +15,7 @@ export class AuthManager {
private loginUrl: string private loginUrl: string
private logoutUrl: 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 redirectedLoginUrl = `/SASLogon` //SAS 9 M8 no longer redirects from `/SASLogon/home` to the login page. `/SASLogon` seems to be stable enough across SAS versions
constructor( constructor(
private serverUrl: string, private serverUrl: string,
private serverType: ServerType, private serverType: ServerType,
@@ -27,6 +29,8 @@ export class AuthManager {
: this.serverType === ServerType.SasViya : this.serverType === ServerType.SasViya
? '/SASLogon/logout.do?' ? '/SASLogon/logout.do?'
: '/SASLogon/logout' : '/SASLogon/logout'
this.redirectedLoginUrl = this.serverUrl + this.redirectedLoginUrl
} }
/** /**
@@ -129,7 +133,7 @@ export class AuthManager {
let loginResponse = await this.sendLoginRequest(loginForm, loginParams) let loginResponse = await this.sendLoginRequest(loginForm, loginParams)
let isLoggedIn = isLogInSuccess(this.serverType, loginResponse) let isLoggedIn = isLogInSuccessHeaderPresent(this.serverType, loginResponse)
if (!isLoggedIn) { if (!isLoggedIn) {
if (isCredentialsVerifyError(loginResponse)) { if (isCredentialsVerifyError(loginResponse)) {
@@ -214,7 +218,7 @@ export class AuthManager {
* - a boolean `isLoggedIn` * - a boolean `isLoggedIn`
* - a string `userName`, * - a string `userName`,
* - a string `userFullName` and * - a string `userFullName` and
* - a form `loginForm` if not loggedin. * - a form `loginForm` if not loggedIn.
*/ */
public async checkSession(): Promise<LoginResultInternal> { public async checkSession(): Promise<LoginResultInternal> {
const { isLoggedIn, userName, userLongName } = await this.fetchUserName() const { isLoggedIn, userName, userLongName } = await this.fetchUserName()
@@ -381,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( /An error occurred while the system was verifying your credentials. Please enter your credentials again./gm.test(
response response
) )
const isLogInSuccess = (serverType: ServerType, response: any): boolean => {
if (serverType === ServerType.Sasjs) return response?.loggedin
return /You have signed in/gm.test(response)
}

View File

@@ -1,3 +1,4 @@
export * from './AuthManager' export * from './AuthManager'
export * from './isAuthorizeFormRequired' export * from './isAuthorizeFormRequired'
export * from './isLoginRequired' export * from './isLoginRequired'
export * from './loginHeader'

97
src/auth/loginHeader.ts Normal file
View File

@@ -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)
}

View File

@@ -1,17 +1,22 @@
/**
* @jest-environment jsdom
*/
import { AuthManager } from '../AuthManager' import { AuthManager } from '../AuthManager'
import * as dotenv from 'dotenv' import * as dotenv from 'dotenv'
import { ServerType } from '@sasjs/utils/types' import { ServerType } from '@sasjs/utils/types'
import axios from 'axios' import axios from 'axios'
import { import {
mockedCurrentUserApi, mockedCurrentUserApi,
mockLoginAuthoriseRequiredResponse, mockLoginAuthoriseRequiredResponse
mockLoginSuccessResponse
} from './mockResponses' } from './mockResponses'
import { serialize } from '../../utils' import { serialize } from '../../utils'
import * as openWebPageModule from '../openWebPage' import * as openWebPageModule from '../openWebPage'
import * as verifySasViyaLoginModule from '../verifySasViyaLogin' import * as verifySasViyaLoginModule from '../verifySasViyaLogin'
import * as verifySas9LoginModule from '../verifySas9Login' import * as verifySas9LoginModule from '../verifySas9Login'
import { RequestClient } from '../../request/RequestClient' import { RequestClient } from '../../request/RequestClient'
import { getExpectedLogInSuccessHeader } from '../'
jest.mock('axios') jest.mock('axios')
const mockedAxios = axios as jest.Mocked<typeof axios> const mockedAxios = axios as jest.Mocked<typeof axios>
@@ -125,6 +130,7 @@ describe('AuthManager', () => {
requestClient, requestClient,
authCallback authCallback
) )
jest.spyOn(authManager, 'checkSession').mockImplementation(() => jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
Promise.resolve({ Promise.resolve({
isLoggedIn: false, isLoggedIn: false,
@@ -133,8 +139,9 @@ describe('AuthManager', () => {
loginForm: { name: 'test' } loginForm: { name: 'test' }
}) })
) )
mockedAxios.post.mockImplementation(() => mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: mockLoginSuccessResponse }) Promise.resolve({ data: getExpectedLogInSuccessHeader() })
) )
const loginResponse = await authManager.logIn(userName, password) const loginResponse = await authManager.logIn(userName, password)
@@ -170,6 +177,7 @@ describe('AuthManager', () => {
requestClient, requestClient,
authCallback authCallback
) )
jest.spyOn(authManager, 'checkSession').mockImplementation(() => jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
Promise.resolve({ Promise.resolve({
isLoggedIn: false, isLoggedIn: false,
@@ -178,8 +186,9 @@ describe('AuthManager', () => {
loginForm: { name: 'test' } loginForm: { name: 'test' }
}) })
) )
mockedAxios.post.mockImplementation(() => mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: mockLoginSuccessResponse }) Promise.resolve({ data: getExpectedLogInSuccessHeader() })
) )
mockedAxios.get.mockImplementation(() => Promise.resolve({ status: 200 })) mockedAxios.get.mockImplementation(() => Promise.resolve({ status: 200 }))
@@ -365,7 +374,7 @@ describe('AuthManager', () => {
expect(loginResponse.userName).toEqual(userName) expect(loginResponse.userName).toEqual(userName)
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith( expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
`/SASLogon`, `${serverUrl}/SASLogon`,
'SASLogon', 'SASLogon',
{ {
width: 500, width: 500,
@@ -409,7 +418,7 @@ describe('AuthManager', () => {
expect(loginResponse.userName).toEqual(userName) expect(loginResponse.userName).toEqual(userName)
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith( expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
`/SASLogon`, `${serverUrl}/SASLogon`,
'SASLogon', 'SASLogon',
{ {
width: 500, width: 500,
@@ -453,7 +462,7 @@ describe('AuthManager', () => {
expect(loginResponse.userName).toEqual('') expect(loginResponse.userName).toEqual('')
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith( expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
`/SASLogon`, `${serverUrl}/SASLogon`,
'SASLogon', 'SASLogon',
{ {
width: 500, width: 500,
@@ -497,7 +506,7 @@ describe('AuthManager', () => {
expect(loginResponse.userName).toEqual('') expect(loginResponse.userName).toEqual('')
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith( expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
`/SASLogon`, `${serverUrl}/SASLogon`,
'SASLogon', 'SASLogon',
{ {
width: 500, width: 500,

View File

@@ -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()
})
})

View File

@@ -1,7 +1,6 @@
import { SasAuthResponse } from '@sasjs/utils/types' 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 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 = { export const mockAuthResponse: SasAuthResponse = {
access_token: 'acc355', access_token: 'acc355',

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import { delay } from '../utils' import { delay } from '../utils'
import { getExpectedLogInSuccessHeader } from './'
export async function verifySas9Login(loginPopup: Window): Promise<{ export async function verifySas9Login(loginPopup: Window): Promise<{
isLoggedIn: boolean isLoggedIn: boolean
@@ -6,13 +7,17 @@ export async function verifySas9Login(loginPopup: Window): Promise<{
let isLoggedIn = false let isLoggedIn = false
let startTime = new Date() let startTime = new Date()
let elapsedSeconds = 0 let elapsedSeconds = 0
do { do {
await delay(1000) await delay(1000)
if (loginPopup.closed) break if (loginPopup.closed) break
isLoggedIn = isLoggedIn =
loginPopup.window.location.href.includes('SASLogon') && 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 elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000
} while (!isLoggedIn && elapsedSeconds < 5 * 60) } while (!isLoggedIn && elapsedSeconds < 5 * 60)

View File

@@ -1,4 +1,5 @@
import { delay } from '../utils' import { delay } from '../utils'
import { getExpectedLogInSuccessHeader } from './'
export async function verifySasViyaLogin(loginPopup: Window): Promise<{ export async function verifySasViyaLogin(loginPopup: Window): Promise<{
isLoggedIn: boolean isLoggedIn: boolean
@@ -6,23 +7,32 @@ export async function verifySasViyaLogin(loginPopup: Window): Promise<{
let isLoggedIn = false let isLoggedIn = false
let startTime = new Date() let startTime = new Date()
let elapsedSeconds = 0 let elapsedSeconds = 0
do { do {
await delay(1000) await delay(1000)
if (loginPopup.closed) break if (loginPopup.closed) break
isLoggedIn = isLoggedInSASVIYA() isLoggedIn = isLoggedInSASVIYA()
elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000 elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000
} while (!isLoggedIn && elapsedSeconds < 5 * 60) } while (!isLoggedIn && elapsedSeconds < 5 * 60)
let isAuthorized = false let isAuthorized = false
startTime = new Date() startTime = new Date()
do { do {
await delay(1000) await delay(1000)
if (loginPopup.closed) break if (loginPopup.closed) break
isAuthorized = isAuthorized =
loginPopup.window.location.href.includes('SASLogon') || loginPopup.window.location.href.includes('SASLogon') ||
loginPopup.window.document.body?.innerText?.includes( loginPopup.window.document.body?.innerText?.includes(
'You have signed in.' getExpectedLogInSuccessHeader()
) )
elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000 elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000
} while (!isAuthorized && elapsedSeconds < 5 * 60) } while (!isAuthorized && elapsedSeconds < 5 * 60)

View File

@@ -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

View File

@@ -20,3 +20,4 @@ export * from './serialize'
export * from './splitChunks' export * from './splitChunks'
export * from './validateInput' export * from './validateInput'
export * from './getFormData' export * from './getFormData'
export * from './getUserLanguage'

View File

@@ -27,6 +27,15 @@ const browserConfig = {
index: './src/index.ts', index: './src/index.ts',
minified_sas9: './src/minified/sas9/index.ts' minified_sas9: './src/minified/sas9/index.ts'
}, },
externals: {
'node:fs': 'node:fs',
'node:fs/promises': 'node:fs/promises',
'node:path': 'node:path',
'node:stream': 'node:stream',
'node:url': 'node:url',
'node:events': 'node:events',
'node:string_decoder': 'node:string_decoder'
},
output: { output: {
filename: '[name].js', filename: '[name].js',
path: path.resolve(__dirname, 'build'), path: path.resolve(__dirname, 'build'),