Skip to content

Feat/paperless attachement links#1492

Open
szaiser wants to merge 12 commits into
sysadminsmedia:mainfrom
szaiser:feat/paperless-immich-attachement-links
Open

Feat/paperless attachement links#1492
szaiser wants to merge 12 commits into
sysadminsmedia:mainfrom
szaiser:feat/paperless-immich-attachement-links

Conversation

@szaiser
Copy link
Copy Markdown
Contributor

@szaiser szaiser commented May 12, 2026

Compacted conversation---

What type of PR is this?

  • feature

What this PR does / why we need it:

Builds on #1481 to add a Paperless-ngx integration and lay the groundwork for further service integrations (e.g. Immich). Users can drag-drop a Paperless document URL onto an item's attachment zone — the document is stored as an external reference (no file copy) and renders as a rich card with thumbnail, title, correspondent, document type, tags, page count and an open-in-Paperless button.

homebox_paperless_link.mp4

Changes:

  • New endpoint: GET /v1/integrations/{name}/proxy?path={relPath} — generic authenticated reverse-proxy; reads {name}_url / {name}_token from user settings, validates name and path, forwards Authorization: Token {token}
  • Repo: MimeTypePaperlessDocument = "paperless/document" registered in MimeTypeForSourceType()
  • Frontend: integration-adapters.ts registry (ServiceAdapter interface + SERVICE_ADAPTERS); integration-cache.ts Pinia store for URL state and enriched-data cache; Paperless card in AttachmentsList.vue (thumbnail, metadata, ⚠ error badge); drag-drop URL detection in edit.vue; Paperless URL + token fields in profile.vue
  • Tests: 25 unit tests in integration-adapters.test.ts

Design decisions:

  • Proxy is intentionally service-agnostic; credentials never reach the browser. Adding a future service (e.g. Immich) requires one new SERVICE_ADAPTERS entry on the frontend and one line in MimeTypeForSourceType() on the backend — no further structural changes.
  • link/url attachments from feat: add support for external URL attachments #1481 whose host matches the configured Paperless URL are auto-promoted to the rich card at render time — fully backward compatible
  • Registry pattern eliminates all service-specific if/else chains in drop detection, URL classification, and card hydration

Which issue(s) this PR fixes:

Testing

  • ✅ Frontend unit tests: npx vitest run lib/integration-adapters.test.ts — 25/25 pass
  • ✅ Backend tests: go test ./internal/core/services ./internal/data/repo ./app/api/handlers/v1 pass
  • Manual: tested against live Paperless-ngx v2.x — drag-and-drop, card rendering (thumbnail, tags, correspondent), error badge on wrong token, graceful demotion when URL is cleared

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 79dbb989-2979-44a9-8fcc-7dbb3c576146

📥 Commits

Reviewing files that changed from the base of the PR and between 1ed1bc6 and 30e18e2.

⛔ Files ignored due to path filters (1)
  • backend/go.sum is excluded by !**/*.sum
📒 Files selected for processing (15)
  • backend/app/api/handlers/v1/v1_ctrl_integration_proxy.go
  • backend/app/api/routes.go
  • backend/internal/core/services/service_items_attachments_external_test.go
  • backend/internal/data/repo/repo_item_attachments.go
  • backend/internal/data/repo/repo_item_attachments_test.go
  • frontend/components/Item/AttachmentsList.vue
  • frontend/composables/preferences-utils.ts
  • frontend/composables/use-preferences.test.ts
  • frontend/composables/use-preferences.ts
  • frontend/lib/integration-adapters.test.ts
  • frontend/lib/integration-adapters.ts
  • frontend/locales/en.json
  • frontend/pages/item/[id]/index/edit.vue
  • frontend/pages/profile.vue
  • frontend/stores/integration-cache.ts
✅ Files skipped from review due to trivial changes (1)
  • frontend/locales/en.json
🚧 Files skipped from review as they are similar to previous changes (14)
  • frontend/composables/use-preferences.test.ts
  • backend/internal/data/repo/repo_item_attachments.go
  • frontend/pages/profile.vue
  • backend/app/api/routes.go
  • frontend/lib/integration-adapters.test.ts
  • frontend/pages/item/[id]/index/edit.vue
  • backend/internal/data/repo/repo_item_attachments_test.go
  • backend/app/api/handlers/v1/v1_ctrl_integration_proxy.go
  • frontend/composables/use-preferences.ts
  • frontend/composables/preferences-utils.ts
  • frontend/lib/integration-adapters.ts
  • backend/internal/core/services/service_items_attachments_external_test.go
  • frontend/stores/integration-cache.ts
  • frontend/components/Item/AttachmentsList.vue

Summary by CodeRabbit

New Features

  • Added Paperless document integration with rich metadata display including thumbnails, correspondent info, tags, and document type
  • Added integration URL and API token configuration in user profile settings
  • Enhanced external attachment handling with service-specific enrichment and better URL classification

Refactor

  • Reorganized attachment handling and preferences management code for better maintainability

Tests

  • Expanded test coverage for attachment operations and integration functionality

Walkthrough

Adds end-to-end integration support (Paperless): a secure backend proxy, frontend adapter/registry, TTL-backed cache store, integration settings UI, attachment enrichment/hydration, and a preference-sync fix that preserves integration credentials.

Changes

Integration Services Support

Layer / File(s) Summary
Backend integration proxy endpoint
backend/app/api/handlers/v1/v1_ctrl_integration_proxy.go, backend/app/api/routes.go
Implements an authenticated proxy handler validating integration name/path, loading per-user {name}_url and {name}_token, performing an authenticated GET via a shared http.Client with SSRF/IP protections and 30s timeout, mapping upstream errors (404→not-found, ≥400→502), enforcing a 10MB response limit, and forwarding Content-Type and body. Registers GET /api/v1/integrations/{name}/proxy with user middleware.
Backend MIME types & external-link tests
backend/internal/data/repo/repo_item_attachments.go, backend/internal/data/repo/repo_item_attachments_test.go, backend/internal/core/services/service_items_attachments_external_test.go
Adds MimeTypePaperlessDocument and maps "paperless" source type to it. Refactors external-link repo/service tests to table-driven suites driven by known MIME/source cases and validates persistence, attachment type variants, multi-attachment handling, no-blob-storage behavior, invalid-entity errors, and deletion.
Frontend integration adapter framework
frontend/lib/integration-adapters.ts, frontend/lib/integration-adapters.test.ts
Adds ServiceAdapter interface, extractWithPattern helper, extractPaperlessDocId, SERVICE_ADAPTERS registry, getAdapter/getAdapterByMimeType, detectServiceFromUrl, and classifyDroppedUrl. Tests exercise URL parsing, sub-path/base URL installs, host-prefix attack rejection, adapter lookup, and registry schema.
Frontend preference sync security fix
frontend/composables/preferences-utils.ts, frontend/composables/use-preferences.ts, frontend/composables/use-preferences.test.ts
Extracts preference schema and sync helpers (buildSyncedSettings, mergeSyncedSettings) to a pure module; updates use-preferences to GET current server settings, merge the synced payload into them, and persist the merged result (GET → merge → PUT) so integration keys are preserved. Includes unit/regression tests covering build/merge and round-trip behavior.
Frontend integration cache store
frontend/stores/integration-cache.ts
Adds useIntegrationCacheStore (Pinia) implementing TTL-backed localStorage caching (30 minutes), per-attachment fetch state (loading/ok/stale/error), enriched data storage, and helpers to set/clear/invalidate cached enriched data and update service URLs.
Frontend integration settings UI & wiring
frontend/pages/profile.vue, frontend/pages/item/[id]/index/edit.vue, frontend/locales/en.json
Adds Profile integration settings (Paperless URL/token) with load/save handlers that update the integration cache store; edit page loads integrationSettings before refresh and classifies dropped URLs via classifyDroppedUrl; adds i18n strings and service-linked toasts.
Frontend attachment rendering with enrichment
frontend/components/Item/AttachmentsList.vue
Makes AttachmentsList integration-aware: lifts link/url attachments to paperless/document when adapter matches, renders rich Paperless cards (or unconfigured fallback), generic external links, and downloadable attachments; hydrates metadata and thumbnails via proxy endpoints, updates cache and per-attachment fetch states, manages thumbnail object URLs, and reacts to service URL changes.

Sequence Diagram(s)

sequenceDiagram
  participant User as User (Item Editor)
  participant EditPage as Edit Page
  participant DropHandler as Drop Handler
  participant ServiceAdapter as Service Adapter
  participant API as API
  participant ProxyEndpoint as Proxy Endpoint
  participant ExternalService as Paperless Service
  User->>EditPage: Drag Paperless link onto attachment area
  EditPage->>DropHandler: classifyDroppedUrl(url, integrationSettings)
  DropHandler->>ServiceAdapter: detectServiceFromUrl + extractId
  ServiceAdapter-->>DropHandler: {adapter: paperless, id: "42"}
  EditPage->>API: POST /attachments/external-link?sourceType=paperless
  API-->>EditPage: ✓ attachment created with paperless/document MIME
  EditPage->>EditPage: Show service-specific toast
  par Async Enrichment
    Note over EditPage: User views item in AttachmentsList
    EditPage->>ProxyEndpoint: GET /api/v1/integrations/paperless/proxy?path=/api/documents/42/
    ProxyEndpoint->>ExternalService: Proxied request + auth token
    ExternalService-->>ProxyEndpoint: Document metadata
    ProxyEndpoint-->>EditPage: Metadata response
    EditPage->>EditPage: Store enriched data + update fetch state
    EditPage->>EditPage: Render Paperless card with correspondent/tags
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • tankerkiller125

Security Recommendations

  • Verify the proxy never logs authorization tokens, full upstream URLs with sensitive query parameters, or raw headers; redact sensitive fields.
  • Ensure tenant/user isolation when reading per-user {name}_url and {name}_token to prevent cross-tenant credential leakage.
  • Expand SSRF defenses: add tests for DNS resolving to private IPs (IPv4/IPv6), and consider verifying IPs after CNAME chains and when using proxy DNS.
  • Add integration tests for oversized upstream responses, DNS-to-private-IP resolution, and upstream timeouts to validate error mapping, resource bounds, and timeouts.
  • Consider streaming large responses instead of buffering up to 10MB in memory for binary assets and document the policy for allowed content-types/sizes.

A proxy hums and checks each host,
Preferences merge, no creds are lost;
Paperless cards awake,
Thumbs and metadata bake,
Enriched attachments—safely crossed. 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title mentions the main feature (Paperless attachments) but contains a typo ('attachement' instead of 'attachment') and uses informal phrasing that could be clearer.
Description check ✅ Passed The PR description covers all required template sections: PR type (feature), detailed explanation of changes including file-by-file breakdown, linked issues, and comprehensive testing notes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added ⬆️ enhancement New feature or request review needed A review is needed on this PR or Issue go Pull requests that update Go code labels May 12, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (6)
frontend/composables/use-preferences.test.ts (1)

38-43: 💤 Low value

Consider using valid theme values in test data.

Line 39 uses theme: "dark" as never, which suggests "dark" may not be a valid DaisyTheme value. Using as never bypasses type checking and reduces test quality.

Recommend either:

  1. Use a valid theme value from the DaisyTheme type (e.g., "homebox")
  2. If "dark" is actually valid, remove the as never assertion

Note: Line 67 in a later test uses theme: "dark" without as never, which is inconsistent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/composables/use-preferences.test.ts` around lines 38 - 43, The test
uses an invalid cast "theme: \"dark\" as never" which hides type errors; update
the test in use-preferences.test.ts to supply a valid DaisyTheme or remove the
cast: change the prefs object (currently spread from DEFAULT_PREFERENCES) passed
to buildSyncedSettings(SYNC_ALL) so theme is either a real DaisyTheme value
(e.g., "homebox" or whatever is defined in the DaisyTheme union) or, if "dark"
is valid, drop the `as never` and use `theme: "dark"`. Ensure consistency with
the later test that already uses `theme: "dark"` without a cast and keep
references to DEFAULT_PREFERENCES, buildSyncedSettings, and SYNC_ALL unchanged.
frontend/composables/preferences-utils.ts (1)

89-101: 💤 Low value

Consider improving type safety instead of using as never.

The as never assertion on line 97 bypasses TypeScript's type checking entirely, which could hide runtime type errors if the server returns unexpected value types for preference keys.

Consider one of these approaches:

  1. Add runtime type validation before assignment
  2. Define a more specific type for the settings parameter that constrains it to valid preference value types
  3. At minimum, add a comment explaining why this assertion is safe

The current implementation works but sacrifices type safety for convenience.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/composables/preferences-utils.ts` around lines 89 - 101,
mergeSyncedSettings currently silences TypeScript by casting settings[key] to
never before assigning to nextPreferences; instead make the assignment type-safe
by either (A) narrowing the settings parameter to a keyed type that maps known
preference keys to their allowed value types (e.g., Record<keyof
LocationViewPreferences, ...>) so you can assign without assertions, or (B)
perform a runtime type-check per key inside mergeSyncedSettings using a small
type-check map/type guard for each preference key (validate settings[key]
matches the expected type) and only then assign to nextPreferences; remove the
`as never` cast and replace it with the validated value (or tighten the function
signature) and keep forEachSyncedPreference usage the same.
frontend/pages/profile.vue (1)

515-520: ⚡ Quick win

Consider adding a password visibility toggle for the API token field.

Users often need to verify they've pasted their token correctly. The token is stored in settings (not as sensitive as a password that's never stored), so adding a show/hide toggle would improve usability without significantly increasing security risk.

This is already implemented for password fields elsewhere in the app (see FormPassword component with toggle). You could either:

  1. Use FormPassword component which has built-in toggle
  2. Add a similar toggle to the token field

Example using FormPassword:

<FormPassword
  v-model="integrationSettings.paperlessToken"
  :label="$t('profile.paperless_token')"
  :placeholder="$t('profile.paperless_token_placeholder')"
/>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/pages/profile.vue` around lines 515 - 520, Replace the current
FormTextField usage for the API token with a field that supports visibility
toggling (either swap to the existing FormPassword component or add the same
toggle behavior) so users can show/hide integrationSettings.paperlessToken when
verifying input; update the component at the FormTextField instance in
profile.vue (the block rendering integrationSettings.paperlessToken with label
profile.paperless_token and placeholder profile.paperless_token_placeholder) to
use FormPassword or mirror its show/hide logic and props.
backend/app/api/handlers/v1/v1_ctrl_integration_proxy.go (1)

41-128: ⚖️ Poor tradeoff

Consider: Rate limiting per user for proxy endpoint.

Since users control the upstream URL, they could abuse this endpoint to:

  1. Hammer a third-party service with requests
  2. Use the proxy as a generic HTTP client
  3. Cause excessive outbound bandwidth usage

While the existing auth middleware provides per-user isolation, consider adding per-user rate limiting for this endpoint specifically.

Example approach:

// In routes.go, wrap the handler with a rate limiter
r.Get("/integrations/{name}/proxy", 
  chain.ToHandlerFunc(
    v1Ctrl.HandleIntegrationProxy(), 
    append(userMW, a.integrationProxyLimiter.middleware)...,
  ),
)

This is lower priority than the timeout/size fixes but worth considering for production hardening.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/api/handlers/v1/v1_ctrl_integration_proxy.go` around lines 41 -
128, Add per-user rate limiting to the integration proxy by wrapping the
V1Controller.HandleIntegrationProxy handler with a user-scoped limiter
middleware: implement or reuse an integrationProxyLimiter that keys limits by
the authenticated user ID (services.UseUserCtx(ctx.Context).ID) and attach its
middleware when registering the route (e.g., in routes.go wrap the GET
"/integrations/{name}/proxy" with
chain.ToHandlerFunc(v1Ctrl.HandleIntegrationProxy(), append(userMW,
a.integrationProxyLimiter.middleware)...)); ensure the limiter runs before the
handler and returns a proper 429/Retry-After response when the per-user quota is
exceeded.
frontend/components/Item/AttachmentsList.vue (2)

105-107: ⚡ Quick win

Hardcoded service name in i18n string.

The service name "Paperless" is hardcoded in the i18n interpolation. If additional services are added in the future, this will need to be updated manually.

Consider deriving the service name from the attachment's MIME type via the adapter registry:

♻️ Suggested refactor for dynamic service names

Add a helper to get the display name:

+function getServiceDisplayName(attachment: ItemAttachment): string {
+  const adapter = SERVICE_ADAPTERS.find(a => a.mimeType === attachment.mimeType);
+  return adapter ? adapter.name.charAt(0).toUpperCase() + adapter.name.slice(1) : "Service";
+}

Then use it in the template:

 <TooltipContent>
-  {{ $t("components.item.attachments_list.open_in_service", { service: "Paperless" }) }}
+  {{ $t("components.item.attachments_list.open_in_service", { service: getServiceDisplayName(attachment) }) }}
 </TooltipContent>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/components/Item/AttachmentsList.vue` around lines 105 - 107, The
template currently hardcodes the service name "Paperless" in the i18n
interpolation for TooltipContent (key
"components.item.attachments_list.open_in_service"); replace this with a dynamic
display name derived from the attachment's MIME type by calling a helper that
looks up the adapter registry (e.g., create a helper like
getServiceDisplayNameFromMime(mimeType) and use it in the component's computed
properties or methods to pass the resolved service name into $t). Ensure
TooltipContent uses the computed/serviceName value instead of the hardcoded
string so future services are handled automatically.

395-444: ⚖️ Poor tradeoff

Silent error swallowing in enrichment sub-fetches.

The correspondent, document_type, and tag fetches use .catch(() => {}) to silently swallow errors (lines 407, 423, 443). This is graceful degradation — if a related entity fetch fails, the main document data is still shown with partial enrichment.

This is likely intentional, but consider whether users should be notified when partial data couldn't be loaded (e.g., "Some metadata unavailable"). Current behavior shows incomplete data without indication.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/components/Item/AttachmentsList.vue` around lines 395 - 444, The
sub-fetches for correspondent, document_type, and tags in AttachmentsList.vue
currently swallow errors with .catch(() => {}) which hides partial-enrichment
failures; replace those empty catches by capturing the error (e) and (1) logging
it (console.error or a logger) and (2) marking a shared flag (e.g.,
enrichmentFailed or push to enrichmentErrors) accessible alongside
doc/tagResults/jobs so the UI can render a small “Some metadata unavailable”
notice when any enrichmentFailed is true; update the code paths that push to
jobs (the correspondent/document_type/tag fetch chains) to set this flag on
error and ensure the component renders the notification when
enrichmentErrors.length>0.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/app/api/handlers/v1/v1_ctrl_integration_proxy.go`:
- Around line 104-109: Replace use of http.DefaultClient in the integration
proxy handler with a dedicated HTTP client that enforces a timeout and sane
transport settings: add a package-level variable (e.g., proxyHTTPClient)
configured with Timeout: 30*time.Second and a Transport that sets MaxIdleConns,
IdleConnTimeout, and DisableCompression as suggested, ensure the package imports
time, and update the call in the handler (where resp, err :=
http.DefaultClient.Do(req)) to use proxyHTTPClient.Do(req) instead.
- Around line 124-126: The handler currently calls io.Copy(w, resp.Body) without
bounds (see w.WriteHeader and io.Copy usage) which can allow huge upstream
responses; wrap resp.Body with io.LimitReader (or use io.CopyN) and enforce a
hard size limit (e.g. const maxRespSize = 10 << 20) when copying to w, detect if
the upstream exceeds that limit and return a safe error/status (e.g. write
http.StatusBadGateway or http.StatusRequestEntityTooLarge and a small error
body) instead of streaming unlimited data; update the code around the resp.Body
copy to use io.LimitReader(resp.Body, maxRespSize+1) and check if bytesCopied >
maxRespSize to handle the overflow case.

In `@backend/internal/core/services/service_items_attachments_external_test.go`:
- Around line 37-44: The test data in the knownSources variable still contains
an Immich entry even though Immich was removed; update the knownSources slice in
service_items_attachments_external_test.go by removing the {"immich",
"1df4f848-dead-beef-cafe-123456789abc"} element (or, if Immich is intentionally
kept, update the PR summary to state that Immich support remains) so the test
data matches the intended adapter registry state; edit the knownSources
declaration to only include the remaining valid sources (e.g., "paperless" and
"link") and run the tests.

In `@frontend/components/Item/AttachmentsList.vue`:
- Around line 351-363: The function describeRequestError returns hardcoded
English messages; replace them with i18n lookups using the
component/localization instance (e.g., $t) and add corresponding keys
(components.item.attachments_list.errors.auth_failed, .request_failed with
{status}, and .service_unreachable with {baseUrl}) to your locale files (e.g.,
frontend/locales/en.json); update describeRequestError to call $t(...) for the
401/403 branch, the generic HTTP branch (passing status), and the network-level
branch (passing baseUrl) so all user-facing strings are translatable.

In `@frontend/lib/integration-adapters.test.ts`:
- Around line 40-42: The extractPaperlessDocId implementation silently falls
back to pattern-only matching when parsing the configured baseUrl fails; update
the catch block in extractPaperlessDocId to emit a developer-facing warning
(e.g., console.warn or the app logger.warn) that includes the invalid baseUrl
and the parse error, and add a small validation helper (e.g.,
validatePaperlessBaseUrl) to run at configuration time to validate/normalize the
baseUrl and surface errors earlier instead of silently relying on heuristic
pattern matching.

In `@frontend/pages/item/`[id]/index/edit.vue:
- Around line 91-102: The loadIntegrationSettings function silently returns when
api.user.getSettings() fails or data?.item is missing; update
loadIntegrationSettings to handle errors by logging the error (e.g.,
console.error or process logger) and surface a user-facing notification/toast so
users know settings failed to load, while preserving the existing fallback to
empty integrationSettings; specifically catch or check for error from
api.user.getSettings(), log the error and call the component's
notification/toast method (or emit an event) before returning, and also validate
data.item before casting to Record<string, unknown> (refer to
loadIntegrationSettings, api.user.getSettings, and integrationSettings).
- Around line 414-419: Replace the hardcoded `${serviceName} linked` toast with
a translatable string: use the i18n key (e.g. "items.toast.service_linked") and
call t with a replacement object so the service name (from
classified.adapter.name → serviceName) is injected, then pass that translated
string into toast.success; ensure the locale files include the "{service}
linked" entry and update the toast invocation near the classified handling code
that computes serviceName.
- Around line 40-46: Remove the unused Immich fields from the reactive
integrationSettings object: delete immich_url and immich_token so
integrationSettings only contains paperless_url and paperless_token; also remove
any code that assigns loaded settings into integrationSettings. Leave
classifyDroppedUrl and SERVICE_ADAPTERS unchanged (they only use Paperless), and
ensure no other references to integrationSettings. This will eliminate vestigial
Immich config while keeping the Paperless integration intact.

---

Nitpick comments:
In `@backend/app/api/handlers/v1/v1_ctrl_integration_proxy.go`:
- Around line 41-128: Add per-user rate limiting to the integration proxy by
wrapping the V1Controller.HandleIntegrationProxy handler with a user-scoped
limiter middleware: implement or reuse an integrationProxyLimiter that keys
limits by the authenticated user ID (services.UseUserCtx(ctx.Context).ID) and
attach its middleware when registering the route (e.g., in routes.go wrap the
GET "/integrations/{name}/proxy" with
chain.ToHandlerFunc(v1Ctrl.HandleIntegrationProxy(), append(userMW,
a.integrationProxyLimiter.middleware)...)); ensure the limiter runs before the
handler and returns a proper 429/Retry-After response when the per-user quota is
exceeded.

In `@frontend/components/Item/AttachmentsList.vue`:
- Around line 105-107: The template currently hardcodes the service name
"Paperless" in the i18n interpolation for TooltipContent (key
"components.item.attachments_list.open_in_service"); replace this with a dynamic
display name derived from the attachment's MIME type by calling a helper that
looks up the adapter registry (e.g., create a helper like
getServiceDisplayNameFromMime(mimeType) and use it in the component's computed
properties or methods to pass the resolved service name into $t). Ensure
TooltipContent uses the computed/serviceName value instead of the hardcoded
string so future services are handled automatically.
- Around line 395-444: The sub-fetches for correspondent, document_type, and
tags in AttachmentsList.vue currently swallow errors with .catch(() => {}) which
hides partial-enrichment failures; replace those empty catches by capturing the
error (e) and (1) logging it (console.error or a logger) and (2) marking a
shared flag (e.g., enrichmentFailed or push to enrichmentErrors) accessible
alongside doc/tagResults/jobs so the UI can render a small “Some metadata
unavailable” notice when any enrichmentFailed is true; update the code paths
that push to jobs (the correspondent/document_type/tag fetch chains) to set this
flag on error and ensure the component renders the notification when
enrichmentErrors.length>0.

In `@frontend/composables/preferences-utils.ts`:
- Around line 89-101: mergeSyncedSettings currently silences TypeScript by
casting settings[key] to never before assigning to nextPreferences; instead make
the assignment type-safe by either (A) narrowing the settings parameter to a
keyed type that maps known preference keys to their allowed value types (e.g.,
Record<keyof LocationViewPreferences, ...>) so you can assign without
assertions, or (B) perform a runtime type-check per key inside
mergeSyncedSettings using a small type-check map/type guard for each preference
key (validate settings[key] matches the expected type) and only then assign to
nextPreferences; remove the `as never` cast and replace it with the validated
value (or tighten the function signature) and keep forEachSyncedPreference usage
the same.

In `@frontend/composables/use-preferences.test.ts`:
- Around line 38-43: The test uses an invalid cast "theme: \"dark\" as never"
which hides type errors; update the test in use-preferences.test.ts to supply a
valid DaisyTheme or remove the cast: change the prefs object (currently spread
from DEFAULT_PREFERENCES) passed to buildSyncedSettings(SYNC_ALL) so theme is
either a real DaisyTheme value (e.g., "homebox" or whatever is defined in the
DaisyTheme union) or, if "dark" is valid, drop the `as never` and use `theme:
"dark"`. Ensure consistency with the later test that already uses `theme:
"dark"` without a cast and keep references to DEFAULT_PREFERENCES,
buildSyncedSettings, and SYNC_ALL unchanged.

In `@frontend/pages/profile.vue`:
- Around line 515-520: Replace the current FormTextField usage for the API token
with a field that supports visibility toggling (either swap to the existing
FormPassword component or add the same toggle behavior) so users can show/hide
integrationSettings.paperlessToken when verifying input; update the component at
the FormTextField instance in profile.vue (the block rendering
integrationSettings.paperlessToken with label profile.paperless_token and
placeholder profile.paperless_token_placeholder) to use FormPassword or mirror
its show/hide logic and props.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3a8cf4a1-03dd-4a60-8ea5-c76e7c97c8d2

📥 Commits

Reviewing files that changed from the base of the PR and between e5b0fe2 and 4cf2532.

⛔ Files ignored due to path filters (1)
  • backend/go.sum is excluded by !**/*.sum
📒 Files selected for processing (16)
  • backend/app/api/handlers/v1/v1_ctrl_integration_proxy.go
  • backend/app/api/routes.go
  • backend/internal/core/services/service_items_attachments_external_test.go
  • backend/internal/data/repo/repo_item_attachments.go
  • backend/internal/data/repo/repo_item_attachments_test.go
  • docs/todo-integration-features.md
  • frontend/components/Item/AttachmentsList.vue
  • frontend/composables/preferences-utils.ts
  • frontend/composables/use-preferences.test.ts
  • frontend/composables/use-preferences.ts
  • frontend/lib/integration-adapters.test.ts
  • frontend/lib/integration-adapters.ts
  • frontend/locales/en.json
  • frontend/pages/item/[id]/index/edit.vue
  • frontend/pages/profile.vue
  • frontend/stores/integration-cache.ts

Comment thread backend/app/api/handlers/v1/v1_ctrl_integration_proxy.go Outdated
Comment thread backend/app/api/handlers/v1/v1_ctrl_integration_proxy.go Outdated
Comment thread frontend/components/Item/AttachmentsList.vue
Comment thread frontend/lib/integration-adapters.test.ts
Comment thread frontend/pages/item/[id]/index/edit.vue Outdated
Comment thread frontend/pages/item/[id]/index/edit.vue
szaiser pushed a commit to szaiser/homebox that referenced this pull request May 12, 2026
…dia#1492

- backend: replace http.DefaultClient with dedicated proxyHTTPClient
  (30s timeout, bounded transport) to prevent upstream hangs (critical)
- backend: cap proxy response at 10 MB via io.LimitReader to prevent
  memory exhaustion from unbounded upstream responses (major)
- frontend: remove vestigial immich_url / immich_token fields from
  integrationSettings in edit.vue; these were never used after the
  Immich removal (minor)
- frontend: add console.warn in loadIntegrationSettings on API failure
  instead of silently returning (minor)
- frontend: replace hardcoded service-linked toast string with i18n key
  items.toast.service_linked (minor)
- frontend: replace hardcoded English strings in describeRequestError
  with i18n keys under components.item.attachments_list.errors (minor)
- frontend: add console.warn in integration-adapters.ts catch block
  when baseUrl fails to parse (minor)
- locales: add items.toast.service_linked and
  components.item.attachments_list.errors.{auth_failed,request_failed,
  service_unreachable} keys to en.json
szaiser pushed a commit to szaiser/homebox that referenced this pull request May 12, 2026
- Remove MimeTypeImmichAsset constant from repo_item_attachments.go
- Remove immich from externalLinkMimeTypes slice
- Remove immich case from MimeTypeForSourceType()
- Remove immich test block from repo_item_attachments_test.go
  (TestMimeTypeForSourceType and externalLinkMimeTypeCases)
- Remove immich entry from knownSources in
  service_items_attachments_external_test.go

Resolves CodeRabbit review comment on PR sysadminsmedia#1492 (immich inconsistency).
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
backend/app/api/handlers/v1/v1_ctrl_integration_proxy.go (1)

35-52: ⚡ Quick win

Update the godoc or implement per-integration auth scheme support.

Line 114 hardcodes Authorization: Token <token> for all integrations. This contradicts the godoc claim on lines 41–43 that "adding a new integration only requires a Vue component and a settings entry — no new Go code." Paperless uses Token auth, but Immich uses x-api-key, and most modern APIs use Authorization: Bearer. The frontend adapter registry is already extensible, but the backend lacks a way to specify per-integration auth schemes.

Either:

  1. Store auth scheme in settings ({name}_auth_scheme, default "Token"), or
  2. Add a backend per-integration registry mapping names to auth scheme handlers

Choose one to make the endpoint's actual behavior match its documented behavior, especially important for multi-tenancy/user data isolation considerations with external service credentials.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/api/handlers/v1/v1_ctrl_integration_proxy.go` around lines 35 -
52, The handler HandleIntegrationProxy currently always sets "Authorization:
Token <token>" using the {name}_token setting; update it to read an optional
{name}_auth_scheme setting (default "Token") and use that to build the outgoing
auth header (support at least "Token", "Bearer", and "x-api-key" schemes) before
sending the proxied request, so the code that retrieves {name}_token and sets
the header is replaced with logic that switches on authScheme and sets either
Authorization: Token <token>, Authorization: Bearer <token>, or X-API-Key:
<token> accordingly; also update the godoc to reflect that integrations may
specify an auth scheme via settings and/or validate unknown schemes with a 400
or fall back to default.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/app/api/handlers/v1/v1_ctrl_integration_proxy.go`:
- Around line 138-144: Replace the current truncation check that treats n ==
maxResponseSize as truncated by reading one extra byte: use
io.LimitReader(resp.Body, maxResponseSize+1) (instead of maxResponseSize) when
copying to w, then treat truncation only when n > maxResponseSize; keep logging
via log.Warn().Str("integration", name).Int64("bytes", n).Msg(...) and preserve
the existing copyErr handling. This ensures a true positive truncation detection
for the variables n, copyErr, resp.Body and maxResponseSize in the integration
proxy response copy block.
- Around line 92-108: The handler currently proxies user-provided
settings[name+"_url"] without IP filtering, hardcodes the auth scheme as "Token
"+token, and misdetects truncation; fix by (1) adding a custom DialContext to
proxyHTTPClient that resolves the target hostname and rejects connections to
loopback, link-local, unspecified and RFC1918/private ranges (or enforce
HTTPS/host allow-list) before dialing; reference proxyHTTPClient and the
upstream variable to locate where to apply the DialContext and validation; (2)
make the auth scheme configurable by reading settings[name+"_auth_scheme"]
(fallback to "Token") instead of always using "Token "+token so integrations can
use Bearer/Basic/x-api-key; reference the code that sets the Authorization
header; and (3) detect response truncation correctly by reading with
io.LimitReader(resp.Body, maxResponseSize+1) and checking if bytesRead >
maxResponseSize (reference maxResponseSize and the io.LimitReader usage).

In `@frontend/pages/item/`[id]/index/edit.vue:
- Around line 89-99: The loadIntegrationSettings function should avoid logging
the raw error object and must validate setting types before assigning to
integrationSettings; change the error logging to a generic message (e.g.,
"Failed to load integration settings") and, if you need details, log only safe
fields like error?.message, and ensure api.user.getSettings() errors are not
printing full payloads. For assignments to integrationSettings.paperless_url and
paperless_token, check the type of settings.paperless_url and
settings.paperless_token (e.g., typeof === "string") before using them,
otherwise assign an empty string or a safe default; also treat data.item
defensively (ensure it's an object) when reading values from
api.user.getSettings.

---

Nitpick comments:
In `@backend/app/api/handlers/v1/v1_ctrl_integration_proxy.go`:
- Around line 35-52: The handler HandleIntegrationProxy currently always sets
"Authorization: Token <token>" using the {name}_token setting; update it to read
an optional {name}_auth_scheme setting (default "Token") and use that to build
the outgoing auth header (support at least "Token", "Bearer", and "x-api-key"
schemes) before sending the proxied request, so the code that retrieves
{name}_token and sets the header is replaced with logic that switches on
authScheme and sets either Authorization: Token <token>, Authorization: Bearer
<token>, or X-API-Key: <token> accordingly; also update the godoc to reflect
that integrations may specify an auth scheme via settings and/or validate
unknown schemes with a 400 or fall back to default.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b20defff-c331-4d61-90b5-90df83d23fbb

📥 Commits

Reviewing files that changed from the base of the PR and between 4cf2532 and 01fbe70.

📒 Files selected for processing (5)
  • backend/app/api/handlers/v1/v1_ctrl_integration_proxy.go
  • frontend/components/Item/AttachmentsList.vue
  • frontend/lib/integration-adapters.ts
  • frontend/locales/en.json
  • frontend/pages/item/[id]/index/edit.vue
✅ Files skipped from review due to trivial changes (1)
  • frontend/locales/en.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/lib/integration-adapters.ts
  • frontend/components/Item/AttachmentsList.vue

Comment thread backend/app/api/handlers/v1/v1_ctrl_integration_proxy.go
Comment thread backend/app/api/handlers/v1/v1_ctrl_integration_proxy.go Outdated
Comment thread frontend/pages/item/[id]/index/edit.vue
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
backend/internal/data/repo/repo_item_attachments.go (1)

98-110: Ensure path validation occurs at the API handler layer.

The repository correctly stores the external ID without validation (appropriate for the data access layer). Per the PR objectives, ensure the API handler validates both the name and path parameters before calling repository methods to prevent injection or traversal attacks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/data/repo/repo_item_attachments.go` around lines 98 - 110,
The repo currently accepts external IDs without validation (e.g.,
MimeTypeForSourceType in repo_item_attachments.go), so add explicit validation
in the API handler(s) that call the repository attachment methods: check both
name and path are non-empty, within length limits, match an allowed character
set (or whitelist), normalize with filepath.Clean and then reject values that
are absolute (start with "/") or contain path traversal (cleaned starts with
".." or contains ".." segments or differs from a safe relative representation),
and return a 4xx error before invoking any repo methods; implement this
validation in the handler(s) that parse the incoming request and call the repo
attachment functions so the data layer continues to accept raw IDs but only
after the API layer has validated them.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@backend/internal/data/repo/repo_item_attachments.go`:
- Around line 98-110: The repo currently accepts external IDs without validation
(e.g., MimeTypeForSourceType in repo_item_attachments.go), so add explicit
validation in the API handler(s) that call the repository attachment methods:
check both name and path are non-empty, within length limits, match an allowed
character set (or whitelist), normalize with filepath.Clean and then reject
values that are absolute (start with "/") or contain path traversal (cleaned
starts with ".." or contains ".." segments or differs from a safe relative
representation), and return a 4xx error before invoking any repo methods;
implement this validation in the handler(s) that parse the incoming request and
call the repo attachment functions so the data layer continues to accept raw IDs
but only after the API layer has validated them.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9e9b5dc7-a03c-42fb-afb7-809396468b4a

📥 Commits

Reviewing files that changed from the base of the PR and between 01fbe70 and 7cf7d3e.

📒 Files selected for processing (4)
  • backend/internal/core/services/service_items_attachments_external_test.go
  • backend/internal/data/repo/repo_item_attachments.go
  • backend/internal/data/repo/repo_item_attachments_test.go
  • frontend/composables/use-preferences.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/composables/use-preferences.test.ts
  • backend/internal/core/services/service_items_attachments_external_test.go

szaiser pushed a commit to szaiser/homebox that referenced this pull request May 12, 2026
- Add ssrfSafeDialContext: custom DialContext that rejects loopback,
  link-local (incl. AWS/GCP/Azure IMDS 169.254.169.254), RFC1918,
  shared (RFC6598), and ULA ranges; public hostnames are unrestricted.
  Both literal IPs and DNS-resolved names are validated before dialing.
- Add blockedCIDRs package-level var (lazy-initialised via IIFE).
- Add checkBlockedIP helper.
- Wire ssrfSafeDialContext into proxyHTTPClient Transport.DialContext.
- Validate baseURL scheme (http:// or https://) before proxying.
- Fix truncation false-positive: buffer with LimitReader(body, max+1),
  detect len(buf) > max and return 502 before writing any headers,
  so callers never receive a partial 200 with invalid JSON.
- Reject known-oversized responses via Content-Length check upfront.
- Fix loadIntegrationSettings (edit.vue): drop raw error from console.warn;
  use typeof guards instead of unsafe string casts.

Resolves CodeRabbit review comments on PR sysadminsmedia#1492.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/app/api/handlers/v1/v1_ctrl_integration_proxy.go`:
- Around line 194-200: The outbound request is created with http.NewRequest
which loses the incoming request's context; replace it with
http.NewRequestWithContext(req.Context(), http.MethodGet, upstream, nil) (or the
handler's ctx variable) so client cancellations and deadlines propagate
upstream, then continue to set the Authorization header and call
proxyHTTPClient.Do(req) as before and preserve the existing error handling via
validate.NewRequestError.
- Around line 200-203: The upstream URL is logged verbatim in the error path,
which can leak query strings or credentials; before calling log.Err(...) replace
the raw upstream with a sanitized version (e.g., parse upstream with net/url,
clear u.User and u.RawQuery/Fragment, then build a string like u.Scheme + "://"
+ u.Host + u.Path or simply u.Host + u.Path) and use that sanitized value in the
.Str("upstream", ...) field; implement this directly where
proxyHTTPClient.Do(req) error is handled (the log.Err(err).Str("integration",
name).Str("upstream", upstream).Msg(...) call) or extract the logic into a small
helper function (e.g., sanitizeUpstreamURL) and call it before logging.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0dfd9b0a-6960-425c-9d79-69e51c6182ba

📥 Commits

Reviewing files that changed from the base of the PR and between 7cf7d3e and 7c01b94.

📒 Files selected for processing (2)
  • backend/app/api/handlers/v1/v1_ctrl_integration_proxy.go
  • frontend/pages/item/[id]/index/edit.vue
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/pages/item/[id]/index/edit.vue

Comment thread backend/app/api/handlers/v1/v1_ctrl_integration_proxy.go Outdated
Comment thread backend/app/api/handlers/v1/v1_ctrl_integration_proxy.go
@szaiser
Copy link
Copy Markdown
Contributor Author

szaiser commented May 13, 2026

@coderabbitai resume

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 13, 2026

✅ Actions performed

Reviews resumed.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
backend/internal/core/services/service_items_attachments_external_test.go (1)

149-150: ⚡ Quick win

Assert no persistence on rejected source type (security contract).

This test checks the error path, but it does not verify the “before any DB write” guarantee. Add a post-check that the entity still has zero attachments after the call to lock in the allowlist/no-write behavior for untrusted sourceType input.

Suggested test hardening
 func TestEntityService_AttachmentAddExternalLink_UnknownSourceType(t *testing.T) {
 	svc := &EntityService{repo: tRepos}
 	entity := newExternalLinkEntity(t)

 	_, err := svc.AttachmentAddExternalLink(tCtx, entity.ID, "unknown-source", "42", "Unknown", attachment.TypeAttachment)
 	assert.Error(t, err)
+
+	latest, getErr := svc.repo.Entities.GetOneByGroup(tCtx, tCtx.GID, entity.ID)
+	require.NoError(t, getErr)
+	assert.Empty(t, latest.Attachments)
 }

As per coding guidelines, service-layer behavior should be validated in service tests; this strengthens the rejection-path contract in backend/internal/core/services/.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/core/services/service_items_attachments_external_test.go`
around lines 149 - 150, The test calls svc.AttachmentAddExternalLink(tCtx,
entity.ID, "unknown-source", "42", "Unknown", attachment.TypeAttachment) and
asserts an error but doesn't verify no DB write occurred; after the call add a
retrieval of the entity via the same service/repo path (e.g., call svc.Get or
svc.EntityByID used elsewhere) or query the attachments for entity.ID and assert
length is zero to ensure no attachment was persisted when sourceType is
untrusted, preserving the “no-write on rejected source” contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@backend/internal/core/services/service_items_attachments_external_test.go`:
- Around line 149-150: The test calls svc.AttachmentAddExternalLink(tCtx,
entity.ID, "unknown-source", "42", "Unknown", attachment.TypeAttachment) and
asserts an error but doesn't verify no DB write occurred; after the call add a
retrieval of the entity via the same service/repo path (e.g., call svc.Get or
svc.EntityByID used elsewhere) or query the attachments for entity.ID and assert
length is zero to ensure no attachment was persisted when sourceType is
untrusted, preserving the “no-write on rejected source” contract.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 16c4eef5-1245-416f-8352-e998fd3ca406

📥 Commits

Reviewing files that changed from the base of the PR and between 7c01b94 and 1ed1bc6.

📒 Files selected for processing (11)
  • backend/app/api/handlers/v1/v1_ctrl_integration_proxy.go
  • backend/internal/core/services/service_items_attachments_external_test.go
  • backend/internal/data/repo/repo_item_attachments.go
  • backend/internal/data/repo/repo_item_attachments_test.go
  • frontend/components/Item/AttachmentsList.vue
  • frontend/composables/use-preferences.test.ts
  • frontend/lib/integration-adapters.test.ts
  • frontend/lib/integration-adapters.ts
  • frontend/pages/item/[id]/index/edit.vue
  • frontend/pages/profile.vue
  • frontend/stores/integration-cache.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • frontend/pages/profile.vue
  • backend/internal/data/repo/repo_item_attachments.go
  • frontend/stores/integration-cache.ts
  • frontend/composables/use-preferences.test.ts
  • frontend/lib/integration-adapters.ts
  • backend/app/api/handlers/v1/v1_ctrl_integration_proxy.go
  • frontend/components/Item/AttachmentsList.vue
  • backend/internal/data/repo/repo_item_attachments_test.go

@szaiser
Copy link
Copy Markdown
Contributor Author

szaiser commented May 13, 2026

@tankerkiller125 From my perspective, the PR is ready for review. Will you find time to review anytime soon? Let me know if there is any way to assist (refactor, simplify, split, share info, ..)

DEIN_NAME added 10 commits May 16, 2026 10:12
…nt sources

Backend:
- Register paperless/immich source types in MimeTypeForSourceType and
  externalLinkMimeTypes; add MimeTypePaperlessDocument and MimeTypeImmichAsset
  constants
- Add a single generic reverse-proxy handler (HandleIntegrationProxy) that
  reads {name}_url / {name}_token from user settings; replaces the need for
  per-integration Go handlers
- Register GET /v1/integrations/{name}/proxy route

Frontend:
- New integration-adapters.ts registry: ServiceAdapter interface, extractors
  for Paperless and Immich, classifyDroppedUrl / detectServiceFromUrl helpers
- Rewrite AttachmentsList.vue to render rich cards for Paperless documents
  (thumbnail, correspondent, document type, tags, dates) and Immich assets
  (thumbnail, EXIF date, filename), fetched via the proxy endpoint
- Classify dropped URLs in edit.vue against the adapter registry so dragging
  a Paperless/Immich link creates a typed attachment immediately
- Add "Reclassify as Paperless" button to convert old link/url attachments
- Add Integrations section to profile.vue with URL + token fields for
  Paperless and Immich; save does GET→merge→PUT to avoid clobbering other
  settings
- Fix saveToServer settings-wipe bug in use-preferences.ts (was sending
  bare preference keys, wiping integration credentials on every preference
  change); extract pure helpers to preferences-utils.ts

Tests:
- integration-adapters.test.ts: 36 tests covering extraction, detection,
  classification, and the full adapter registry
- use-preferences.test.ts: 9 regression tests documenting the settings-wipe
  bug and verifying the GET→merge→PUT fix
- service_items_attachments_external_test.go: table-driven over all registered
  source types via knownSources, not hardcoded to one service
- repo_item_attachments_test.go: table-driven over all MIME types via
  externalLinkMimeTypeCases; add AllTypes, EmptyType, InvalidEntityID variants
- Add integration-cache Pinia store with service URL state,
  enriched-data cache (TTL-backed localStorage), and per-attachment
  fetch-state tracking (loading / ok / stale / error)
- Add integration-adapters library: SERVICE_ADAPTERS registry,
  per-service ID extractors (Paperless, Immich), classifyDroppedUrl()
- AttachmentsList: rewrite with liftAttachment() that promotes
  link/url attachments to service cards at display time based on
  configured service URL; demotes gracefully when service URL is
  removed (plain paperclip, no service hints)
- AttachmentsList: rich Paperless card with thumbnail, title from
  API, correspondent, document type, tags, page count, date, error
  states; rich Immich card with thumbnail and date
- AttachmentsList: meaningful error messages (auth failure vs.
  service unreachable) with hint to check API token in Profile
- edit.vue: drop handler always stores as link/url with full URL
  (URL is never lost in DB); reclassify-as-Paperless button removed
- edit.vue: fallback title uses hostname+path instead of generic
  'Paperless Document N'; real document title comes from hydration
- profile.vue: save Paperless/Immich URLs into integration-cache
  store so attachment list reacts reactively without page reload
…gistry

- Drop extractImmichAssetId, MIME_IMMICH constant and all Immich template
  blocks from AttachmentsList.vue
- Remove Immich settings fields (immichUrl/Token) from profile.vue and
  the corresponding en.json locale keys
- Purge Immich entry from SERVICE_ADAPTERS in integration-adapters.ts
- Remove all Immich test cases; add 2 new tests (sub-path install, id-
  extraction failure edge-case) — suite grows to 25 passing tests
…dia#1492

- backend: replace http.DefaultClient with dedicated proxyHTTPClient
  (30s timeout, bounded transport) to prevent upstream hangs (critical)
- backend: cap proxy response at 10 MB via io.LimitReader to prevent
  memory exhaustion from unbounded upstream responses (major)
- frontend: remove vestigial immich_url / immich_token fields from
  integrationSettings in edit.vue; these were never used after the
  Immich removal (minor)
- frontend: add console.warn in loadIntegrationSettings on API failure
  instead of silently returning (minor)
- frontend: replace hardcoded service-linked toast string with i18n key
  items.toast.service_linked (minor)
- frontend: replace hardcoded English strings in describeRequestError
  with i18n keys under components.item.attachments_list.errors (minor)
- frontend: add console.warn in integration-adapters.ts catch block
  when baseUrl fails to parse (minor)
- locales: add items.toast.service_linked and
  components.item.attachments_list.errors.{auth_failed,request_failed,
  service_unreachable} keys to en.json
- Remove MimeTypeImmichAsset constant from repo_item_attachments.go
- Remove immich from externalLinkMimeTypes slice
- Remove immich case from MimeTypeForSourceType()
- Remove immich test block from repo_item_attachments_test.go
  (TestMimeTypeForSourceType and externalLinkMimeTypeCases)
- Remove immich entry from knownSources in
  service_items_attachments_external_test.go

Resolves CodeRabbit review comment on PR sysadminsmedia#1492 (immich inconsistency).
Remove immich_url and immich_token from INTEGRATION_KEYS and the
merge round-trip test scenario. Paperless-only data is sufficient to
cover the same invariant.
- Add ssrfSafeDialContext: custom DialContext that rejects loopback,
  link-local (incl. AWS/GCP/Azure IMDS 169.254.169.254), RFC1918,
  shared (RFC6598), and ULA ranges; public hostnames are unrestricted.
  Both literal IPs and DNS-resolved names are validated before dialing.
- Add blockedCIDRs package-level var (lazy-initialised via IIFE).
- Add checkBlockedIP helper.
- Wire ssrfSafeDialContext into proxyHTTPClient Transport.DialContext.
- Validate baseURL scheme (http:// or https://) before proxying.
- Fix truncation false-positive: buffer with LimitReader(body, max+1),
  detect len(buf) > max and return 502 before writing any headers,
  so callers never receive a partial 200 with invalid JSON.
- Reject known-oversized responses via Content-Length check upfront.
- Fix loadIntegrationSettings (edit.vue): drop raw error from console.warn;
  use typeof guards instead of unsafe string casts.

Resolves CodeRabbit review comments on PR sysadminsmedia#1492.
Add doc comments to three undocumented functions:
- checkBlockedIP (v1_ctrl_integration_proxy.go)
- isExternalLink (repo_item_attachments.go)
- CreateExternalLink (repo_item_attachments.go)

Brings docstring coverage from 57% to 100% for the changed functions.
- Use http.NewRequestWithContext(r.Context(), ...) so client disconnects
  and request deadlines propagate to the upstream call, rather than
  running until the 30s client timeout.
- Log only host+path (via url.Parse) on upstream failure to avoid
  leaking query strings or accidentally embedded credentials from
  baseURL into server logs.
DEIN_NAME added 2 commits May 16, 2026 10:12
Backend:
- proxy: allocate net.Dialer once before fast-path/loop; remove dead else
  branch (url.Parse cannot fail after http/https prefix check); drop
  redundant w.WriteHeader(http.StatusOK) before w.Write (implicit)
- attachments repo: unify MIME registry into single sourceTypeMIMEs map so
  MimeTypeForSourceType and externalLinkMimeTypes are always in sync; replace
  linear isExternalLink loop with slices.Contains; add guard in
  CreateExternalLink to reject unknown MIME types up-front
- attachments repo test: convert TestMimeTypeForSourceType to table-driven
  with t.Run sub-tests
- service test: fix MultipleAttachments assertion to assert.Equal (entity is
  fresh, count must be exact); remove redundant NoBlobStorage test (fully
  covered by SourceTypes)

Frontend:
- integration-adapters: remove dead ?? adapter.extractId(url) fallback in
  classifyDroppedUrl (primary call returning null means no match)
- integration-adapters test: tighten mimeType assertion to full MIME regex
- use-preferences test: derive INTEGRATION_KEYS from SERVICE_ADAPTERS so new
  adapters are covered automatically; remove 'old broken approach' test
  (assertions duplicate what 'never includes integration keys' already checks)
- integration-cache store: remove unnecessary try/catch from lsDelete
  (localStorage.removeItem never throws); simplify getEnrichedData return
  type from 'unknown | null' to 'unknown'
- AttachmentsList: replace MIME_PAPERLESS literal with adapter registry lookup
  via getAdapterByMimeType; replace hydrateHandlers dispatch table (overkill
  for one entry) with direct if branch; simplify mimeToServiceName to one line
- edit.vue: remove loadIntegrationSettings (redundant api.user.getSettings
  call on every mount) and integrationSettings reactive; build settings map
  from useIntegrationCacheStore at drop time
- profile.vue: loop over SERVICE_ADAPTERS in loadIntegrationSettings and
  saveIntegrationSettings so new adapters are picked up automatically
…ternalLink

The IIFE to derive a []string from the map was more lines than the original
two-registry approach it replaced. Just range sourceTypeMIMEs directly in
isExternalLink – same single-source-of-truth guarantee, no extra allocation.
@szaiser szaiser force-pushed the feat/paperless-immich-attachement-links branch from 1ed1bc6 to 30e18e2 Compare May 16, 2026 08:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⬆️ enhancement New feature or request go Pull requests that update Go code review needed A review is needed on this PR or Issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant