From 2166e632eb124cfbce2f11c7937927ed2d16c892 Mon Sep 17 00:00:00 2001 From: Robert Baruck Date: Mon, 22 Jun 2026 15:29:36 +0200 Subject: [PATCH] FEATURE: Reload page to go to login when user session expires; drop obsolete dev-deps --- Configuration/Settings.yaml | 6 + Resources/Private/Neos.Ui/.gitignore | 1 + Resources/Private/Neos.Ui/.npmrc | 1 + Resources/Private/Neos.Ui/.nvmrc | 1 + Resources/Private/Neos.Ui/esbuild.js | 24 + Resources/Private/Neos.Ui/package-lock.json | 531 ++++++++++++++++++ Resources/Private/Neos.Ui/package.json | 17 + Resources/Private/Neos.Ui/src/index.ts | 1 + Resources/Private/Neos.Ui/src/manifest.ts | 15 + .../Neos.Ui/src/neos-ui-extensibility.d.ts | 13 + .../Neos.Ui/src/reloadOnAuthTimeoutSaga.ts | 19 + Resources/Private/Neos.Ui/tsconfig.json | 16 + Resources/Public/Neos.Ui/Plugin.js | 94 ++++ Tests/E2E/features/default/login.feature | 7 + Tests/E2E/helpers/system.ts | 10 + Tests/E2E/steps/session-timeout.steps.ts | 42 ++ composer.json | 6 +- 17 files changed, 799 insertions(+), 5 deletions(-) create mode 100644 Resources/Private/Neos.Ui/.gitignore create mode 100644 Resources/Private/Neos.Ui/.npmrc create mode 100644 Resources/Private/Neos.Ui/.nvmrc create mode 100644 Resources/Private/Neos.Ui/esbuild.js create mode 100644 Resources/Private/Neos.Ui/package-lock.json create mode 100644 Resources/Private/Neos.Ui/package.json create mode 100644 Resources/Private/Neos.Ui/src/index.ts create mode 100644 Resources/Private/Neos.Ui/src/manifest.ts create mode 100644 Resources/Private/Neos.Ui/src/neos-ui-extensibility.d.ts create mode 100644 Resources/Private/Neos.Ui/src/reloadOnAuthTimeoutSaga.ts create mode 100644 Resources/Private/Neos.Ui/tsconfig.json create mode 100644 Resources/Public/Neos.Ui/Plugin.js create mode 100644 Tests/E2E/steps/session-timeout.steps.ts diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index 4b6761f..268f2e0 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -33,6 +33,12 @@ Neos: 'Sandstorm.NeosTwoFactorAuthentication:AdditionalScripts': 'resource://Sandstorm.NeosTwoFactorAuthentication/Public/index.js' 'Sandstorm.NeosTwoFactorAuthentication:WebAuthnScripts': 'resource://Sandstorm.NeosTwoFactorAuthentication/Public/JavaScript/webauthn.js' + Ui: + resources: + javascript: + 'Sandstorm.NeosTwoFactorAuthentication:ReloadOnAuthTimeout': + resource: 'resource://Sandstorm.NeosTwoFactorAuthentication/Public/Neos.Ui/Plugin.js' + Flow: http: middlewares: diff --git a/Resources/Private/Neos.Ui/.gitignore b/Resources/Private/Neos.Ui/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/Resources/Private/Neos.Ui/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/Resources/Private/Neos.Ui/.npmrc b/Resources/Private/Neos.Ui/.npmrc new file mode 100644 index 0000000..7230105 --- /dev/null +++ b/Resources/Private/Neos.Ui/.npmrc @@ -0,0 +1 @@ +min-release-age = 7 # days diff --git a/Resources/Private/Neos.Ui/.nvmrc b/Resources/Private/Neos.Ui/.nvmrc new file mode 100644 index 0000000..a3b7a31 --- /dev/null +++ b/Resources/Private/Neos.Ui/.nvmrc @@ -0,0 +1 @@ +v24.14.1 diff --git a/Resources/Private/Neos.Ui/esbuild.js b/Resources/Private/Neos.Ui/esbuild.js new file mode 100644 index 0000000..363f3eb --- /dev/null +++ b/Resources/Private/Neos.Ui/esbuild.js @@ -0,0 +1,24 @@ +const esbuild = require("esbuild"); +const extensibilityMap = require("@neos-project/neos-ui-extensibility/extensibilityMap.json"); +const isWatch = process.argv.includes("--watch"); + +// `alias: extensibilityMap` resolves redux-saga, @neos-project/* etc. to the +// host's shared runtime instead of bundling private copies. This is essential +// for redux-saga: a separately bundled copy uses different effect Symbols, so +// the host saga middleware would not recognise our `take()` and the saga would +// silently never fire. +/** @type {import("esbuild").BuildOptions} */ +const options = { + logLevel: "info", + bundle: true, + target: "es2020", + entryPoints: { Plugin: "src/index.ts" }, + outdir: "../../Public/Neos.Ui/", + alias: extensibilityMap, +}; + +if (isWatch) { + esbuild.context(options).then((ctx) => ctx.watch()); +} else { + esbuild.build(options); +} diff --git a/Resources/Private/Neos.Ui/package-lock.json b/Resources/Private/Neos.Ui/package-lock.json new file mode 100644 index 0000000..5264c15 --- /dev/null +++ b/Resources/Private/Neos.Ui/package-lock.json @@ -0,0 +1,531 @@ +{ + "name": "@sandstorm/neos-two-factor-authentication-neos-ui", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@sandstorm/neos-two-factor-authentication-neos-ui", + "version": "1.0.0", + "dependencies": { + "@neos-project/neos-ui-extensibility": "^8.0.0" + }, + "devDependencies": { + "esbuild": "^0.28.1", + "typescript": "^5.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@neos-project/neos-ui-extensibility": { + "version": "8.4.3", + "resolved": "https://registry.npmjs.org/@neos-project/neos-ui-extensibility/-/neos-ui-extensibility-8.4.3.tgz", + "integrity": "sha512-AN2gkvOSHpqTho7cbWX2KD6sQJZ3JbMR/KF7wfpoY2VpfYXfU6YbV+SEU4P/w0s//3bjzOuUDJQX8FXAEFPaDQ==", + "dependencies": { + "@neos-project/positional-array-sorter": "8.4.3" + } + }, + "node_modules/@neos-project/positional-array-sorter": { + "version": "8.4.3", + "resolved": "https://registry.npmjs.org/@neos-project/positional-array-sorter/-/positional-array-sorter-8.4.3.tgz", + "integrity": "sha512-++9mWMGZC1zDUKF9Bp0W7RHRAAvvqyDN9lbSv8YW2Nc5P6DHLUy7pUJ8pxazrmvthqNUZBtJJDY0sNlvltK3qg==", + "license": "GNU GPLv3" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/Resources/Private/Neos.Ui/package.json b/Resources/Private/Neos.Ui/package.json new file mode 100644 index 0000000..56413f9 --- /dev/null +++ b/Resources/Private/Neos.Ui/package.json @@ -0,0 +1,17 @@ +{ + "name": "@sandstorm/neos-two-factor-authentication-neos-ui", + "private": true, + "version": "1.0.0", + "scripts": { + "build": "node esbuild.js", + "watch": "node esbuild.js --watch", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "esbuild": "^0.28.1", + "typescript": "^5.0.0" + }, + "dependencies": { + "@neos-project/neos-ui-extensibility": "^8.0.0" + } +} diff --git a/Resources/Private/Neos.Ui/src/index.ts b/Resources/Private/Neos.Ui/src/index.ts new file mode 100644 index 0000000..a528a96 --- /dev/null +++ b/Resources/Private/Neos.Ui/src/index.ts @@ -0,0 +1 @@ +import './manifest'; diff --git a/Resources/Private/Neos.Ui/src/manifest.ts b/Resources/Private/Neos.Ui/src/manifest.ts new file mode 100644 index 0000000..6728c7b --- /dev/null +++ b/Resources/Private/Neos.Ui/src/manifest.ts @@ -0,0 +1,15 @@ +import manifest from '@neos-project/neos-ui-extensibility'; +import reloadOnAuthTimeout from './reloadOnAuthTimeoutSaga'; + +manifest('Sandstorm.NeosTwoFactorAuthentication:ReloadOnAuthTimeout', {}, (globalRegistry: any) => { + const sagasRegistry = globalRegistry.get('sagas'); + + if (!sagasRegistry) { + console.error('[2FA] sagas registry not found; cannot register reload-on-auth-timeout saga'); + return; + } + + sagasRegistry.set('Sandstorm.NeosTwoFactorAuthentication/reloadOnAuthTimeout', { + saga: reloadOnAuthTimeout, + }); +}); diff --git a/Resources/Private/Neos.Ui/src/neos-ui-extensibility.d.ts b/Resources/Private/Neos.Ui/src/neos-ui-extensibility.d.ts new file mode 100644 index 0000000..cd5e498 --- /dev/null +++ b/Resources/Private/Neos.Ui/src/neos-ui-extensibility.d.ts @@ -0,0 +1,13 @@ +declare module '@neos-project/neos-ui-extensibility' { + type ManifestCallback = (globalRegistry: any) => void; + function manifest(identifier: string, options: Record, callback: ManifestCallback): void; + export default manifest; +} + +declare module '@neos-project/neos-ui-redux-store' { + export const actionTypes: any; +} + +declare module 'redux-saga/effects' { + export function take(pattern: string): any; +} diff --git a/Resources/Private/Neos.Ui/src/reloadOnAuthTimeoutSaga.ts b/Resources/Private/Neos.Ui/src/reloadOnAuthTimeoutSaga.ts new file mode 100644 index 0000000..b2d5a0f --- /dev/null +++ b/Resources/Private/Neos.Ui/src/reloadOnAuthTimeoutSaga.ts @@ -0,0 +1,19 @@ +import { take } from 'redux-saga/effects'; +import { actionTypes } from '@neos-project/neos-ui-redux-store'; + +/** + * Reloads the page when the Neos backend session times out. + * + * Neos core dispatches `@neos/neos-ui/System/AUTHENTICATION_TIMEOUT` on a 401 — + * the very same action that triggers the core ReloginDialog. For accounts with + * a second factor the client-side relogin cannot complete the 2FA step, so + * instead of relying on the dialog we reload the page and let the server-side + * login flow (which handles 2FA) take over. + * + * Uses the exported `actionTypes` constant rather than the literal string so we + * stay bound to the core contract. The action type is identical in Neos 8 and 9. + */ +export default function* reloadOnAuthTimeout() { + yield take(actionTypes.System.AUTHENTICATION_TIMEOUT); + window.location.reload(); +} diff --git a/Resources/Private/Neos.Ui/tsconfig.json b/Resources/Private/Neos.Ui/tsconfig.json new file mode 100644 index 0000000..0be68d0 --- /dev/null +++ b/Resources/Private/Neos.Ui/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "outDir": "dist", + "rootDir": "src", + "lib": ["ES2020", "DOM"] + }, + "include": ["src"] +} diff --git a/Resources/Public/Neos.Ui/Plugin.js b/Resources/Public/Neos.Ui/Plugin.js new file mode 100644 index 0000000..b3e7a72 --- /dev/null +++ b/Resources/Public/Neos.Ui/Plugin.js @@ -0,0 +1,94 @@ +"use strict"; +(() => { + var __create = Object.create; + var __defProp = Object.defineProperty; + var __getOwnPropDesc = Object.getOwnPropertyDescriptor; + var __getOwnPropNames = Object.getOwnPropertyNames; + var __getProtoOf = Object.getPrototypeOf; + var __hasOwnProp = Object.prototype.hasOwnProperty; + var __esm = (fn, res, err) => function __init() { + if (err) throw err[0]; + try { + return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; + } catch (e) { + throw err = [e], e; + } + }; + var __commonJS = (cb, mod) => function __require() { + try { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; + } catch (e) { + throw mod = 0, e; + } + }; + var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; + }; + var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod + )); + + // node_modules/@neos-project/neos-ui-extensibility/dist/readFromConsumerApi.js + function readFromConsumerApi(key) { + return (...args) => { + if (window["@Neos:HostPluginAPI"] && window["@Neos:HostPluginAPI"][`@${key}`]) { + return window["@Neos:HostPluginAPI"][`@${key}`](...args); + } + throw new Error("You are trying to read from a consumer api that hasn't been initialized yet!"); + }; + } + var init_readFromConsumerApi = __esm({ + "node_modules/@neos-project/neos-ui-extensibility/dist/readFromConsumerApi.js"() { + } + }); + + // node_modules/@neos-project/neos-ui-extensibility/dist/shims/vendor/redux-saga-effects/index.js + var require_redux_saga_effects = __commonJS({ + "node_modules/@neos-project/neos-ui-extensibility/dist/shims/vendor/redux-saga-effects/index.js"(exports, module) { + init_readFromConsumerApi(); + module.exports = readFromConsumerApi("vendor")().reduxSagaEffects; + } + }); + + // node_modules/@neos-project/neos-ui-extensibility/dist/shims/neosProjectPackages/neos-ui-redux-store/index.js + var require_neos_ui_redux_store = __commonJS({ + "node_modules/@neos-project/neos-ui-extensibility/dist/shims/neosProjectPackages/neos-ui-redux-store/index.js"(exports, module) { + init_readFromConsumerApi(); + module.exports = readFromConsumerApi("NeosProjectPackages")().NeosUiReduxStore; + } + }); + + // node_modules/@neos-project/neos-ui-extensibility/dist/index.js + init_readFromConsumerApi(); + var dist_default = readFromConsumerApi("manifest"); + + // src/reloadOnAuthTimeoutSaga.ts + var import_effects = __toESM(require_redux_saga_effects()); + var import_neos_ui_redux_store = __toESM(require_neos_ui_redux_store()); + function* reloadOnAuthTimeout() { + yield (0, import_effects.take)(import_neos_ui_redux_store.actionTypes.System.AUTHENTICATION_TIMEOUT); + window.location.reload(); + } + + // src/manifest.ts + dist_default("Sandstorm.NeosTwoFactorAuthentication:ReloadOnAuthTimeout", {}, (globalRegistry) => { + const sagasRegistry = globalRegistry.get("sagas"); + if (!sagasRegistry) { + console.error("[2FA] sagas registry not found; cannot register reload-on-auth-timeout saga"); + return; + } + sagasRegistry.set("Sandstorm.NeosTwoFactorAuthentication/reloadOnAuthTimeout", { + saga: reloadOnAuthTimeout + }); + }); +})(); diff --git a/Tests/E2E/features/default/login.feature b/Tests/E2E/features/default/login.feature index 4417729..db30201 100644 --- a/Tests/E2E/features/default/login.feature +++ b/Tests/E2E/features/default/login.feature @@ -95,6 +95,13 @@ Feature: Login flow with default settings And I enter a valid TOTP for device "Admin Test Device" Then I should land on "/neos/management/twoFactorAuthentication" + Scenario: The page reloads to the login screen when the backend session is destroyed server-side + When I log in with username "admin" and password "password" + And I should see the Neos content page + And the Neos backend session is destroyed on the server + And the Neos UI makes a backend request + Then I should see the login page + Scenario: User is redirected to the originally requested page after logging in with WebAuthn Given I have a virtual security key When I log in with username "admin" and password "password" diff --git a/Tests/E2E/helpers/system.ts b/Tests/E2E/helpers/system.ts index 1c47385..b86bf68 100644 --- a/Tests/E2E/helpers/system.ts +++ b/Tests/E2E/helpers/system.ts @@ -21,3 +21,13 @@ export function removeAllUsers() { export async function logout(page: Page) { await page.context().request.post('/neos/logout'); } + +// Destroys ALL Neos/Flow sessions server-side by flushing the session caches. +// This is the server-side equivalent of every logged-in user's session timing +// out at once — the next authenticated backend request they make answers 401. +export function destroyAllSessions() { + execSync( + `docker exec -u www-data -w /app ${CONTAINER} bash -c "./flow flow:session:destroyAll"`, + { stdio: 'ignore', cwd: dirname('.') } + ) +} diff --git a/Tests/E2E/steps/session-timeout.steps.ts b/Tests/E2E/steps/session-timeout.steps.ts new file mode 100644 index 0000000..7fad34d --- /dev/null +++ b/Tests/E2E/steps/session-timeout.steps.ts @@ -0,0 +1,42 @@ +import { createBdd } from 'playwright-bdd'; +import { destroyAllSessions } from '../helpers/system.ts'; + +const { When } = createBdd(); + +When('the Neos backend session is destroyed on the server', async () => { + destroyAllSessions(); +}); + +When('the Neos UI makes a backend request', async ({ page }) => { + // Reproduce what the Neos UI does on any backend interaction: issue an + // authenticated AJAX request through the core's own fetchWithErrorHandling. + // With the session gone the firewall answers 401, the core dispatches + // @neos/neos-ui/System/AUTHENTICATION_TIMEOUT, and our reload saga then calls + // window.location.reload() — which, without a session, lands on the login page. + await page.waitForFunction( + () => + Boolean( + (window as any)['@Neos:HostPluginAPI']?.['@NeosProjectPackages']?.() + ?.NeosUiBackendConnector?.fetchWithErrorHandling, + ), + ); + + await page.evaluate(() => { + const { fetchWithErrorHandling } = (window as any)['@Neos:HostPluginAPI'][ + '@NeosProjectPackages' + ]().NeosUiBackendConnector; + + // The route exists in both Neos 8 and 9; an empty change set is harmless if + // it ever did reach the controller, but the firewall intercepts first. + void fetchWithErrorHandling.withCsrfToken((csrfToken: string) => ({ + url: '/neos/ui-services/change', + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + 'X-Flow-Csrftoken': csrfToken, + }, + body: JSON.stringify({ changes: [] }), + })); + }); +}); diff --git a/composer.json b/composer.json index 7b0bdbc..1ba3045 100644 --- a/composer.json +++ b/composer.json @@ -21,9 +21,6 @@ "chillerlan/php-qrcode": "^5.0", "web-auth/webauthn-lib": "^4.9.3" }, - "require-dev": { - "sandstorm/neos-init-e2e-tests-plugin": "@dev" - }, "autoload": { "psr-4": { "Sandstorm\\NeosTwoFactorAuthentication\\": "Classes/" @@ -55,8 +52,7 @@ }, "config": { "allow-plugins": { - "neos/composer-plugin": true, - "sandstorm/neos-init-e2e-tests-plugin": true + "neos/composer-plugin": true } } }