From 8ac5b15f51ef531edab3cbe96d2efef141bd6ad1 Mon Sep 17 00:00:00 2001 From: curo1305 Date: Wed, 17 Jun 2026 09:26:45 +0200 Subject: [PATCH] docs(11): add code review report --- .../11-REVIEW.md | 345 ++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 .planning/phases/11-visual-design-responsive-layout-cleanup/11-REVIEW.md diff --git a/.planning/phases/11-visual-design-responsive-layout-cleanup/11-REVIEW.md b/.planning/phases/11-visual-design-responsive-layout-cleanup/11-REVIEW.md new file mode 100644 index 0000000..879f15d --- /dev/null +++ b/.planning/phases/11-visual-design-responsive-layout-cleanup/11-REVIEW.md @@ -0,0 +1,345 @@ +--- +phase: 11-visual-design-responsive-layout-cleanup +reviewed: 2026-06-17T00:00:00Z +depth: standard +files_reviewed: 13 +files_reviewed_list: + - frontend/vite.config.js + - frontend/src/router/index.js + - frontend/src/App.vue + - frontend/src/layouts/AdminLayout.vue + - frontend/src/components/layout/AppSidebar.vue + - frontend/src/components/admin/AdminSidebar.vue + - frontend/src/components/storage/StorageBrowser.vue + - frontend/src/components/sharing/ShareModal.vue + - frontend/src/components/cloud/CloudCredentialModal.vue + - frontend/src/components/folders/FolderDeleteModal.vue + - frontend/src/components/documents/DocumentPreviewModal.vue + - frontend/src/views/DocumentView.vue + - frontend/src/router/__tests__/router.guard.test.js +findings: + critical: 4 + warning: 7 + info: 4 + total: 15 +status: issues_found +--- + +# Phase 11: Code Review Report + +**Reviewed:** 2026-06-17T00:00:00Z +**Depth:** standard +**Files Reviewed:** 13 +**Status:** issues_found + +## Summary + +This phase ships responsive drawer navigation, lazy-loading for routes, and modal polish. The core drawer implementation is structurally sound — drawer state is correctly owned by the layout components, backdrop is teleported to body to guarantee stacking, and route-change watchers auto-close the drawer. The router guard logic is correct and well-tested. `rollup-plugin-visualizer` is correctly placed in `devDependencies`. + +Four blocker-class issues were found: the iframe in `DocumentPreviewModal` renders arbitrary blobs without a `sandbox` attribute (XSS vector in the app's own origin); `CloudCredentialModal` can submit an empty server URL without any client-side validation; `suggestTopics()` writes errors into the wrong ref (`classifyError` instead of a dedicated `suggestError`), causing stale errors to bleed across unrelated actions; and the high-traffic routes `/`, `/topics`, `/document/:id`, and `/settings` lack explicit `meta` flags, making the auth guard work only by convention and leaving unauthenticated access untested by the new guard test suite. + +--- + +## Structural Findings (fallow) + +None provided. + +--- + +## Narrative Findings (AI reviewer) + +## Critical Issues + +### CR-01: iframe in DocumentPreviewModal has no sandbox attribute — XSS in app origin + +**File:** `frontend/src/components/documents/DocumentPreviewModal.vue:52-57` +**Issue:** The preview iframe loads an object URL created from a blob fetched from the backend. No `sandbox` attribute is present on the ` +``` +For PDF rendering `allow-scripts` is required by most PDF.js-based renderers. If the iframe does not need to communicate back to the parent or access cookies, do not add `allow-same-origin`. If only PDFs are previewed, the strictest safe value is `sandbox="allow-scripts"`. Evaluate whether `allow-same-origin` is actually needed before adding it, as that combination weakens the sandbox significantly. + +--- + +### CR-02: CloudCredentialModal submits empty server URL with no client-side validation + +**File:** `frontend/src/components/cloud/CloudCredentialModal.vue:301-313` +**Issue:** `submit()` reads `finalUrl = resolvedServerUrl.value` and immediately calls `api.connectWebDav(props.provider.key, finalUrl, ...)`. `resolvedServerUrl` returns `autoServerUrl.value` for the Nextcloud path, which evaluates to `''` when either `serverBase` or `username` is empty (line 219: `if (!serverBase.value || !username.value) return ''`). For the plain WebDAV path, `serverUrl` initializes to `''` and has no HTML `required` attribute — the form's `@submit.prevent` bypass prevents browser validation from firing. There is no guard in `submit()` that checks `!finalUrl`. An empty string is sent to the backend, which may attempt a connection to a relative path or log a misleading error. The user sees "Connection failed" with no actionable message about what field is missing. +**Fix:** +```js +async function submit() { + connectError.value = '' + const finalUrl = resolvedServerUrl.value + if (!finalUrl) { + connectError.value = 'Please enter the server URL before connecting.' + return + } + if (!username.value.trim()) { + connectError.value = 'Please enter a username.' + return + } + saving.value = true + try { + await api.connectWebDav(props.provider.key, finalUrl, username.value, password.value) + emit('connected') + emit('close') + } catch (e) { + connectError.value = e.message || 'Connection failed. Please check your credentials.' + } finally { + saving.value = false + } +} +``` + +--- + +### CR-03: `suggestTopics()` writes errors into `classifyError` — wrong ref, stale error leaks across actions + +**File:** `frontend/src/views/DocumentView.vue:246-257` +**Issue:** The `suggestTopics` function (line 253) assigns exceptions to `classifyError.value` — the same ref used by `reclassify()`. Two concrete bugs result: +1. `suggestTopics()` never resets `classifyError` before running (unlike `reclassify()` which clears it at line 234), so a prior reclassify failure remains visible while the suggest request is in-flight. +2. A `suggestTopics` failure displays an error under the Topics header near the Re-classify button — the wrong location for a suggest-related message, misleading the user into thinking re-classification failed. +3. Running `reclassify()` after a `suggestTopics` failure will clear the suggest error (correct), but running `suggestTopics` after a `reclassify` failure will leave the reclassify error visible (incorrect). +**Fix:** Add a separate `suggestError` ref and reset it at function entry: +```js +const suggestError = ref(null) + +async function suggestTopics() { + suggesting.value = true + suggestError.value = null + try { + const result = await api.suggestTopics(doc.value.id) + suggestions.value = result.suggested + selectedSuggestions.value = [] + } catch (e) { + suggestError.value = e.message || 'Failed to fetch suggestions.' + } finally { + suggesting.value = false + } +} +``` +Add `

{{ suggestError }}

` near the Suggest Topics button. + +--- + +### CR-04: Routes `/`, `/topics`, `/document/:id`, `/settings` have no meta — auth guard relies on implicit convention, unauthenticated access untested + +**File:** `frontend/src/router/index.js:13-17` +**Issue:** The five routes on lines 13–17 have no `meta` object. The navigation guard at line 96 uses `!to.meta.public` to decide whether to attempt a silent token refresh — this works because `undefined` is falsy. However: +1. Future guard branches that check `to.meta.requiresAuth` explicitly (mirroring the pattern on `/cloud`, `/folders/:folderId`, `/shared`) would silently skip these routes, potentially allowing unauthenticated access to the file manager and document pages. +2. The D-09 admin-redirect guard (line 113: `!isAdminRoute && !to.meta.public && isAdmin`) correctly fires on these routes only because of the same implicit convention. This is correct today but fragile. +3. The `router.guard.test.js` test suite covers unauthenticated redirect only for `/settings` (via the refresh-fails test at line 183) and does not include a negative test asserting that navigating to `/` or `/document/some-uuid` without a token redirects to `/login`. This means the primary auth protection on the file manager has no regression test. +**Fix:** Add explicit meta to all five routes and add unauthenticated-redirect tests: +```js +{ path: '/', component: FileManagerView, meta: { requiresAuth: true } }, +{ path: '/topics', component: () => import('../views/TopicsView.vue'), meta: { requiresAuth: true } }, +{ path: '/topics/:name', component: () => import('../views/TopicsView.vue'), meta: { requiresAuth: true } }, +{ path: '/document/:id', component: () => import('../views/DocumentView.vue'), meta: { requiresAuth: true } }, +{ path: '/settings', component: () => import('../views/SettingsView.vue'), meta: { requiresAuth: true } }, +``` +Add to `router.guard.test.js`: +```js +it('unauthenticated user visiting / is redirected to /login', async () => { + useAuthStore.mockReturnValue({ + accessToken: null, user: null, + refresh: vi.fn().mockRejectedValue(new Error('no session')), + }) + await router.push('/') + expect(router.currentRoute.value.path).toBe('/login') +}) +``` + +--- + +## Warnings + +### WR-01: AdminLayout has no Escape key handler for the mobile drawer + +**File:** `frontend/src/layouts/AdminLayout.vue:46-59` +**Issue:** `App.vue` registers a `keydown` listener on `document` (lines 98–100) but that listener is not active when the admin layout is rendered — `App.vue` routes admin paths through a bare `` (line 3), bypassing the `div` that owns the keydown listener. `AdminLayout.vue` registers no keyboard event handler. An admin user who opens the mobile drawer on a phone cannot close it with Escape. The `CloudCredentialModal` uses `@keydown.escape.window` which confirms the escape pattern is understood; the drawer simply lacks it. +**Fix:** +```js +// Add to AdminLayout.vue