From aaaa4a9a70042ce4db0074d4040be401f71ca80a Mon Sep 17 00:00:00 2001 From: Florian Schade Date: Tue, 1 Feb 2022 04:06:56 +0100 Subject: [PATCH] add option to use vues history mode --- changelog/unreleased/enhancement-history-mode | 8 + changelog/unreleased/enhancement-sidecar-mode | 6 + config/config.json.sample-oc10 | 2 +- dev/docker/oc10.Dockerfile | 8 + dev/docker/oc10.entrypoint.sh | 7 + docker-compose.yml | 6 +- packages/web-container/oidc-callback.html | 13 +- .../web-container/oidc-silent-redirect.html | 13 +- packages/web-runtime/src/defaults/index.ts | 1 - packages/web-runtime/src/index.ts | 4 +- packages/web-runtime/src/router/index.js | 187 ++++++++++-------- packages/web-runtime/src/services/auth.js | 11 +- .../src/services/clientRegistration.js | 12 +- packages/web-runtime/src/store/user.js | 2 +- .../tests/unit/router/index.spec.ts | 29 +++ 15 files changed, 196 insertions(+), 113 deletions(-) create mode 100644 changelog/unreleased/enhancement-history-mode create mode 100644 changelog/unreleased/enhancement-sidecar-mode create mode 100644 dev/docker/oc10.Dockerfile create mode 100644 packages/web-runtime/tests/unit/router/index.spec.ts 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..8f897df99cc 100755 --- a/dev/docker/oc10.entrypoint.sh +++ b/dev/docker/oc10.entrypoint.sh @@ -24,6 +24,13 @@ then M8W5mo3wQV3VHWYsaYpWhkr8dwa949i4GljCkedHhl7GWqmHMkxSeJgK2PcS0jt5 \ sqvPYXK94tMsEEVOYORxg8Ufesi2kC4WpJJSYb0Kj1DSAYl6u2XvJZjc3VcitjDv \ http://host.docker.internal:8080/index.php/apps/web/oidc-callback.html + 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 cd785b1a2a9..3aa89859f1a 100644 --- a/packages/web-runtime/src/defaults/index.ts +++ b/packages/web-runtime/src/defaults/index.ts @@ -8,7 +8,6 @@ import { createStore } from 'vuex-extensions' import Vuex from 'vuex' 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 } 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 3cb6223242d..1c6dc4c9f8e 100644 --- a/packages/web-runtime/src/router/index.js +++ b/packages/web-runtime/src/router/index.js @@ -9,66 +9,120 @@ 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', - routes: [ - { - path: '/login', - name: 'login', - components: { - fullscreen: LoginPage +const base = document.querySelector('base') +export const router = patchRouter( + new Router({ + ...(base && { + mode: 'history', + base: new URL(base.href).pathname + }), + routes: [ + { + path: '/login', + name: 'login', + components: { + fullscreen: LoginPage + }, + meta: { auth: false, hideHeadbar: true, title: $gettext('Login') } }, - meta: { auth: false, hideHeadbar: true, title: $gettext('Login') } - }, - { - path: '/oidc-callback', - components: { - fullscreen: OidcCallbackPage + { + path: '/oidc-callback', + components: { + fullscreen: OidcCallbackPage + }, + meta: { auth: false, hideHeadbar: true, title: $gettext('Oidc callback') } }, - meta: { auth: false, hideHeadbar: true, title: $gettext('Oidc callback') } - }, - { - path: '/oidc-silent-redirect', - components: { - fullscreen: OidcCallbackPage + { + path: '/oidc-silent-redirect', + components: { + fullscreen: OidcCallbackPage + }, + meta: { auth: false, hideHeadbar: true, title: $gettext('Oidc redirect') } }, - meta: { auth: false, hideHeadbar: true, title: $gettext('Oidc redirect') } - }, - { - path: '/f/:fileId', - name: 'privateLink', - redirect: '/files/ops/resolver/private-link/:fileId', - meta: { hideHeadbar: true, title: $gettext('Private link') } - }, - { - path: '/s/:token', - name: 'publicLink', - redirect: '/files/ops/resolver/public-link/:token', - meta: { auth: false, hideHeadbar: true, title: $gettext('Public link') } - }, - { - path: '/access-denied', - name: 'accessDenied', - components: { - fullscreen: AccessDeniedPage + { + path: '/f/:fileId', + name: 'privateLink', + redirect: '/files/ops/resolver/private-link/:fileId', + meta: { hideHeadbar: true, title: $gettext('Private link') } }, - meta: { auth: false, hideHeadbar: true, title: $gettext('Access denied') } - }, - { - path: '/account', - name: 'account', - components: { - app: Account + { + path: '/s/:token', + name: 'publicLink', + redirect: '/files/ops/resolver/public-link/:token', + meta: { auth: false, hideHeadbar: true, title: $gettext('Public link') } }, - meta: { title: $gettext('Account') } - } - ] -}) + { + path: '/access-denied', + name: 'accessDenied', + components: { + fullscreen: AccessDeniedPage + }, + meta: { auth: false, hideHeadbar: true, title: $gettext('Access denied') } + }, + { + path: '/account', + name: 'account', + components: { + app: Account + }, + meta: { title: $gettext('Account') } + } + ] + }) +) + +export const buildUrl = (pathname) => { + const baseUrl = new URL(window.location.href.split('#')[0]) + if (baseUrl.pathname.endsWith('/index.html')) { + baseUrl.pathname = baseUrl.pathname.substr(0, baseUrl.pathname.length - 11) + } + + if (/\.(html?)$/i.test(pathname)) { + baseUrl.pathname = base + ? pathname + : [...baseUrl.pathname.split('/'), ...pathname.split('/')].filter(Boolean).join('/') + } else { + baseUrl[base ? 'pathname' : 'hash'] = router.resolve(pathname).href + } + + return baseUrl.href +} router.beforeEach(function (to, from, next) { const store = Vue.$store @@ -117,36 +171,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..8b55eb6d126 --- /dev/null +++ b/packages/web-runtime/tests/unit/router/index.spec.ts @@ -0,0 +1,29 @@ +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/#/login'} | ${''} | ${'/bar.html'} | ${'https://localhost:8080/index.php/apps/web/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'} | ${'/bar.html'} | ${'https://localhost:9200/bar.html'} + ${'https://localhost:9200/files/list/all'} | ${'/foo'} | ${'/bar.htm'} | ${'https://localhost:9200/bar.htm'} + `('$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) + }) +})