diff --git a/.drone.star b/.drone.star index 11612824154..d2debfed376 100644 --- a/.drone.star +++ b/.drone.star @@ -2019,7 +2019,7 @@ def setUpOauth2(forIntegrationApp, disableFileLocking): "make vendor", "cd %s || exit" % dir["server"], "php occ a:e oauth2", - "php occ oauth2:add-client Web Cxfj9F9ZZWQbQZps1E1M0BszMz6OOFq3lxjSuc8Uh4HLEYb9KIfyRMmgY5ibXXrU 930C6aA0U1VhM03IfNiheR2EwSzRi4hRSpcNqIhhbpeSGU6h38xssVfNcGP0sSwQ %s" % oidcURL, + "php occ oauth2:add-client Web Cxfj9F9ZZWQbQZps1E1M0BszMz6OOFq3lxjSuc8Uh4HLEYb9KIfyRMmgY5ibXXrU 930C6aA0U1VhM03IfNiheR2EwSzRi4hRSpcNqIhhbpeSGU6h38xssVfNcGP0sSwQ %s false true" % oidcURL, ] if (disableFileLocking): diff --git a/changelog/unreleased/enhancement-history-mode b/changelog/unreleased/enhancement-history-mode new file mode 100644 index 00000000000..4d4055291d4 --- /dev/null +++ b/changelog/unreleased/enhancement-history-mode @@ -0,0 +1,8 @@ +Enhancement: Option to enable Vue history mode + +We've added the option to use vue's history mode. All configuration is done automatically by the system. +To enable it, add a `` header tag to `index.html`, `oidc-callback.html` and `oidc-silent-redirect.html`. +Adding `` is not needed for ocis. + +https://github.com/owncloud/web/issues/6363 +https://github.com/owncloud/web/issues/6277 diff --git a/changelog/unreleased/enhancement-sidecar-mode b/changelog/unreleased/enhancement-sidecar-mode new file mode 100644 index 00000000000..4d7f41cc335 --- /dev/null +++ b/changelog/unreleased/enhancement-sidecar-mode @@ -0,0 +1,6 @@ +Enhancement: Run web as oc10 sidecar + +We've added the option to run web in oc10 sidecar mode. +Copy `config/config.json.sample-oc10` to `config/config.json`, run `yarn server` and then `docker compose up oc10`. + +https://github.com/owncloud/web/issues/6363 diff --git a/config/config.json.sample-oc10 b/config/config.json.sample-oc10 index 1a7bb458b3e..12987676929 100644 --- a/config/config.json.sample-oc10 +++ b/config/config.json.sample-oc10 @@ -3,7 +3,7 @@ "theme": "https://localhost:9100/themes/owncloud/theme.json", "version": "0.1.0", "auth": { - "clientId": "", + "clientId": "UmCVsEIxdWmssxa6uVRRPC3txYBVN4qqxooJbsPhuuoPmHk9Pt9Oy68N4ZaKXUYy", "url": "http://localhost:8080/index.php/apps/oauth2/api/v1/token", "authUrl": "http://localhost:8080/index.php/apps/oauth2/authorize" }, diff --git a/dev/docker/oc10.Dockerfile b/dev/docker/oc10.Dockerfile new file mode 100644 index 00000000000..d5bb584d1e3 --- /dev/null +++ b/dev/docker/oc10.Dockerfile @@ -0,0 +1,8 @@ +ARG OC10_IMAGE +FROM ${OC10_IMAGE} + +RUN apt -qqy update \ + && apt -qqy --no-install-recommends install \ + bash \ + && rm -rf /var/lib/apt/lists/* \ + && apt -qyy clean diff --git a/dev/docker/oc10.entrypoint.sh b/dev/docker/oc10.entrypoint.sh index 469a88574b3..abc121f406a 100755 --- a/dev/docker/oc10.entrypoint.sh +++ b/dev/docker/oc10.entrypoint.sh @@ -16,14 +16,28 @@ done if [ ! -d /mnt/data/apps/testing ] then + # testing app git clone https://github.com/owncloud/testing.git /mnt/data/apps/testing - occ app:enable oauth2 occ app:enable testing + # oauth2 app + rm -rf /var/www/owncloud/apps/oauth2 + git clone https://github.com/owncloud/oauth2.git /var/www/owncloud/apps/oauth2 + make -C /var/www/owncloud/apps/oauth2 vendor + occ app:enable oauth2 occ oauth2:add-client \ web \ M8W5mo3wQV3VHWYsaYpWhkr8dwa949i4GljCkedHhl7GWqmHMkxSeJgK2PcS0jt5 \ sqvPYXK94tMsEEVOYORxg8Ufesi2kC4WpJJSYb0Kj1DSAYl6u2XvJZjc3VcitjDv \ - http://host.docker.internal:8080/index.php/apps/web/oidc-callback.html + http://host.docker.internal:8080/index.php/apps/web/oidc-callback.html \ + false \ + true + occ oauth2:add-client \ + web-sidecar \ + UmCVsEIxdWmssxa6uVRRPC3txYBVN4qqxooJbsPhuuoPmHk9Pt9Oy68N4ZaKXUYy \ + HW1fo6lbtgEERBQufBouJ4HID2QaDfngvIdc2vjDUE46qKB4JRG1YDir41LliReC \ + http://localhost:9100/oidc-callback.html + occ config:system:set trusted_domains 0 --value="localhost" + occ config:system:set cors.allowed-domains 0 --value="http://localhost:9100" fi if [ -d /var/www/owncloud/apps/web/ ] diff --git a/docker-compose.yml b/docker-compose.yml index 640cfba51a1..e0f2797f308 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,7 +22,11 @@ services: - host.docker.internal:${DOCKER_HOST:-host-gateway} oc10: - image: ${OC10_IMAGE:-owncloud/server:latest} + build: + dockerfile: oc10.Dockerfile + context: ./dev/docker + args: + OC10_IMAGE: ${OC10_IMAGE:-owncloud/server:latest} container_name: web_oc10 ports: - 8080:8080 diff --git a/packages/web-container/oidc-callback.html b/packages/web-container/oidc-callback.html index 06aa86652d4..ee4ee43141f 100644 --- a/packages/web-container/oidc-callback.html +++ b/packages/web-container/oidc-callback.html @@ -1,10 +1,13 @@ + + diff --git a/packages/web-container/oidc-silent-redirect.html b/packages/web-container/oidc-silent-redirect.html index 8c83b021f96..fad53492f8d 100644 --- a/packages/web-container/oidc-silent-redirect.html +++ b/packages/web-container/oidc-silent-redirect.html @@ -1,10 +1,13 @@ + + diff --git a/packages/web-runtime/src/defaults/index.ts b/packages/web-runtime/src/defaults/index.ts index 0f7ed51fafa..9f57ed8e629 100644 --- a/packages/web-runtime/src/defaults/index.ts +++ b/packages/web-runtime/src/defaults/index.ts @@ -12,7 +12,7 @@ import '@fortawesome/fontawesome-free/attribution' export { default as Vue } from './vue' export { default as DesignSystem } from 'owncloud-design-system' -export { default as Router } from '../router' + export const store = createStore(Vuex.Store, { ...Store }) export const pages = { success: App, failure: missingOrInvalidConfigPage } export const translations = merge({}, coreTranslations, odsTranslations) diff --git a/packages/web-runtime/src/index.ts b/packages/web-runtime/src/index.ts index a753e17a530..89e8c7477db 100644 --- a/packages/web-runtime/src/index.ts +++ b/packages/web-runtime/src/index.ts @@ -4,9 +4,11 @@ import { translations, supportedLanguages, store, - Router as router, Vue } from './defaults' + +import { router } from './router' + import { requestConfiguration, announceApplications, diff --git a/packages/web-runtime/src/router/index.js b/packages/web-runtime/src/router/index.js index 51f11047481..ac969f0722b 100644 --- a/packages/web-runtime/src/router/index.js +++ b/packages/web-runtime/src/router/index.js @@ -10,67 +10,131 @@ import Account from '../pages/account.vue' Vue.use(Router) +// type: patch +// temporary patch till we have upgraded web to the latest vue router which make this obsolete +// this takes care that routes like 'foo/bar/baz' which by default would be converted to 'foo%2Fbar%2Fbaz' stay as they are +// should immediately go away and be removed after finalizing the update +// to apply the patch to a route add meta.patchCleanPath = true to it +// to patch needs to be enabled on a route level, to do so add meta.patchCleanPath = true property to the route +const patchRouter = (router) => { + const bindMatcher = router.match.bind(router) + const cleanPath = (route) => + [ + ['%2F', '/'], + ['//', '/'] + ].reduce((path, rule) => path.replaceAll(rule[0], rule[1]), route || '') + + router.match = (raw, current, redirectFrom) => { + const bindMatch = bindMatcher(raw, current, redirectFrom) + + if (!get(bindMatch, 'meta.patchCleanPath', false)) { + return bindMatch + } + + return { + ...bindMatch, + path: cleanPath(bindMatch.path), + fullPath: cleanPath(bindMatch.fullPath) + } + } + + return router +} + // just a dummy function to trick gettext tools function $gettext(msg) { return msg } -const router = new Router({ - // mode: 'history', - parseQuery(query) { - return qs.parse(query) - }, - stringifyQuery(obj) { - return qs.stringify(obj, { - allowDots: true, - addQueryPrefix: true - }) - }, - routes: [ - { - path: '/login', - name: 'login', - component: LoginPage, - meta: { auth: false, title: $gettext('Login') } - }, - { - path: '/oidc-callback', - name: 'oidcCallback', - component: OidcCallbackPage, - meta: { auth: false, title: $gettext('Oidc callback') } - }, - { - path: '/oidc-silent-redirect', - name: 'oidcSilentRedirect', - component: OidcCallbackPage, - meta: { auth: false, title: $gettext('Oidc redirect') } - }, - { - path: '/f/:fileId', - name: 'privateLink', - redirect: '/files/ops/resolver/private-link/:fileId', - meta: { title: $gettext('Private link') } - }, - { - path: '/s/:token', - name: 'publicLink', - redirect: '/files/ops/resolver/public-link/:token', - meta: { auth: false, title: $gettext('Public link') } +const base = document.querySelector('base') +export const router = patchRouter( + new Router({ + parseQuery(query) { + return qs.parse(query) }, - { - path: '/access-denied', - name: 'accessDenied', - component: AccessDeniedPage, - meta: { auth: false, title: $gettext('Access denied') } + stringifyQuery(obj) { + return qs.stringify(obj, { + allowDots: true, + addQueryPrefix: true + }) }, - { - path: '/account', - name: 'account', - component: Account, - meta: { title: $gettext('Account') } + ...(base && { + mode: 'history', + base: new URL(base.href).pathname + }), + routes: [ + { + path: '/login', + name: 'login', + component: LoginPage, + meta: { auth: false, title: $gettext('Login') } + }, + { + path: '/oidc-callback', + component: OidcCallbackPage, + meta: { auth: false, title: $gettext('Oidc callback') } + }, + { + path: '/oidc-silent-redirect', + component: OidcCallbackPage, + meta: { auth: false, title: $gettext('Oidc redirect') } + }, + { + path: '/f/:fileId', + name: 'privateLink', + redirect: '/files/ops/resolver/private-link/:fileId', + meta: { title: $gettext('Private link') } + }, + { + path: '/s/:token', + name: 'publicLink', + redirect: '/files/ops/resolver/public-link/:token', + meta: { auth: false, title: $gettext('Public link') } + }, + { + path: '/access-denied', + name: 'accessDenied', + component: AccessDeniedPage, + meta: { auth: false, title: $gettext('Access denied') } + }, + { + path: '/account', + name: 'account', + component: Account, + meta: { title: $gettext('Account') } + } + ] + }) +) + +export const buildUrl = (pathname) => { + const isHistoryMode = !!base + const baseUrl = new URL(window.location.href.split('#')[0]) + if (isHistoryMode) { + // in history mode we can't determine the base path, it must be provided by the document + baseUrl.pathname = new URL(base.href).pathname + } else { + // in hash mode, auto-determine the base path by removing `/index.html` + if (baseUrl.pathname.endsWith('/index.html')) { + baseUrl.pathname = baseUrl.pathname.split('/').slice(0, -1).filter(Boolean).join('/') } - ] -}) + } + + /** + * build full url by either + * - concatenating baseUrl and pathname (for unknown/non-router urls, e.g. `oidc-callback.html`) or + * - resolving via router (for known routes) + */ + if (/\.(html?)$/i.test(pathname)) { + baseUrl.pathname = [...baseUrl.pathname.split('/'), ...pathname.split('/')] + .filter(Boolean) + .join('/') + } else { + baseUrl[isHistoryMode ? 'pathname' : 'hash'] = router.resolve(pathname).href + } + + return baseUrl.href +} router.beforeEach(function (to, from, next) { const store = Vue.$store @@ -119,36 +183,3 @@ const isAuthRequired = (router, to) => { } return false } - -// type: patch -// temporary patch till we have upgraded web to the latest vue router which make this obsolete -// this takes care that routes like 'foo/bar/baz' which by default would be converted to 'foo%2Fbar%2Fbaz' stay as they are -// should immediately go away and be removed after finalizing the update -// to apply the patch to a route add meta.patchCleanPath = true to it -// to patch needs to be enabled on a route level, to do so add meta.patchCleanPath = true property to the route -const patchRouter = (router) => { - const bindMatcher = router.match.bind(router) - const cleanPath = (route) => - [ - ['%2F', '/'], - ['//', '/'] - ].reduce((path, rule) => path.replaceAll(rule[0], rule[1]), route || '') - - router.match = (raw, current, redirectFrom) => { - const bindMatch = bindMatcher(raw, current, redirectFrom) - - if (!get(bindMatch, 'meta.patchCleanPath', false)) { - return bindMatch - } - - return { - ...bindMatch, - path: cleanPath(bindMatch.path), - fullPath: cleanPath(bindMatch.fullPath) - } - } - - return router -} - -export default patchRouter(router) diff --git a/packages/web-runtime/src/services/auth.js b/packages/web-runtime/src/services/auth.js index 34a01175a7b..b3cf48d024b 100644 --- a/packages/web-runtime/src/services/auth.js +++ b/packages/web-runtime/src/services/auth.js @@ -1,4 +1,5 @@ import { Log, User, UserManager, WebStorageStateStore } from 'oidc-client' +import { buildUrl } from '../router' export function initVueAuthenticate(config) { if (config) { @@ -6,21 +7,17 @@ export function initVueAuthenticate(config) { prefix: 'oc_oAuth', store: sessionStorage }) - let baseUrl = window.location.href.split('#')[0] - if (baseUrl.endsWith('/index.html')) { - baseUrl = baseUrl.substr(0, baseUrl.length - 10) - } const openIdConfig = { userStore: store, - redirect_uri: baseUrl + 'oidc-callback.html', + redirect_uri: buildUrl('/oidc-callback.html'), response_type: 'code', // code triggers auth code grant flow response_mode: 'query', scope: 'openid profile offline_access', monitorSession: false, // set uri directly to the login route to prevent problems with query parameters. // See https://github.com/owncloud/web/issues/3285 - post_logout_redirect_uri: baseUrl + '#/login', - silent_redirect_uri: baseUrl + 'oidc-silent-redirect.html', + post_logout_redirect_uri: buildUrl('/login'), + silent_redirect_uri: buildUrl('/oidc-silent-redirect.html'), accessTokenExpiringNotificationTime: 10, automaticSilentRenew: true, filterProtocolClaims: true, diff --git a/packages/web-runtime/src/services/clientRegistration.js b/packages/web-runtime/src/services/clientRegistration.js index 9cd139bf853..61b545cbb56 100644 --- a/packages/web-runtime/src/services/clientRegistration.js +++ b/packages/web-runtime/src/services/clientRegistration.js @@ -1,3 +1,5 @@ +import { buildUrl } from '../router' + async function get(url) { return await fetch(url) .then((res) => { @@ -35,16 +37,10 @@ export async function registerClient(openIdConfig) { } } sessionStorage.removeItem('dynamicClientData') - - let baseUrl = window.location.href.split('#')[0] - if (baseUrl.endsWith('/index.html')) { - baseUrl = baseUrl.substr(0, baseUrl.length - 10) - } - const wellKnown = await get(`${openIdConfig.authority}/.well-known/openid-configuration`) const resp = await post(wellKnown.registration_endpoint, { - redirect_uris: [baseUrl + 'oidc-callback.html'], - client_name: `ownCloud Web on ${baseUrl}` + redirect_uris: [buildUrl('/oidc-callback.html')], + client_name: `ownCloud Web on ${window.location.origin}` }) sessionStorage.setItem('dynamicClientData', JSON.stringify(resp)) return resp diff --git a/packages/web-runtime/src/store/user.js b/packages/web-runtime/src/store/user.js index 720abc61a15..24e0f50a893 100644 --- a/packages/web-runtime/src/store/user.js +++ b/packages/web-runtime/src/store/user.js @@ -1,7 +1,7 @@ import get from 'lodash-es/get.js' import isEmpty from 'lodash-es/isEmpty' import initVueAuthenticate from '../services/auth' -import router from '../router/' +import { router } from '../router' let vueAuthInstance diff --git a/packages/web-runtime/tests/unit/router/index.spec.ts b/packages/web-runtime/tests/unit/router/index.spec.ts new file mode 100644 index 00000000000..f8281a63224 --- /dev/null +++ b/packages/web-runtime/tests/unit/router/index.spec.ts @@ -0,0 +1,33 @@ +describe('buildUrl', () => { + it.each` + location | base | path | expected + ${'https://localhost:8080/index.php/apps/web/index.html#/files/list/all'} | ${''} | ${'/login'} | ${'https://localhost:8080/index.php/apps/web#/login'} + ${'https://localhost:8080/index.php/apps/web/index.html#/files/list/all'} | ${''} | ${'/login/foo'} | ${'https://localhost:8080/index.php/apps/web#/login/foo'} + ${'https://localhost:8080/////index.php/apps/web/////index.html#/files/list/all'} | ${''} | ${'/login/foo'} | ${'https://localhost:8080/index.php/apps/web#/login/foo'} + ${'https://localhost:8080/index.php/apps/web/#/login'} | ${''} | ${'/bar.html'} | ${'https://localhost:8080/index.php/apps/web/bar.html'} + ${'https://localhost:8080/index.php/apps/web/#/login'} | ${'/index.php/apps/web/foo'} | ${'/bar'} | ${'https://localhost:8080/index.php/apps/web/foo/bar'} + ${'https://localhost:8080/index.php/apps/web/#/login'} | ${'/index.php/apps/web/foo'} | ${'/bar.html'} | ${'https://localhost:8080/index.php/apps/web/foo/bar.html'} + ${'https://localhost:9200/#/files/list/all'} | ${''} | ${'/login/foo'} | ${'https://localhost:9200/#/login/foo'} + ${'https://localhost:9200/#/files/list/all'} | ${''} | ${'/bar.html'} | ${'https://localhost:9200/bar.html'} + ${'https://localhost:9200/files/list/all'} | ${'/'} | ${'/login/foo'} | ${'https://localhost:9200/login/foo'} + ${'https://localhost:9200/files/list/all'} | ${'/foo'} | ${'/login/foo'} | ${'https://localhost:9200/foo/login/foo'} + ${'https://localhost:9200/files/list/all'} | ${'/'} | ${'/bar.html'} | ${'https://localhost:9200/bar.html'} + ${'https://localhost:9200/files/list/all'} | ${'/foo'} | ${'/bar.html'} | ${'https://localhost:9200/foo/bar.html'} + `('$path -> $expected', async ({ location, base, path, expected }) => { + delete window.location + window.location = new URL(location) as any + + document.querySelectorAll('base').forEach((e) => e.remove()) + + if (base) { + const baseElement = document.createElement('base') + baseElement.href = base + document.getElementsByTagName('head')[0].appendChild(baseElement) + } + + const { buildUrl } = await import('../../../src/router') + jest.resetModules() + + expect(buildUrl(path)).toBe(expected) + }) +}) diff --git a/tests/acceptance/features/webUILogin/oauthLogin.feature b/tests/acceptance/features/webUILogin/oauthLogin.feature index dbb5f7eb3a6..0f2454fc4e7 100644 --- a/tests/acceptance/features/webUILogin/oauthLogin.feature +++ b/tests/acceptance/features/webUILogin/oauthLogin.feature @@ -15,7 +15,6 @@ Feature: login users Scenario: admin login Given the user has browsed to the login page When the user logs in with username "admin" and password "admin" using the webUI - And the user authorizes access to web Then the files table should be displayed And the files table should not be empty diff --git a/tests/acceptance/helpers/loginHelper.js b/tests/acceptance/helpers/loginHelper.js index dbc97a9d5da..4e5cfd60ac5 100644 --- a/tests/acceptance/helpers/loginHelper.js +++ b/tests/acceptance/helpers/loginHelper.js @@ -21,7 +21,6 @@ module.exports = { await client.page.ocisLoginPage().login(userId, password) } else { await client.page.ownCloudLoginPage().login(userId, password) - await client.page.ownCloudAuthorizePage().authorize() } await client.page .webPage() diff --git a/tests/acceptance/pageObjects/accountPage.js b/tests/acceptance/pageObjects/accountPage.js index 25dfdac3559..b5712bfe598 100644 --- a/tests/acceptance/pageObjects/accountPage.js +++ b/tests/acceptance/pageObjects/accountPage.js @@ -2,7 +2,7 @@ const { join } = require('../helpers/path') module.exports = { url: function () { - return join(this.api.launchUrl, '/index.html#/account/') + return join(this.api.launchUrl, '/#/account/') }, commands: { /** diff --git a/tests/acceptance/pageObjects/ownCloudAuthorizePage.js b/tests/acceptance/pageObjects/ownCloudAuthorizePage.js deleted file mode 100644 index 1eece00b57c..00000000000 --- a/tests/acceptance/pageObjects/ownCloudAuthorizePage.js +++ /dev/null @@ -1,57 +0,0 @@ -module.exports = { - url: function () { - return this.api.launchUrl - }, - elements: { - body: { - selector: '#body-login' - }, - authorizeButton: { - selector: 'button[type=submit]' - }, - inputUsername: { - selector: '//input[@id="user"]', - locateStrategy: 'xpath' - } - }, - commands: [ - { - authorize: function () { - return this.waitForElementVisible('@authorizeButton') - .click('@authorizeButton') - .waitForElementNotPresent( - { - selector: '@authorizeButton', - abortOnFailure: false - }, - (result) => { - if (result.value.length > 0) { - // click failed - console.log( - "WARNING: looks like I'm still on auth page. " + - 'I will click the auth button again' - ) - this.click('@authorizeButton').waitForElementNotPresent('@authorizeButton') - } - } - ) - }, - waitForPage: async function () { - let isLoginPageVisible = false - - await this.waitForElementVisible('@body') - await this.api.element('@inputUsername', (result) => { - if (result.status > -1) { - isLoginPageVisible = true - } - }) - - if (isLoginPageVisible) { - return this.assert.ok(isLoginPageVisible) - } - - return this.api.expect.element(this.elements.authorizeButton.selector).to.be.visible - } - } - ] -} diff --git a/tests/acceptance/pageObjects/webPage.js b/tests/acceptance/pageObjects/webPage.js index bc0f278ba27..91f8d6eb3e3 100644 --- a/tests/acceptance/pageObjects/webPage.js +++ b/tests/acceptance/pageObjects/webPage.js @@ -311,7 +311,7 @@ module.exports = { selector: '#web' }, appContainer: { - selector: '.app-content' + selector: '#files-view' }, notificationElement: { selector: '//div[@id="oc-notification"]//h4', diff --git a/tests/acceptance/stepDefinitions/loginContext.js b/tests/acceptance/stepDefinitions/loginContext.js index 812269815ed..4c60f33b04d 100644 --- a/tests/acceptance/stepDefinitions/loginContext.js +++ b/tests/acceptance/stepDefinitions/loginContext.js @@ -7,8 +7,6 @@ Given(/^the user has browsed to the login page$/, () => { return client.page.loginPage().navigate() }) -Given('the user has clicked the authenticate button', () => client.page.loginPage().authenticate()) - When('the user clicks the authenticate button', () => client.page.loginPage().authenticate()) When( @@ -33,10 +31,6 @@ When( When('user {string} logs in using the webUI', (username) => loginHelper.loginAsUser(username)) -When('the user authorizes access to web', () => { - return client.page.ownCloudAuthorizePage().authorize() -}) - Then('the files table should not be empty', () => { return ( client.page.FilesPageElement.filesList() @@ -110,11 +104,7 @@ Then('the user should be redirected to the user disabled page', function () { }) Then('the user should be redirected to the IdP login page', function () { - if (client.globals.openid_login) { - return client.page.ocisLoginPage().waitForPage() - } - - return client.page.ownCloudAuthorizePage().waitForPage() + return client.page.ocisLoginPage().waitForPage() }) Then('the user should be redirected to the login error page', function () {