Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
} from '@nextcloud/l10n'
import { generateUrl } from '@nextcloud/router'
import { createApp, h } from 'vue'
import { createRouter, createWebHashHistory } from 'vue-router'
import { createRouter, createWebHistory } from 'vue-router'
import App from './App.vue'
import { ensureSkipActionsTarget } from './bootstrap/skip-actions.js'
import appIcons from './icons.js'
Expand Down Expand Up @@ -133,8 +133,37 @@ function routesFromManifest(manifest) {
return routes
}

/**
* The router base for THIS page load.
*
* ⚠️ `generateUrl('/apps/keepiq')` alone is not enough. Nextcloud serves the
* app under BOTH `/apps/keepiq/...` and `/index.php/apps/keepiq/...`, but
* `generateUrl()` returns only the form the instance is configured for. A
* visitor arriving on the other form — a bookmark, an emailed deep link, an
* integration that hardcodes `/index.php` — falls outside the router base,
* vue-router cannot resolve the path, and the catch-all redirects to `/`. They
* land on the dashboard with no error: the deep link is silently swallowed.
*
* Hash routing never had this, because the route travelled in the fragment and
* the path prefix was irrelevant. This app's own e2e helpers use the
* `/index.php` form (`APP_BASE = '/index.php/apps/keepiq'`), so without this
* every deep link the suite makes would break.
*
* It matters more here than elsewhere: keepiq's share and send links
* (`/share/link/:token`, `/send/:token`, `/share/request/:token`) are handed to
* people OUTSIDE the app, in messages and emails, in whichever URL shape the
* sender's client produced. A swallowed deep link there is a share that
* silently does nothing.
*
* @return {string} The base path vue-router should strip from the URL.
*/
function routerBase() {
const match = window.location.pathname.match(/^(.*\/apps\/keepiq)(?:\/|$)/)
return match ? match[1] : generateUrl('/apps/keepiq')
}

const router = createRouter({
history: createWebHashHistory(generateUrl('/apps/keepiq')),
history: createWebHistory(routerBase()),
routes: routesFromManifest(mergedManifest),
})

Expand Down
4 changes: 2 additions & 2 deletions tests/e2e/spec-coverage/navigation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ test.describe('App navigation — manifest menu', () => {
// The Dashboard entry points at the keepiq app root (not /apps/dashboard).
// Matched on the hash SUFFIX, not the whole href: under vue-router 4 the
// hash-history links render relative (`#/`) rather than carrying the
// absolute app base (`/apps/keepiq/#/`). Both resolve to the same route
// absolute app base (`/apps/keepiq/`). Both resolve to the same route
// from any keepiq page; asserting the literal absolute form would pin a
// router implementation detail rather than the requirement.
await expect(nav.locator('a[href$="#/"]').first()).toBeVisible()
Expand All @@ -93,7 +93,7 @@ test.describe('App navigation — manifest menu', () => {
}) => {
await unlockVault(page)

// "Lock vault" is a keepiq-owned route (/apps/keepiq/#/lock) in the
// "Lock vault" is a keepiq-owned route (/apps/keepiq/lock) in the
// settings foldout — expand it and click the entry. App.vue's $route
// watcher calls session.lock() on entering /lock while unlocked, so
// this drives the real re-lock flow end to end.
Expand Down
6 changes: 3 additions & 3 deletions tests/e2e/visual/keepiq-pages.visual.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ test.describe('Keepiq — routed page baselines', () => {
(r: any) => r.status === 'pending',
)
expect(pending, 'no pending secret request to fill').toBeTruthy()
await page.goto(`${APP_BASE}/#/share/request/${pending.token}`, {
await page.goto(`${APP_BASE}/share/request/${pending.token}`, {
waitUntil: 'domcontentloaded',
})
await shootComponent(page, 'secret-request-fill', 'secret-request-fill.png')
Expand Down Expand Up @@ -205,7 +205,7 @@ test.describe('Keepiq — routed page baselines', () => {
).toBeLessThan(300)
const token = created.json.token ?? created.json.data?.token
expect(token, 'the created link share carries no token').toBeTruthy()
await page.goto(`${APP_BASE}/#/share/link/${token}`, {
await page.goto(`${APP_BASE}/share/link/${token}`, {
waitUntil: 'domcontentloaded',
})
await shootComponent(page, 'link-share-access', 'link-share-access.png')
Expand All @@ -228,7 +228,7 @@ test.describe('Keepiq — routed page baselines', () => {
).toBeLessThan(300)
const token = created.json.token ?? created.json.data?.token
expect(token, 'the created ephemeral send carries no token').toBeTruthy()
await page.goto(`${APP_BASE}/#/send/${token}`, {
await page.goto(`${APP_BASE}/send/${token}`, {
waitUntil: 'domcontentloaded',
})
await shootComponent(page, 'send-access-page', 'ephemeral-send-access.png')
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/visual/keepiq.visual.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ const APP = '/index.php/apps/keepiq'

test.describe('Keepiq — visual baselines', () => {
test('dashboard', async ({ page }) => {
await shootSurface(page, `${APP}/#/`, 'dashboard.png')
await shootSurface(page, `${APP}/`, 'dashboard.png')
})
})
4 changes: 2 additions & 2 deletions tests/e2e/workflows/compromise-recovery.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ async function clickByLabel(page: Page, label: string): Promise<void> {
* @return True when setup ran; false when the user already owned a suite.
*/
async function setUpVault(page: Page): Promise<boolean> {
await page.goto(`${APP_BASE}/#/lock`, { waitUntil: 'domcontentloaded' })
await page.goto(`${APP_BASE}/lock`, { waitUntil: 'domcontentloaded' })
await page
.locator('.lock-screen__card')
.waitFor({ state: 'visible', timeout: 30_000 })
Expand Down Expand Up @@ -410,7 +410,7 @@ test.describe('Workflow: compromise recovery — encryption-suites/spec.md', ()
// The negative case is the one that regresses quietly: a banner shown to
// everyone gets noticed immediately, one shown to nobody does not.
await loginAsVaultUser(page)
await page.goto(`${APP_BASE}/#/lock`, { waitUntil: 'domcontentloaded' })
await page.goto(`${APP_BASE}/lock`, { waitUntil: 'domcontentloaded' })
await page
.locator('.lock-screen__card')
.waitFor({ state: 'visible', timeout: 30_000 })
Expand Down
6 changes: 3 additions & 3 deletions tests/e2e/workflows/page-surfaces.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ test.describe('Routed page surfaces — public recipient routes', () => {
pending,
'no pending secret request — the dev seed is missing',
).toBeTruthy()
await page.goto(`${APP_BASE}/#/share/request/${pending.token}`, {
await page.goto(`${APP_BASE}/share/request/${pending.token}`, {
waitUntil: 'domcontentloaded',
})
await expect(
Expand Down Expand Up @@ -448,7 +448,7 @@ test.describe('Routed page surfaces — public recipient routes', () => {
).toBeLessThan(300)
const token = created.json.token ?? created.json.data?.token
expect(token, 'the created link share carries no token').toBeTruthy()
await page.goto(`${APP_BASE}/#/share/link/${token}`, {
await page.goto(`${APP_BASE}/share/link/${token}`, {
waitUntil: 'domcontentloaded',
})
await expect(page.locator('[data-testid="link-share-access"]')).toBeVisible({
Expand Down Expand Up @@ -487,7 +487,7 @@ test.describe('Routed page surfaces — public recipient routes', () => {
).toBeLessThan(300)
const token = created.json.token ?? created.json.data?.token
expect(token, 'the created ephemeral send carries no token').toBeTruthy()
await page.goto(`${APP_BASE}/#/send/${token}`, {
await page.goto(`${APP_BASE}/send/${token}`, {
waitUntil: 'domcontentloaded',
})
await expect(page.locator('[data-testid="send-access-page"]')).toBeVisible({
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/workflows/password-health.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ test.describe('password health', () => {
// redirected to the lock gate by the zero-knowledge router guard (the same
// behaviour the gated-routes spec verifies for every in-app route), so the
// dashboard — and any health data — never mounts.
await page.goto(`${APP_BASE}/#/`, { waitUntil: 'domcontentloaded' })
await page.goto(`${APP_BASE}/`, { waitUntil: 'domcontentloaded' })
await expect(page.locator('.lock-screen')).toBeVisible({ timeout: 20_000 })

// No health data leaks while locked: no strength badges, no dashboard card.
Expand Down
Loading