Skip to content

fix: print label uses wrong tenant on mobile#1480

Open
lhns wants to merge 1 commit into
sysadminsmedia:mainfrom
lhns:fix/labelmaker-mobile-tenant
Open

fix: print label uses wrong tenant on mobile#1480
lhns wants to merge 1 commit into
sysadminsmedia:mainfrom
lhns:fix/labelmaker-mobile-tenant

Conversation

@lhns

@lhns lhns commented May 7, 2026

Copy link
Copy Markdown

What type of PR is this?

  • bug

What this PR does / why we need it:

On mobile, opening Print Label on an item from a non-default collection rendered the label for a different item - the item that happened to share the same assetId in the default collection. Switching the mobile browser to "desktop mode" worked around it; on desktop it always worked.

Root cause

LabelMaker.vue#getLabelUrl read the active tenant from useCollections().selectedId and appended it as a ?tenant= query param. selectedId is a module-level ref that is only populated when Collection/Selector.vue runs useCollections().load() in its onMounted hook.

The Selector lives inside the layout's <Sidebar>. On desktop the sidebar is a regular always-mounted <div>. On mobile (components/ui/sidebar/Sidebar.vue lines 29–43) it's rendered inside a <Sheet> (Reka UI DialogPortal / DialogContent), which is conditionally rendered - its slot is not mounted until the user opens the hamburger menu. Until that happens, selectedId.value is null, so:

  1. The label URL is built without ?tenant=….
  2. The backend's mwTenant middleware falls back to user.DefaultGroupID.
  3. HandleGetAssetLabel calls QueryByAssetID(auth, defaultGroupID, assetID). Because asset ids are tenant-scoped sequential numbers (e.g. 000-001), the lookup happily returns a different item with the same id from the default collection.

This also explains the "no network request on mobile" symptom: with the tenant param missing, the URL on mobile is byte-identical to the cached URL from a previous visit to the default collection's matching asset id, so the browser serves it from cache without firing a request. On desktop the URL includes a unique tenant uuid, so it never collides with the cache.

Fix

  • frontend/components/global/LabelMaker.vue - read the active tenant from useViewPreferences().value.collectionId instead of useCollections().selectedId. Preferences are persisted via useLocalStorage and updated by useCollections.set() / load() whenever the user switches collection, so they don't depend on the Selector ever being mounted.

This is the same fix @tonyaellie applied to attachment URLs in lib/api/base/base-api.ts in 24e79952 ("fix: use prefs instead of use collection for attachments"). LabelMaker.vue was missed by that earlier pass.

Which issue(s) this PR fixes:

none

Special notes:

I audited every consumer of useCollections(). Two other files have the same root cause but a different user-visible symptom - both early-return when selectedCollection.value is null:

  • frontend/pages/collection/index.vue (members page) - loadMembers() (line 90), isActionDisabled (line 87), handleLeaveCollection (line 123).
  • frontend/pages/collection/index/settings.vue - loadSettings() (line 34), save() (line 94).

If a user deep-links / bookmarks /collection/members or /collection/settings on mobile and lands on it without ever opening the sidebar, they'll see an empty members list or a "Select a collection" empty state. Opening and closing the sidebar mounts the Selector once, populates selectedId, and the page works. The underlying API calls would actually succeed because base-api.ts already reads from prefs.

Two ways to fix these, both reasonable - happy to do either as a follow-up:

  1. Local fix, mirroring this PR: switch the selectedCollection.value checks/reads in those two files to use prefs.value.collectionId (and re-derive the collection from collections.value when the display name is needed).
  2. Layout-level fix: call void useCollections().load() once in layouts/default.vue's onMounted. load() already self-guards against duplicate / concurrent calls (see use-collections.ts:22-25), so this is safe and would populate selectedId for every authenticated page, fixing the latent collection-page issue and any future component that reads selectedId directly.

A couple of other call sites build /qrcode?data=… URLs without a tenant param (components/global/PageQRCode.vue, pages/reports/label-generator.vue), but the /qrcode endpoint just renders the supplied data string into a QR image, so the rendered output doesn't depend on the active tenant - no visible bug there.

Testing

  • In a multi-collection account with asset id 000-001 present in two collections, open the non-default collection's 000-001 item on a mobile viewport (or hard-reload mobile after clearing cache so the previous bad cache entry is gone).
  • Click Print Label. Confirm DevTools → Network shows a fresh request to /api/v1/labelmaker/asset/000-001?print=false&tenant=<non-default-uuid> and the rendered image shows the correct item's name and parent location.
  • Repeat on desktop - no regression.
  • Test an item without an assetId (LabelMaker is invoked with type="item" / id=item.id) and a location detail page (type="location") to confirm the other branches of getLabelUrl() still work.

Disclaimer: I have written this MR with the help of Claude Code

@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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: aeb292b8-1439-4048-a610-0d1619bd7178

📥 Commits

Reviewing files that changed from the base of the PR and between 9bd309d and 6e6d1a2.

📒 Files selected for processing (2)
  • frontend/layouts/default.vue
  • frontend/lib/api/base/base-api.ts
💤 Files with no reviewable changes (2)
  • frontend/lib/api/base/base-api.ts
  • frontend/layouts/default.vue

Summary by CodeRabbit

  • Bug Fixes
    • Collections are loaded earlier during app startup to ensure collection data is reliably available for UI flows.
    • API requests now resolve tenant context from the currently selected collection, fixing incorrect collection context in attachments and related requests.

Walkthrough

Layout mount now calls useCollections().load() and BaseAPI.authURL sources its tenant query parameter from useCollections().selectedId.value instead of view preferences.

Changes

Collection Initialization and Auth Tenant

Layer / File(s) Summary
Collection Loading and Auth URL Tenant Source
frontend/layouts/default.vue, frontend/lib/api/base/base-api.ts
onMounted invokes useCollections().load() before location refresh and modal logic. BaseAPI.authURL sets params.tenant from useCollections().selectedId.value when truthy, replacing the previous view-preferences source.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • sysadminsmedia/homebox#1303: Also updates URL query-parameter generation to include a tenant derived from useCollections().selectedId.

Suggested reviewers

  • tonyaellie
  • tankerkiller125

Security Recommendations

  • Verify backend enforces tenant authorization regardless of the client-provided tenant query parameter.
  • Ensure useCollections().selectedId.value cannot be manipulated to access other tenants; validate and sanitize on the server.
  • Consider handling races where auth URLs are generated before useCollections().load() completes; ensure missing tenant is safely handled.

Collections wake at mount, a tidy start,
Tenants follow where the selector leads,
Auth links now borrow the chosen part,
Quiet loads hum through startup deeds. 🎐

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main bug being fixed: the print label feature using the wrong tenant context on mobile devices.
Description check ✅ Passed The description comprehensively covers all required template sections with detailed root cause analysis, fix rationale, and testing instructions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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

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.

@tonyaellie

Copy link
Copy Markdown
Collaborator

Thanks for such a thorough investigate!

I think adding a voided call to the collection load function somewhere in layout probably makes most sense as it will fix other bugs, I also believe (tho im far from certain) that using the selectedCollection.value instead of prefs has some advantage.

I think that I switched base api because using the selectedCollection.value caused an infinite loop.

In the case we do add a call to load the collection in the layout then switching will be unecesary?

LabelMaker built the label URL from useCollections().selectedId, which is
only populated when Collection/Selector.vue mounts. On mobile the sidebar
(and the Selector inside it) lives in a Sheet that is conditionally
rendered, so until the user opens the hamburger menu, selectedId stays
null and the ?tenant= query param is omitted from the label URL. The
backend then falls back to the user's default group, returning the wrong
tenant's item — and because the URL is byte-identical between visits,
the browser serves a cached image without firing a request.

Switch to useViewPreferences().value.collectionId, matching the same
pattern that 24e7995 applied to base-api.ts for attachments.
@lhns lhns force-pushed the fix/labelmaker-mobile-tenant branch from 9bd309d to 6e6d1a2 Compare June 10, 2026 16:35

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@frontend/lib/api/base/base-api.ts`:
- Around line 80-84: BaseAPI.authURL() can miss tenant because ImageDialog
generates URLs once on open before useCollections().selectedId is populated; fix
by making URL generation reactive or waiting for collections: in ImageDialog.vue
modify the dialog open handler (the callback that currently builds URLs) to
either await useCollections().load() or watch useCollections().selectedId and
regenerate the attachment URLs when selectedId becomes non-null, so the tenant
query param is included reliably; keep BaseAPI.authURL() as the single source of
truth but ensure ImageDialog uses the latest useCollections().selectedId value
(or re-computes the URLs from BaseAPI.authURL()) instead of snapshotting a null
selectedId.
🪄 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: 8b047b0b-536d-419e-9517-ec0ce74a9a41

📥 Commits

Reviewing files that changed from the base of the PR and between d5b8efe and 9bd309d.

📒 Files selected for processing (2)
  • frontend/layouts/default.vue
  • frontend/lib/api/base/base-api.ts

Comment thread frontend/lib/api/base/base-api.ts Outdated
Comment on lines +80 to +84
const { selectedId } = useCollections();
const params: Record<string, string> = { access_token: this.attachmentToken };

const prefs = useViewPreferences();
if (prefs.value.collectionId) {
params.tenant = prefs.value.collectionId;
if (selectedId.value) {
params.tenant = selectedId.value;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify useCollections().load() error handling and selectedId initialization

# Check load() implementation for error cases
ast-grep --pattern $'export const useCollections = () => {
  $$$
  const load = async () => {
    $$$
  }
  $$$
}'

# Find all authURL call sites to verify they handle null tenant gracefully
rg -n 'authURL\(' frontend/ --type=ts --type=vue -C3

# Check if any code validates selectedId before calling authURL
rg -n 'selectedId.*authURL|authURL.*selectedId' frontend/ --type=ts --type=vue -C5

Repository: sysadminsmedia/homebox

Length of output: 8254


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## base-api.ts authURL implementation (lines ~50-130)"
nl -ba frontend/lib/api/base/base-api.ts | sed -n '50,140p'

echo
echo "## authURL call sites (Vue/TS/TSX/JS) - show a few context lines"
rg -n "authURL\(" frontend --glob '*.{vue,ts,tsx,js,jsx}' -S -C2

echo
echo "## Any guarding/validation around selectedId before authURL"
rg -n "selectedId(\.value)?[^\n]*\n[^\n]*authURL\(" frontend --glob '*.{vue,ts,tsx,js,jsx}' -S || true
rg -n "if\s*\(.*selectedId.*\)" frontend --glob '*.{vue,ts,tsx,js,jsx}' -S | head -n 80

echo
echo "## Where tenant is set/used in base-api.ts"
rg -n "tenant" frontend/lib/api/base/base-api.ts -S || true

Repository: sysadminsmedia/homebox

Length of output: 181


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="frontend/lib/api/base/base-api.ts"
echo "## base-api.ts (${FILE}) authURL implementation (lines ~60-140)"
awk 'NR>=60 && NR<=140 {printf "%d\t%s\n", NR, $0}' "$FILE"

echo
echo "## authURL call sites (frontend) with context"
rg -n "authURL\(" frontend --glob '*.{vue,ts,tsx,js,jsx}' -S -C2

echo
echo "## Any obvious guard patterns around selectedId before authURL"
rg -n "selectedId(\.value)?[^\n]*" frontend --glob '*.{vue,ts,tsx,js,jsx}' -S -C0 | head -n 120

echo
echo "## Search for base-api tenant usage patterns"
rg -n "access_token|tenant" "$FILE" -S

Repository: sysadminsmedia/homebox

Length of output: 8706


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## Find where useCollections().load() is called"
rg -n "useCollections\(\)\.load\(" frontend --glob '*.{ts,vue}' -S

echo
echo "## Find where useCollections().load (without immediate call) is referenced"
rg -n "useCollections\(\).*load" frontend --glob '*.{ts,vue}' -S

echo
echo "## List all uses of useCollections in frontend (for context)"
rg -n "useCollections\(" frontend --glob '*.{ts,vue}' -S

Repository: sysadminsmedia/homebox

Length of output: 1167


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# show the default layout call around the reported line
echo "## frontend/layouts/default.vue around useCollections().load()"
awk 'NR>=470 && NR<=540 {printf "%d\t%s\n", NR, $0}' frontend/layouts/default.vue

echo
echo "## frontend/pages/item/[id]/index.vue around api.authURL() usage"
awk 'NR>=120 && NR<=190 {printf "%d\t%s\n", NR, $0}' frontend/pages/item/[id]/index.vue

echo
echo "## frontend/pages/location/[id]/index/index.vue around api.authURL() usage"
awk 'NR>=80 && NR<=140 {printf "%d\t%s\n", NR, $0}' frontend/pages/location/[id]/index/index.vue

echo
echo "## frontend/components/Item/ImageDialog.vue around api.authURL() usage"
awk 'NR>=1 && NR<=120 {printf "%d\t%s\n", NR, $0}' frontend/components/Item/ImageDialog.vue

Repository: sysadminsmedia/homebox

Length of output: 10373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="frontend/composables/use-collections.ts"
echo "## frontend/composables/use-collections.ts (lines 1-140)"
awk 'NR>=1 && NR<=140 {printf "%d\t%s\n", NR, $0}' "$FILE"

echo
echo "## use-collections.ts (lines 140-240 if any)"
awk 'NR>=140 && NR<=240 {printf "%d\t%s\n", NR, $0}' "$FILE" || true

Repository: sysadminsmedia/homebox

Length of output: 4558


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="frontend/lib/api/base/base-api.ts"
echo "## base-api.ts imports (lines 1-60)"
awk 'NR>=1 && NR<=60 {printf "%d\t%s\n", NR, $0}' "$FILE"

Repository: sysadminsmedia/homebox

Length of output: 1917


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## Find where BaseAPI is constructed (attachmentToken usage)"
rg -n "new BaseAPI\(" frontend --glob '*.{ts,js,vue}' -S

echo
echo "## Find constructor param name usage 'attachmentToken' in api code"
rg -n "attachmentToken" frontend --glob '*.{ts,js,vue}' -S

echo
echo "## If there is a useUserApi() that sets attachmentToken, show it"
rg -n "function useUserApi|useUserApi\(" frontend --glob '*.{ts,js}' -S

Repository: sysadminsmedia/homebox

Length of output: 127


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## Search for useUserApi implementation"
rg -n "export (const|function) useUserApi|useUserApi\s*=" frontend --glob '*.{ts,js}' -S

echo
echo "## Search for attachmentToken assignments/usages"
rg -n "\battachmentToken\b" frontend --glob '*.{ts,js,vue}' -S

echo
echo "## Search for .authURL() method usage only (to see how api is obtained)"
rg -n "\.authURL\(" frontend --glob '*.{ts,vue,js}' -S

echo
echo "## Search for BaseAPI authURL wrappers"
rg -n "authURL\s*:\s*|authURL\(" frontend/lib/api --glob '*.{ts}' -S

Repository: sysadminsmedia/homebox

Length of output: 3812


Ensure tenant is reliably included (and doesn’t snapshot as null) when building attachment authURLs

BaseAPI.authURL() only adds tenant when useCollections().selectedId.value is truthy (frontend/lib/api/base/base-api.ts:78-88). selectedId starts as null and is only populated by useCollections().load() (frontend/composables/use-collections.ts:11-20, load()), which runs in frontend/layouts/default.vue via onMounted() and is skipped on / (default.vue:506-508, use-collections.ts:17-20). If collections load fails, selectedId remains null (error is caught/logged; finally only flips refreshing).

In frontend/components/Item/ImageDialog.vue, URLs are generated once inside the dialog open callback (ImageDialog.vue:32-47) with no guard/waiting for selectedId, so opening the dialog before collections finish loading (or after a failure) will omit tenant and can cause the backend fallback collection behavior.

Security: treat the tenant query param as untrusted—server-side authorization must be derived from access_token (and validate/ignore tenant) to prevent cross-tenant attachment access.

🤖 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/lib/api/base/base-api.ts` around lines 80 - 84, BaseAPI.authURL()
can miss tenant because ImageDialog generates URLs once on open before
useCollections().selectedId is populated; fix by making URL generation reactive
or waiting for collections: in ImageDialog.vue modify the dialog open handler
(the callback that currently builds URLs) to either await
useCollections().load() or watch useCollections().selectedId and regenerate the
attachment URLs when selectedId becomes non-null, so the tenant query param is
included reliably; keep BaseAPI.authURL() as the single source of truth but
ensure ImageDialog uses the latest useCollections().selectedId value (or
re-computes the URLs from BaseAPI.authURL()) instead of snapshotting a null
selectedId.

@lhns lhns force-pushed the fix/labelmaker-mobile-tenant branch from 6e6d1a2 to 6dfb7ba Compare June 10, 2026 19:19
@lhns

lhns commented Jun 26, 2026

Copy link
Copy Markdown
Author

Hey! I'm not sure if I understood it correctly but I tried to play around with that approach. In the end I wasn't quite sure about its correctness, so I left it like this for now.
Shouldn't all api endpoints be changed to require a tenant id, so working on the wrong tenant cannot happen by accident?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants