Feat/paperless attachement links#1492
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (15)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (14)
Summary by CodeRabbitNew Features
Refactor
Tests
WalkthroughAdds 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. ChangesIntegration Services Support
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested reviewers
Security Recommendations
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches✨ Simplify 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.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (6)
frontend/composables/use-preferences.test.ts (1)
38-43: 💤 Low valueConsider using valid theme values in test data.
Line 39 uses
theme: "dark" as never, which suggests "dark" may not be a validDaisyThemevalue. Usingas neverbypasses type checking and reduces test quality.Recommend either:
- Use a valid theme value from the
DaisyThemetype (e.g., "homebox")- If "dark" is actually valid, remove the
as neverassertionNote: Line 67 in a later test uses
theme: "dark"withoutas 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 valueConsider improving type safety instead of using
as never.The
as neverassertion 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:
- Add runtime type validation before assignment
- Define a more specific type for the
settingsparameter that constrains it to valid preference value types- 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 winConsider 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
FormPasswordcomponent with toggle). You could either:
- Use
FormPasswordcomponent which has built-in toggle- 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 tradeoffConsider: Rate limiting per user for proxy endpoint.
Since users control the upstream URL, they could abuse this endpoint to:
- Hammer a third-party service with requests
- Use the proxy as a generic HTTP client
- 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 winHardcoded 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 tradeoffSilent 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
⛔ Files ignored due to path filters (1)
backend/go.sumis excluded by!**/*.sum
📒 Files selected for processing (16)
backend/app/api/handlers/v1/v1_ctrl_integration_proxy.gobackend/app/api/routes.gobackend/internal/core/services/service_items_attachments_external_test.gobackend/internal/data/repo/repo_item_attachments.gobackend/internal/data/repo/repo_item_attachments_test.godocs/todo-integration-features.mdfrontend/components/Item/AttachmentsList.vuefrontend/composables/preferences-utils.tsfrontend/composables/use-preferences.test.tsfrontend/composables/use-preferences.tsfrontend/lib/integration-adapters.test.tsfrontend/lib/integration-adapters.tsfrontend/locales/en.jsonfrontend/pages/item/[id]/index/edit.vuefrontend/pages/profile.vuefrontend/stores/integration-cache.ts
…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).
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
backend/app/api/handlers/v1/v1_ctrl_integration_proxy.go (1)
35-52: ⚡ Quick winUpdate 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 usesTokenauth, but Immich usesx-api-key, and most modern APIs useAuthorization: Bearer. The frontend adapter registry is already extensible, but the backend lacks a way to specify per-integration auth schemes.Either:
- Store auth scheme in settings (
{name}_auth_scheme, default"Token"), or- 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
📒 Files selected for processing (5)
backend/app/api/handlers/v1/v1_ctrl_integration_proxy.gofrontend/components/Item/AttachmentsList.vuefrontend/lib/integration-adapters.tsfrontend/locales/en.jsonfrontend/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
There was a problem hiding this comment.
🧹 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
nameandpathparameters 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
📒 Files selected for processing (4)
backend/internal/core/services/service_items_attachments_external_test.gobackend/internal/data/repo/repo_item_attachments.gobackend/internal/data/repo/repo_item_attachments_test.gofrontend/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
- 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.
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
backend/app/api/handlers/v1/v1_ctrl_integration_proxy.gofrontend/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
|
@coderabbitai resume |
✅ Actions performedReviews resumed. |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
backend/internal/core/services/service_items_attachments_external_test.go (1)
149-150: ⚡ Quick winAssert 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
sourceTypeinput.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
📒 Files selected for processing (11)
backend/app/api/handlers/v1/v1_ctrl_integration_proxy.gobackend/internal/core/services/service_items_attachments_external_test.gobackend/internal/data/repo/repo_item_attachments.gobackend/internal/data/repo/repo_item_attachments_test.gofrontend/components/Item/AttachmentsList.vuefrontend/composables/use-preferences.test.tsfrontend/lib/integration-adapters.test.tsfrontend/lib/integration-adapters.tsfrontend/pages/item/[id]/index/edit.vuefrontend/pages/profile.vuefrontend/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
|
@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, ..) |
…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.
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.
1ed1bc6 to
30e18e2
Compare
Compacted conversation---
What type of PR is this?
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:
GET /v1/integrations/{name}/proxy?path={relPath}— generic authenticated reverse-proxy; reads{name}_url/{name}_tokenfrom user settings, validates name and path, forwardsAuthorization: Token {token}MimeTypePaperlessDocument = "paperless/document"registered inMimeTypeForSourceType()integration-adapters.tsregistry (ServiceAdapterinterface +SERVICE_ADAPTERS);integration-cache.tsPinia store for URL state and enriched-data cache; Paperless card inAttachmentsList.vue(thumbnail, metadata, ⚠ error badge); drag-drop URL detection inedit.vue; Paperless URL + token fields inprofile.vueintegration-adapters.test.tsDesign decisions:
SERVICE_ADAPTERSentry on the frontend and one line inMimeTypeForSourceType()on the backend — no further structural changes.link/urlattachments 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 compatibleif/elsechains in drop detection, URL classification, and card hydrationWhich issue(s) this PR fixes:
Testing
npx vitest run lib/integration-adapters.test.ts— 25/25 passgo test ./internal/core/services ./internal/data/repo ./app/api/handlers/v1pass