Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 6 additions & 86 deletions backend/go.sum

Large diffs are not rendered by default.

20 changes: 17 additions & 3 deletions frontend/components/Item/CreateModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,12 @@
import BaseModal from "@/components/App/CreateModal.vue";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import type { EntityCreate, EntityTemplateOut, EntityTemplateSummary, EntityOut } from "~~/lib/api/types/data-contracts";
import type {
EntityCreate,
EntityTemplateOut,
EntityTemplateSummary,
EntityOut,
} from "~~/lib/api/types/data-contracts";
import { useTagStore } from "~/stores/tags";
import { useLocationStore } from "~~/stores/locations";
import MdiBarcode from "~icons/mdi/barcode";
Expand Down Expand Up @@ -390,7 +395,11 @@
if (et?.defaultTemplateId && et.defaultTemplate) {
const { data, error } = await api.templates.get(et.defaultTemplateId);
if (!error && data) {
selectedTemplate.value = { id: data.id, name: data.name, description: data.description } as EntityTemplateSummary;
selectedTemplate.value = {
id: data.id,
name: data.name,
description: data.description,
} as EntityTemplateSummary;
templateData.value = data;
form.quantity = data.defaultQuantity;
if (data.defaultName) form.name = data.defaultName;
Expand Down Expand Up @@ -635,6 +644,11 @@
return;
}

if (form.quantity < 0) {
toast.error(t("items.toast.quantity_cannot_negative"));
return;
}
Comment on lines +647 to +650

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 | 🔴 Critical

Wrong i18n key — toast will display the raw key string instead of a translated message. 🐰🌍

The guard calls t("components.item.create_modal.toast.quantity_cannot_negative"), but that path doesn't exist in frontend/locales/en.json. The actual key lives under items.toast.quantity_cannot_negative (see en.json line 620), and the sibling frontend/pages/item/[id]/index/edit.vue (lines 97-100) and the PR description both use that path. With vue-i18n's default missing-handler the user will see the literal string components.item.create_modal.toast.quantity_cannot_negative in the toast — in every language. Switch to the existing key.

🥕 Proposed fix
     if (form.quantity < 0) {
-      toast.error(t("components.item.create_modal.toast.quantity_cannot_negative"));
+      toast.error(t("items.toast.quantity_cannot_negative"));
       return;
     }

🔐 Security recommendation (carried over): this remains a client-side check only. Please confirm the backend handlers behind api.items.create and api.templates.createItem reject quantity < 0 (and ideally non-finite values) so a crafted request can't bypass the UI guard.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/Item/CreateModal.vue` around lines 647 - 650, The toast
uses the wrong i18n key so users will see the raw key; in CreateModal.vue update
the t(...) call used in the quantity guard (the code referencing form.quantity
and toast.error) to use the existing key "items.toast.quantity_cannot_negative"
instead of "components.item.create_modal.toast.quantity_cannot_negative", and
confirm server-side validation for api.items.create / api.templates.createItem
rejects quantity < 0 (and non-finite values) so the UI-only guard can't be
bypassed.


loading.value = true;

if (shift?.value) close = false;
Expand All @@ -657,7 +671,7 @@
} else {
// Normal item creation without template
const out: EntityCreate = {
parentId: form.parentId || form.location.id as string,
parentId: form.parentId || (form.location.id as string),
name: form.name,
quantity: form.quantity,
description: form.description,
Expand Down
9 changes: 2 additions & 7 deletions frontend/components/Location/CreateModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,7 @@

<template v-if="showAdvanced">
<TagSelector v-model="form.tags" :tags="tags ?? []" />
<FormTextArea
v-model="form.notes"
label="Notes"
:max-length="1000"
/>
<FormTextArea v-model="form.notes" label="Notes" :max-length="1000" />
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
</template>

<div class="mt-4 flex flex-row-reverse">
Expand Down Expand Up @@ -142,8 +138,7 @@
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import BaseModal from "@/components/App/CreateModal.vue";
import type { EntityTypeSummary } from "~~/lib/api/types/data-contracts";
import type { EntitySummary } from "~~/lib/api/types/data-contracts";
import type { EntityTypeSummary, EntitySummary } from "~~/lib/api/types/data-contracts";
import { AttachmentTypes } from "~~/lib/api/types/non-generated";
import { useDialog, useDialogHotkey } from "~/components/ui/dialog-provider";
import { useTagStore } from "~/stores/tags";
Expand Down
8 changes: 7 additions & 1 deletion frontend/components/Scanner/ARControls.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@
<div class="pointer-events-none absolute inset-0 z-20 flex flex-col justify-between">
<!-- Top bar -->
<div class="pointer-events-auto flex items-center gap-3 bg-background/60 p-3 backdrop-blur-sm">
<Button variant="ghost" size="icon" :aria-label="t('scanner_ar.back')" :title="t('scanner_ar.back')" @click="$emit('back')">
<Button
variant="ghost"
size="icon"
:aria-label="t('scanner_ar.back')"
:title="t('scanner_ar.back')"
@click="$emit('back')"
>
<MdiArrowLeft class="size-5" />
</Button>
<span class="text-sm font-semibold text-foreground">{{ t("scanner_ar.title") }}</span>
Expand Down
4 changes: 3 additions & 1 deletion frontend/lib/api/classes/items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,9 @@ export class ItemsApi extends BaseAPI {
// =========================================================================

async getLocations(q: LocationsQuery = { filterChildren: false }) {
const resp = await this.http.get<{ items: EntitySummary[] }>({ url: route("/entities", { ...q, isLocation: true }) });
const resp = await this.http.get<{ items: EntitySummary[] }>({
url: route("/entities", { ...q, isLocation: true }),
});
// Unwrap paginated response to flat array for backward compat
return {
...resp,
Expand Down
36 changes: 12 additions & 24 deletions frontend/pages/collection/index/entity-types.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,7 @@
import { Badge } from "@/components/ui/badge";
import { Card } from "@/components/ui/card";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { useDialog } from "@/components/ui/dialog-provider";
import { DialogID } from "~/components/ui/dialog-provider/utils";
import FormTextField from "~/components/Form/TextField.vue";
Expand Down Expand Up @@ -98,7 +92,11 @@
updateForm.icon = et.icon;
updateForm.isLocation = et.isLocation;
updateTemplate.value = et.defaultTemplate
? { id: et.defaultTemplate.id, name: et.defaultTemplate.name, description: et.defaultTemplate.description } as EntityTemplateSummary
? ({
id: et.defaultTemplate.id,
name: et.defaultTemplate.name,
description: et.defaultTemplate.description,
} as EntityTemplateSummary)
: null;
openDialog(DialogID.UpdateEntityType);
}
Expand Down Expand Up @@ -154,13 +152,7 @@
<DialogTitle>Create Entity Type</DialogTitle>
</DialogHeader>
<form class="flex flex-col gap-3" @submit.prevent="create">
<FormTextField
v-model="createForm.name"
:autofocus="true"
label="Name"
:max-length="255"
:min-length="1"
/>
<FormTextField v-model="createForm.name" :autofocus="true" label="Name" :max-length="255" :min-length="1" />

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 | 🟡 Minor

Use translated labels instead of hardcoded "Name" in form fields.

Both changed FormTextField usages still hardcode user-facing text. Switch label to t(...) so the dialogs are localizable.

Suggested change
-          <FormTextField v-model="createForm.name" :autofocus="true" label="Name" :max-length="255" :min-length="1" />
+          <FormTextField v-model="createForm.name" :autofocus="true" :label="t('common.name')" :max-length="255" :min-length="1" />
...
-          <FormTextField v-model="updateForm.name" :autofocus="true" label="Name" :max-length="255" :min-length="1" />
+          <FormTextField v-model="updateForm.name" :autofocus="true" :label="t('common.name')" :max-length="255" :min-length="1" />

Security recommendation: keep server-side validation/authorization as the source of truth for entity-type create/update/delete, since UI constraints can be bypassed.

As per coding guidelines "Check for hardcoded strings in UI components that should be translatable."

Also applies to: 173-173

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/pages/collection/index/entity-types.vue` at line 155, Replace
hardcoded label text passed to the FormTextField components with the translation
function; locate the two usages bound to createForm.name (the FormTextField at
the create dialog and the one at line ~173) and change label="Name" to
label=t('...') using the appropriate i18n key (e.g. t('entityType.name') or
existing key convention), ensuring you import or reference the same translation
function used elsewhere in this component so both FormTextField instances use
translated labels.

<FormCheckbox v-model="createForm.isLocation" label="Is a container / location type" />
<TemplateSelector v-model="createTemplate" />

Expand All @@ -178,13 +170,7 @@
<DialogTitle>Update Entity Type</DialogTitle>
</DialogHeader>
<form class="flex flex-col gap-3" @submit.prevent="update">
<FormTextField
v-model="updateForm.name"
:autofocus="true"
label="Name"
:max-length="255"
:min-length="1"
/>
<FormTextField v-model="updateForm.name" :autofocus="true" label="Name" :max-length="255" :min-length="1" />
<FormCheckbox v-model="updateForm.isLocation" label="Is a container / location type" />
<TemplateSelector v-model="updateTemplate" />

Expand All @@ -196,7 +182,7 @@
</Dialog>

<!-- Page Content -->
<div class="flex items-center justify-between mb-4">
<div class="mb-4 flex items-center justify-between">
<h3 class="text-lg font-medium">Entity Types</h3>
<Button size="sm" @click="openDialog(DialogID.CreateEntityType)">
<MdiPlus class="mr-1 size-4" />
Expand All @@ -207,7 +193,9 @@
<div v-if="entityTypes && entityTypes.length > 0" class="space-y-2">
<Card v-for="et in entityTypes" :key="et.id" class="p-4">
<div class="flex items-center gap-3">
<div class="flex size-10 shrink-0 items-center justify-center rounded-full bg-secondary text-secondary-foreground">
<div
class="flex size-10 shrink-0 items-center justify-center rounded-full bg-secondary text-secondary-foreground"
>
<MdiMapMarkerOutline v-if="et.isLocation" class="size-5" />
<MdiPackageVariantClosed v-else class="size-5" />
</div>
Expand Down
5 changes: 5 additions & 0 deletions frontend/pages/item/[id]/index/edit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@
return;
}

if (item.value.quantity < 0) {
toast.error(t("items.toast.quantity_cannot_negative"));
return;
}

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 | 🟡 Minor

Same hop, slightly different burrow — and there's a side-door update path. 🐰🚪

Mirror of the CreateModal feedback, plus one extra:

  1. NaN/empty bypass. The number form fields on this page (e.g., line 632-639) use v-model.number with type="number" and no min. An empty/cleared quantity becomes "" or NaN, and NaN < 0 is false, so the guard lets it through. Consider !Number.isFinite(item.value.quantity) || item.value.quantity < 0. Adding min="0" to the number-type FormTextField branch (and/or to the schema in mainFields) would also harden the UI layer.
  2. Sibling update path bypasses the guard. syncChildEntityLocations() (lines 475-501) also calls api.items.update(itemId.value, payload) and spreads ...item.value into the payload — including a possibly-negative quantity set just before flipping the sync switch. If you want the rule to be "no negative quantities ever leave this page," that path needs the same check (or factor the validation into a shared helper).
  3. 🔐 Security recommendation: Treat this guard as UX, not enforcement. Please confirm the backend items.update rejects negative (and non-finite) quantities — otherwise a hand-crafted PATCH still wins.
🛠️ Proposed shared guard + sibling path coverage
+  function hasInvalidQuantity(): boolean {
+    return !Number.isFinite(item.value.quantity) || item.value.quantity < 0;
+  }
+
   async function saveItem(redirect: boolean) {
     if (!item.value.parent?.id && !parent.value?.id) {
       toast.error(t("items.toast.failed_save_no_location"));
       return;
     }

-    if (item.value.quantity < 0) {
+    if (hasInvalidQuantity()) {
       toast.error(t("items.toast.quantity_cannot_negative"));
       return;
     }
   async function syncChildEntityLocations() {
     if (!item.value.parent?.id && !parent.value?.id) {
       toast.error(t("items.toast.failed_save_no_location"));
       return;
     }
+
+    if (hasInvalidQuantity()) {
+      toast.error(t("items.toast.quantity_cannot_negative"));
+      return;
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/pages/item/`[id]/index/edit.vue around lines 97 - 100, The quantity
check currently allows NaN/empty and is bypassed by the sibling update path;
change the guard around item.value.quantity to reject non-finite values (e.g.,
use Number.isFinite) and negative values so it reads like
!Number.isFinite(item.value.quantity) || item.value.quantity < 0, update the
FormTextField number inputs to include min="0" (and/or add validation in
mainFields) to prevent clearing to NaN in the UI, and apply the same validation
before calling api.items.update in syncChildEntityLocations (or factor the
validation into a shared helper used by both the inline edit save and
syncChildEntityLocations) so negative/non-finite quantities never get sent to
the API; also ensure the backend enforces the rule server-side.


saving.value = true;

let purchasePrice = 0;
Expand Down
9 changes: 4 additions & 5 deletions frontend/pages/location/[id]/index.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
<script setup lang="ts">
definePageMeta({
middleware: ["auth"],
});
</script>
<script setup lang="ts"></script>

<template>
<BaseContainer>
Expand All @@ -12,4 +8,7 @@

<script lang="ts">
import BaseContainer from "@/components/Base/Container.vue";
definePageMeta({
middleware: ["auth"],
});
</script>
13 changes: 9 additions & 4 deletions frontend/pages/location/[id]/index/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,12 @@
else acc.attachments.push(attachment);
return acc;
},
{ attachments: [] as ItemAttachment[], warranty: [] as ItemAttachment[], manuals: [] as ItemAttachment[], receipts: [] as ItemAttachment[] }
{
attachments: [] as ItemAttachment[],
warranty: [] as ItemAttachment[],
manuals: [] as ItemAttachment[],
receipts: [] as ItemAttachment[],
}
);
});

Expand All @@ -171,12 +176,12 @@
type: "markdown",
text: location.value.notes,
},
...((location.value.fields || []).map(field => {
...(location.value.fields || []).map(field => {
return {
name: field.name,
text: field.textValue,
} as AnyDetail;
})),
}),
];

if (!preferences.value.showEmpty) {
Expand Down Expand Up @@ -224,7 +229,7 @@
<button
v-for="(photo, i) in photos"
:key="i"
class="group relative aspect-square overflow-hidden rounded-lg border bg-muted"
class="aspect-square group relative overflow-hidden rounded-lg border bg-muted"
@click="openImageDialog(photo, location.id)"
>
<img
Expand Down
Loading