├── .devcontainer
├── Dockerfile
└── devcontainer.json
├── .dockerignore
├── .github
├── DISCUSSION_TEMPLATE
│ └── ideas.yml
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.yml
│ └── config.yml
├── pull_request_template.md
├── scripts
│ └── update_currencies.py
└── workflows
│ ├── binaries-publish.yaml
│ ├── clear-stale-docker-images.yml
│ ├── docker-publish-rootless.yaml
│ ├── docker-publish.yaml
│ ├── e2e-partial.yaml
│ ├── partial-backend.yaml
│ ├── partial-frontend.yaml
│ ├── pull-requests.yaml
│ ├── update-currencies.yml
│ └── update-currencies
│ └── requirements.txt
├── .gitignore
├── .scaffold
└── model
│ ├── scaffold.yaml
│ └── templates
│ └── model.go
├── .vscode
├── launch.json
└── settings.json
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── Dockerfile.rootless
├── LICENSE
├── README.md
├── SECURITY.md
├── Taskfile.yml
├── backend
├── .gitignore
├── .golangci.yml
├── .goreleaser.yaml
├── app
│ ├── api
│ │ ├── app.go
│ │ ├── bgrunner.go
│ │ ├── demo.go
│ │ ├── handlers
│ │ │ ├── debughandlers
│ │ │ │ └── debug.go
│ │ │ └── v1
│ │ │ │ ├── assets
│ │ │ │ └── QRIcon.png
│ │ │ │ ├── controller.go
│ │ │ │ ├── helpers.go
│ │ │ │ ├── partials.go
│ │ │ │ ├── query_params.go
│ │ │ │ ├── v1_ctrl_actions.go
│ │ │ │ ├── v1_ctrl_assets.go
│ │ │ │ ├── v1_ctrl_auth.go
│ │ │ │ ├── v1_ctrl_group.go
│ │ │ │ ├── v1_ctrl_items.go
│ │ │ │ ├── v1_ctrl_items_attachments.go
│ │ │ │ ├── v1_ctrl_labelmaker.go
│ │ │ │ ├── v1_ctrl_labels.go
│ │ │ │ ├── v1_ctrl_locations.go
│ │ │ │ ├── v1_ctrl_maint_entry.go
│ │ │ │ ├── v1_ctrl_maintenance.go
│ │ │ │ ├── v1_ctrl_notifiers.go
│ │ │ │ ├── v1_ctrl_qrcode.go
│ │ │ │ ├── v1_ctrl_reporting.go
│ │ │ │ ├── v1_ctrl_statistics.go
│ │ │ │ └── v1_ctrl_user.go
│ │ ├── logger.go
│ │ ├── main.go
│ │ ├── middleware.go
│ │ ├── providers
│ │ │ ├── doc.go
│ │ │ ├── extractors.go
│ │ │ └── local.go
│ │ ├── routes.go
│ │ └── static
│ │ │ ├── docs
│ │ │ ├── docs.go
│ │ │ ├── swagger.json
│ │ │ └── swagger.yaml
│ │ │ └── public
│ │ │ └── .gitkeep
│ └── tools
│ │ └── typegen
│ │ └── main.go
├── cosign.key
├── cosign.pub
├── go.mod
├── go.sum
├── internal
│ ├── core
│ │ ├── currencies
│ │ │ ├── currencies.go
│ │ │ └── currencies.json
│ │ └── services
│ │ │ ├── all.go
│ │ │ ├── contexts.go
│ │ │ ├── contexts_test.go
│ │ │ ├── main_test.go
│ │ │ ├── reporting
│ │ │ ├── .testdata
│ │ │ │ └── import
│ │ │ │ │ ├── fields.csv
│ │ │ │ │ ├── minimal.csv
│ │ │ │ │ └── types.csv
│ │ │ ├── bill_of_materials.go
│ │ │ ├── eventbus
│ │ │ │ └── eventbus.go
│ │ │ ├── import.go
│ │ │ ├── io_row.go
│ │ │ ├── io_sheet.go
│ │ │ ├── io_sheet_test.go
│ │ │ ├── value_parsers.go
│ │ │ └── value_parsers_test.go
│ │ │ ├── service_background.go
│ │ │ ├── service_group.go
│ │ │ ├── service_items.go
│ │ │ ├── service_items_attachments.go
│ │ │ ├── service_items_attachments_test.go
│ │ │ ├── service_user.go
│ │ │ └── service_user_defaults.go
│ ├── data
│ │ ├── ent
│ │ │ ├── attachment.go
│ │ │ ├── attachment
│ │ │ │ ├── attachment.go
│ │ │ │ └── where.go
│ │ │ ├── attachment_create.go
│ │ │ ├── attachment_delete.go
│ │ │ ├── attachment_query.go
│ │ │ ├── attachment_update.go
│ │ │ ├── authroles.go
│ │ │ ├── authroles
│ │ │ │ ├── authroles.go
│ │ │ │ └── where.go
│ │ │ ├── authroles_create.go
│ │ │ ├── authroles_delete.go
│ │ │ ├── authroles_query.go
│ │ │ ├── authroles_update.go
│ │ │ ├── authtokens.go
│ │ │ ├── authtokens
│ │ │ │ ├── authtokens.go
│ │ │ │ └── where.go
│ │ │ ├── authtokens_create.go
│ │ │ ├── authtokens_delete.go
│ │ │ ├── authtokens_query.go
│ │ │ ├── authtokens_update.go
│ │ │ ├── client.go
│ │ │ ├── ent.go
│ │ │ ├── enttest
│ │ │ │ └── enttest.go
│ │ │ ├── external.go
│ │ │ ├── generate.go
│ │ │ ├── group.go
│ │ │ ├── group
│ │ │ │ ├── group.go
│ │ │ │ └── where.go
│ │ │ ├── group_create.go
│ │ │ ├── group_delete.go
│ │ │ ├── group_query.go
│ │ │ ├── group_update.go
│ │ │ ├── groupinvitationtoken.go
│ │ │ ├── groupinvitationtoken
│ │ │ │ ├── groupinvitationtoken.go
│ │ │ │ └── where.go
│ │ │ ├── groupinvitationtoken_create.go
│ │ │ ├── groupinvitationtoken_delete.go
│ │ │ ├── groupinvitationtoken_query.go
│ │ │ ├── groupinvitationtoken_update.go
│ │ │ ├── has_id.go
│ │ │ ├── hook
│ │ │ │ └── hook.go
│ │ │ ├── item.go
│ │ │ ├── item
│ │ │ │ ├── item.go
│ │ │ │ └── where.go
│ │ │ ├── item_create.go
│ │ │ ├── item_delete.go
│ │ │ ├── item_query.go
│ │ │ ├── item_update.go
│ │ │ ├── itemfield.go
│ │ │ ├── itemfield
│ │ │ │ ├── itemfield.go
│ │ │ │ └── where.go
│ │ │ ├── itemfield_create.go
│ │ │ ├── itemfield_delete.go
│ │ │ ├── itemfield_query.go
│ │ │ ├── itemfield_update.go
│ │ │ ├── label.go
│ │ │ ├── label
│ │ │ │ ├── label.go
│ │ │ │ └── where.go
│ │ │ ├── label_create.go
│ │ │ ├── label_delete.go
│ │ │ ├── label_query.go
│ │ │ ├── label_update.go
│ │ │ ├── location.go
│ │ │ ├── location
│ │ │ │ ├── location.go
│ │ │ │ └── where.go
│ │ │ ├── location_create.go
│ │ │ ├── location_delete.go
│ │ │ ├── location_query.go
│ │ │ ├── location_update.go
│ │ │ ├── maintenanceentry.go
│ │ │ ├── maintenanceentry
│ │ │ │ ├── maintenanceentry.go
│ │ │ │ └── where.go
│ │ │ ├── maintenanceentry_create.go
│ │ │ ├── maintenanceentry_delete.go
│ │ │ ├── maintenanceentry_query.go
│ │ │ ├── maintenanceentry_update.go
│ │ │ ├── migrate
│ │ │ │ ├── migrate.go
│ │ │ │ └── schema.go
│ │ │ ├── mutation.go
│ │ │ ├── notifier.go
│ │ │ ├── notifier
│ │ │ │ ├── notifier.go
│ │ │ │ └── where.go
│ │ │ ├── notifier_create.go
│ │ │ ├── notifier_delete.go
│ │ │ ├── notifier_query.go
│ │ │ ├── notifier_update.go
│ │ │ ├── predicate
│ │ │ │ └── predicate.go
│ │ │ ├── runtime.go
│ │ │ ├── runtime
│ │ │ │ └── runtime.go
│ │ │ ├── schema
│ │ │ │ ├── attachment.go
│ │ │ │ ├── auth_roles.go
│ │ │ │ ├── auth_tokens.go
│ │ │ │ ├── group.go
│ │ │ │ ├── group_invitation_token.go
│ │ │ │ ├── item.go
│ │ │ │ ├── item_field.go
│ │ │ │ ├── label.go
│ │ │ │ ├── location.go
│ │ │ │ ├── maintenance_entry.go
│ │ │ │ ├── mixins
│ │ │ │ │ └── base.go
│ │ │ │ ├── notifier.go
│ │ │ │ ├── templates
│ │ │ │ │ └── has_id.tmpl
│ │ │ │ └── user.go
│ │ │ ├── tx.go
│ │ │ ├── user.go
│ │ │ ├── user
│ │ │ │ ├── user.go
│ │ │ │ └── where.go
│ │ │ ├── user_create.go
│ │ │ ├── user_delete.go
│ │ │ ├── user_query.go
│ │ │ └── user_update.go
│ │ ├── migrations
│ │ │ ├── migrations.go
│ │ │ ├── postgres
│ │ │ │ ├── 20241027025146_init.sql
│ │ │ │ ├── 20250112202302_sync_children.go
│ │ │ │ ├── 20250419184104_merge_docs_attachments.sql
│ │ │ │ └── main.go
│ │ │ └── sqlite3
│ │ │ │ ├── 20220929052825_init.sql
│ │ │ │ ├── 20241226183416_sync_children.go
│ │ │ │ ├── 20250405050521_merge_docs_and_attachments.sql
│ │ │ │ └── main.go
│ │ ├── repo
│ │ │ ├── asset_id_type.go
│ │ │ ├── asset_id_type_test.go
│ │ │ ├── automappers.go
│ │ │ ├── id_set.go
│ │ │ ├── main_test.go
│ │ │ ├── map_helpers.go
│ │ │ ├── pagination.go
│ │ │ ├── query_helpers.go
│ │ │ ├── repo_group.go
│ │ │ ├── repo_group_test.go
│ │ │ ├── repo_item_attachments.go
│ │ │ ├── repo_item_attachments_test.go
│ │ │ ├── repo_items.go
│ │ │ ├── repo_items_test.go
│ │ │ ├── repo_labels.go
│ │ │ ├── repo_labels_test.go
│ │ │ ├── repo_locations.go
│ │ │ ├── repo_locations_test.go
│ │ │ ├── repo_maintenance.go
│ │ │ ├── repo_maintenance_entry.go
│ │ │ ├── repo_maintenance_entry_test.go
│ │ │ ├── repo_notifier.go
│ │ │ ├── repo_tokens.go
│ │ │ ├── repo_tokens_test.go
│ │ │ ├── repo_users.go
│ │ │ ├── repo_users_test.go
│ │ │ └── repos_all.go
│ │ └── types
│ │ │ └── date.go
│ ├── sys
│ │ ├── analytics
│ │ │ └── analytics.go
│ │ ├── config
│ │ │ ├── conf.go
│ │ │ ├── conf_database.go
│ │ │ ├── conf_logger.go
│ │ │ ├── conf_mailer.go
│ │ │ └── conf_mailer_test.go
│ │ └── validate
│ │ │ ├── errors.go
│ │ │ └── validate.go
│ └── web
│ │ ├── adapters
│ │ ├── actions.go
│ │ ├── adapters.go
│ │ ├── command.go
│ │ ├── decoders.go
│ │ ├── doc.go
│ │ └── query.go
│ │ └── mid
│ │ ├── doc.go
│ │ ├── errors.go
│ │ └── logger.go
└── pkgs
│ ├── cgofreesqlite
│ └── sqlite.go
│ ├── faker
│ ├── random.go
│ └── randoms_test.go
│ ├── hasher
│ ├── doc.go
│ ├── password.go
│ ├── password_test.go
│ ├── token.go
│ └── token_test.go
│ ├── labelmaker
│ └── labelmaker.go
│ ├── mailer
│ ├── mailer.go
│ ├── mailer_test.go
│ ├── message.go
│ ├── message_test.go
│ ├── templates.go
│ ├── templates
│ │ └── welcome.html
│ └── test-mailer-template.json
│ └── set
│ ├── funcs.go
│ ├── funcs_test.go
│ ├── set.go
│ └── set_test.go
├── docker-compose.yml
├── docs
├── .vitepress
│ ├── config.mts
│ ├── menus
│ │ └── en.mts
│ └── theme
│ │ ├── index.ts
│ │ └── style.css
├── en
│ ├── analytics
│ │ ├── index.md
│ │ └── privacy.md
│ ├── api
│ │ ├── index.md
│ │ ├── openapi-2.0.json
│ │ └── openapi-2.0.yaml
│ ├── configure.md
│ ├── contribute
│ │ ├── bounty.md
│ │ ├── get-started.md
│ │ └── shadcn.md
│ ├── images
│ │ └── home-screen.png
│ ├── import-csv.md
│ ├── index.md
│ ├── installation.md
│ ├── migration.md
│ ├── quick-start.md
│ ├── upgrade.md
│ └── user-guide
│ │ ├── organizing-items.md
│ │ └── tips-tricks.md
└── public
│ ├── _redirects
│ ├── favicon.svg
│ ├── homebox-email-banner.jpg
│ └── lilbox.svg
├── frontend
├── .eslintrc.js
├── .nuxtignore
├── app.vue
├── assets
│ └── css
│ │ └── main.css
├── components.json
├── components
│ ├── App
│ │ ├── CreateModal.vue
│ │ ├── HeaderDecor.vue
│ │ ├── HeaderText.vue
│ │ ├── ImportDialog.vue
│ │ ├── LanguageSelector.vue
│ │ ├── Logo.vue
│ │ ├── OutdatedModal.vue
│ │ ├── QuickMenuModal.vue
│ │ └── ScannerModal.vue
│ ├── Base
│ │ ├── Card.vue
│ │ ├── Container.vue
│ │ └── SectionHeader.vue
│ ├── DetailAction.vue
│ ├── Form
│ │ ├── Checkbox.vue
│ │ ├── DatePicker.vue
│ │ ├── Password.vue
│ │ ├── TextArea.vue
│ │ └── TextField.vue
│ ├── Item
│ │ ├── AttachmentsList.vue
│ │ ├── Card.vue
│ │ ├── CreateModal.vue
│ │ ├── Selector.vue
│ │ └── View
│ │ │ ├── Selectable.vue
│ │ │ ├── Table.types.ts
│ │ │ └── Table.vue
│ ├── Label
│ │ ├── Chip.vue
│ │ ├── CreateModal.vue
│ │ └── Selector.vue
│ ├── Location
│ │ ├── Card.vue
│ │ ├── CreateModal.vue
│ │ ├── Selector.vue
│ │ └── Tree
│ │ │ ├── Node.vue
│ │ │ ├── Root.vue
│ │ │ └── tree-state.ts
│ ├── Maintenance
│ │ ├── EditModal.vue
│ │ └── ListView.vue
│ ├── ModalConfirm.vue
│ ├── Search
│ │ └── Filter.vue
│ ├── global
│ │ ├── CopyText.vue
│ │ ├── Currency.vue
│ │ ├── DateTime.vue
│ │ ├── DetailsSection
│ │ │ ├── DetailsSection.vue
│ │ │ └── types.ts
│ │ ├── DropZone.vue
│ │ ├── LabelMaker.vue
│ │ ├── Markdown.vue
│ │ ├── PageQRCode.vue
│ │ ├── PasswordScore.vue
│ │ ├── Spacer.vue
│ │ ├── StatCard
│ │ │ ├── StatCard.vue
│ │ │ └── types.ts
│ │ ├── Subtitle.vue
│ │ └── Table.types.ts
│ └── ui
│ │ ├── alert-dialog
│ │ ├── AlertDialog.vue
│ │ ├── AlertDialogAction.vue
│ │ ├── AlertDialogCancel.vue
│ │ ├── AlertDialogContent.vue
│ │ ├── AlertDialogDescription.vue
│ │ ├── AlertDialogFooter.vue
│ │ ├── AlertDialogHeader.vue
│ │ ├── AlertDialogTitle.vue
│ │ ├── AlertDialogTrigger.vue
│ │ └── index.ts
│ │ ├── badge
│ │ ├── Badge.vue
│ │ └── index.ts
│ │ ├── breadcrumb
│ │ ├── Breadcrumb.vue
│ │ ├── BreadcrumbEllipsis.vue
│ │ ├── BreadcrumbItem.vue
│ │ ├── BreadcrumbLink.vue
│ │ ├── BreadcrumbList.vue
│ │ ├── BreadcrumbPage.vue
│ │ ├── BreadcrumbSeparator.vue
│ │ └── index.ts
│ │ ├── button
│ │ ├── Button.vue
│ │ ├── ButtonGroup.vue
│ │ └── index.ts
│ │ ├── card
│ │ ├── Card.vue
│ │ ├── CardContent.vue
│ │ ├── CardDescription.vue
│ │ ├── CardFooter.vue
│ │ ├── CardHeader.vue
│ │ ├── CardTitle.vue
│ │ └── index.ts
│ │ ├── checkbox
│ │ ├── Checkbox.vue
│ │ └── index.ts
│ │ ├── command
│ │ ├── Command.vue
│ │ ├── CommandDialog.vue
│ │ ├── CommandEmpty.vue
│ │ ├── CommandGroup.vue
│ │ ├── CommandInput.vue
│ │ ├── CommandItem.vue
│ │ ├── CommandList.vue
│ │ ├── CommandSeparator.vue
│ │ ├── CommandShortcut.vue
│ │ └── index.ts
│ │ ├── dialog-provider
│ │ ├── DialogProvider.vue
│ │ ├── index.ts
│ │ └── utils.ts
│ │ ├── dialog
│ │ ├── Dialog.vue
│ │ ├── DialogClose.vue
│ │ ├── DialogContent.vue
│ │ ├── DialogDescription.vue
│ │ ├── DialogFooter.vue
│ │ ├── DialogHeader.vue
│ │ ├── DialogScrollContent.vue
│ │ ├── DialogTitle.vue
│ │ ├── DialogTrigger.vue
│ │ └── index.ts
│ │ ├── drawer
│ │ ├── Drawer.vue
│ │ ├── DrawerContent.vue
│ │ ├── DrawerDescription.vue
│ │ ├── DrawerFooter.vue
│ │ ├── DrawerHeader.vue
│ │ ├── DrawerOverlay.vue
│ │ ├── DrawerTitle.vue
│ │ └── index.ts
│ │ ├── dropdown-menu
│ │ ├── DropdownMenu.vue
│ │ ├── DropdownMenuCheckboxItem.vue
│ │ ├── DropdownMenuContent.vue
│ │ ├── DropdownMenuGroup.vue
│ │ ├── DropdownMenuItem.vue
│ │ ├── DropdownMenuLabel.vue
│ │ ├── DropdownMenuRadioGroup.vue
│ │ ├── DropdownMenuRadioItem.vue
│ │ ├── DropdownMenuSeparator.vue
│ │ ├── DropdownMenuShortcut.vue
│ │ ├── DropdownMenuSub.vue
│ │ ├── DropdownMenuSubContent.vue
│ │ ├── DropdownMenuSubTrigger.vue
│ │ ├── DropdownMenuTrigger.vue
│ │ └── index.ts
│ │ ├── input
│ │ ├── Input.vue
│ │ └── index.ts
│ │ ├── label
│ │ ├── Label.vue
│ │ └── index.ts
│ │ ├── pagination
│ │ ├── PaginationEllipsis.vue
│ │ ├── PaginationFirst.vue
│ │ ├── PaginationLast.vue
│ │ ├── PaginationNext.vue
│ │ ├── PaginationPrev.vue
│ │ └── index.ts
│ │ ├── popover
│ │ ├── Popover.vue
│ │ ├── PopoverContent.vue
│ │ ├── PopoverTrigger.vue
│ │ └── index.ts
│ │ ├── progress
│ │ ├── Progress.vue
│ │ └── index.ts
│ │ ├── select
│ │ ├── Select.vue
│ │ ├── SelectContent.vue
│ │ ├── SelectGroup.vue
│ │ ├── SelectItem.vue
│ │ ├── SelectItemText.vue
│ │ ├── SelectLabel.vue
│ │ ├── SelectScrollDownButton.vue
│ │ ├── SelectScrollUpButton.vue
│ │ ├── SelectSeparator.vue
│ │ ├── SelectTrigger.vue
│ │ ├── SelectValue.vue
│ │ └── index.ts
│ │ ├── separator
│ │ ├── Separator.vue
│ │ └── index.ts
│ │ ├── sheet
│ │ ├── Sheet.vue
│ │ ├── SheetClose.vue
│ │ ├── SheetContent.vue
│ │ ├── SheetDescription.vue
│ │ ├── SheetFooter.vue
│ │ ├── SheetHeader.vue
│ │ ├── SheetTitle.vue
│ │ ├── SheetTrigger.vue
│ │ └── index.ts
│ │ ├── shortcut
│ │ ├── Shortcut.vue
│ │ └── index.ts
│ │ ├── sidebar
│ │ ├── Sidebar.vue
│ │ ├── SidebarContent.vue
│ │ ├── SidebarFooter.vue
│ │ ├── SidebarGroup.vue
│ │ ├── SidebarGroupAction.vue
│ │ ├── SidebarGroupContent.vue
│ │ ├── SidebarGroupLabel.vue
│ │ ├── SidebarHeader.vue
│ │ ├── SidebarInput.vue
│ │ ├── SidebarInset.vue
│ │ ├── SidebarMenu.vue
│ │ ├── SidebarMenuAction.vue
│ │ ├── SidebarMenuBadge.vue
│ │ ├── SidebarMenuButton.vue
│ │ ├── SidebarMenuButtonChild.vue
│ │ ├── SidebarMenuItem.vue
│ │ ├── SidebarMenuLink.vue
│ │ ├── SidebarMenuSkeleton.vue
│ │ ├── SidebarMenuSub.vue
│ │ ├── SidebarMenuSubButton.vue
│ │ ├── SidebarMenuSubItem.vue
│ │ ├── SidebarProvider.vue
│ │ ├── SidebarRail.vue
│ │ ├── SidebarSeparator.vue
│ │ ├── SidebarTrigger.vue
│ │ ├── index.ts
│ │ └── utils.ts
│ │ ├── skeleton
│ │ ├── Skeleton.vue
│ │ └── index.ts
│ │ ├── sonner
│ │ ├── Sonner.vue
│ │ ├── index.ts
│ │ └── toast.ts
│ │ ├── switch
│ │ ├── Switch.vue
│ │ └── index.ts
│ │ ├── table
│ │ ├── Table.vue
│ │ ├── TableBody.vue
│ │ ├── TableCaption.vue
│ │ ├── TableCell.vue
│ │ ├── TableEmpty.vue
│ │ ├── TableFooter.vue
│ │ ├── TableHead.vue
│ │ ├── TableHeader.vue
│ │ ├── TableRow.vue
│ │ └── index.ts
│ │ ├── tags-input
│ │ ├── TagsInput.vue
│ │ ├── TagsInputInput.vue
│ │ ├── TagsInputItem.vue
│ │ ├── TagsInputItemDelete.vue
│ │ ├── TagsInputItemText.vue
│ │ └── index.ts
│ │ ├── textarea
│ │ ├── Textarea.vue
│ │ └── index.ts
│ │ └── tooltip
│ │ ├── Tooltip.vue
│ │ ├── TooltipContent.vue
│ │ ├── TooltipProvider.vue
│ │ ├── TooltipTrigger.vue
│ │ └── index.ts
├── composables
│ ├── use-api.ts
│ ├── use-auth-context.ts
│ ├── use-confirm.ts
│ ├── use-css-var.ts
│ ├── use-defer.ts
│ ├── use-formatters.ts
│ ├── use-item-search.ts
│ ├── use-location-helpers.ts
│ ├── use-min-loader.ts
│ ├── use-password-score.ts
│ ├── use-preferences.ts
│ ├── use-route-params.ts
│ ├── use-server-events.ts
│ ├── use-theme.ts
│ ├── utils.test.ts
│ └── utils.ts
├── error.vue
├── global.d.ts
├── layouts
│ ├── default.vue
│ └── empty.vue
├── lib
│ ├── api
│ │ ├── __test__
│ │ │ ├── factories
│ │ │ │ └── index.ts
│ │ │ ├── public.test.ts
│ │ │ ├── test-utils.ts
│ │ │ └── user
│ │ │ │ ├── group.test.ts
│ │ │ │ ├── items.test.ts
│ │ │ │ ├── labels.test.ts
│ │ │ │ ├── locations.test.ts
│ │ │ │ ├── notifier.test.ts
│ │ │ │ ├── stats.test.ts
│ │ │ │ └── user.test.ts
│ │ ├── base
│ │ │ ├── base-api.test.ts
│ │ │ ├── base-api.ts
│ │ │ ├── index.test.ts
│ │ │ ├── index.ts
│ │ │ └── urls.ts
│ │ ├── classes
│ │ │ ├── actions.ts
│ │ │ ├── assets.ts
│ │ │ ├── group.ts
│ │ │ ├── items.ts
│ │ │ ├── labels.ts
│ │ │ ├── locations.ts
│ │ │ ├── maintenance.ts
│ │ │ ├── notifiers.ts
│ │ │ ├── reports.ts
│ │ │ ├── stats.ts
│ │ │ └── users.ts
│ │ ├── public.ts
│ │ ├── types
│ │ │ ├── data-contracts.ts
│ │ │ └── non-generated.ts
│ │ └── user.ts
│ ├── data
│ │ └── themes.ts
│ ├── datelib
│ │ ├── datelib.test.ts
│ │ └── datelib.ts
│ ├── passwords
│ │ ├── index.test.ts
│ │ └── index.ts
│ ├── requests
│ │ ├── index.ts
│ │ └── requests.ts
│ └── utils.ts
├── locales
│ ├── bs-BA.json
│ ├── ca.json
│ ├── cs-CZ.json
│ ├── da-DK.json
│ ├── de.json
│ ├── en.json
│ ├── es.json
│ ├── fi-FI.json
│ ├── fr.json
│ ├── hu.json
│ ├── id-ID.json
│ ├── it.json
│ ├── ja-JP.json
│ ├── ko-KR.json
│ ├── lb-LU.json
│ ├── lt-LT.json
│ ├── nb-NO.json
│ ├── nl.json
│ ├── pl.json
│ ├── pt-BR.json
│ ├── pt-PT.json
│ ├── ro-RO.json
│ ├── ru.json
│ ├── sk-SK.json
│ ├── sl.json
│ ├── sq-AL.json
│ ├── sv.json
│ ├── ta-IN.json
│ ├── th-TH.json
│ ├── tr.json
│ ├── uk-UA.json
│ ├── zh-CN.json
│ ├── zh-HK.json
│ ├── zh-MO.json
│ └── zh-TW.json
├── middleware
│ └── auth.ts
├── nuxt.config.ts
├── package.json
├── pages
│ ├── a
│ │ └── [id].vue
│ ├── assets
│ │ └── [id].vue
│ ├── home
│ │ ├── index.vue
│ │ ├── statistics.ts
│ │ └── table.ts
│ ├── index.vue
│ ├── item
│ │ └── [id]
│ │ │ ├── index.vue
│ │ │ └── index
│ │ │ ├── edit.vue
│ │ │ └── maintenance.vue
│ ├── items.vue
│ ├── label
│ │ └── [id].vue
│ ├── location
│ │ └── [id].vue
│ ├── locations.vue
│ ├── maintenance.vue
│ ├── profile.vue
│ ├── reports
│ │ └── label-generator.vue
│ └── tools.vue
├── plugins
│ ├── i18n.ts
│ └── scroll.client.ts
├── pnpm-lock.yaml
├── public
│ ├── favicon.svg
│ ├── no-image.jpg
│ ├── pwa-192x192.png
│ ├── pwa-512x512.png
│ └── set-theme.js
├── stores
│ ├── labels.ts
│ └── locations.ts
├── tailwind.config.js
├── test
│ ├── config.ts
│ ├── e2e
│ │ └── login.browser.spec.ts
│ ├── playwright.config.ts
│ ├── playwright.teardown.ts
│ ├── setup.ts
│ └── vitest.config.ts
└── tsconfig.json
├── package.json
└── pnpm-lock.yaml
/.devcontainer/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:22-bullseye
2 |
3 | RUN sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b ~/.local/bin
4 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/typescript-node
3 | {
4 | "name": "Node.js & TypeScript",
5 | "build": {
6 | "dockerfile": "Dockerfile"
7 | },
8 |
9 | // Configure tool-specific properties.
10 | "customizations": {
11 | // Configure properties specific to VS Code.
12 | "vscode": {
13 | // Add the IDs of extensions you want installed when the container is created.
14 | "extensions": [
15 | "dbaeumer.vscode-eslint"
16 | ]
17 | }
18 | },
19 |
20 | // Use 'forwardPorts' to make a list of ports inside the container available locally.
21 | "forwardPorts": [
22 | 7745,
23 | 3000
24 | ],
25 |
26 | // Use 'postCreateCommand' to run commands after the container is created.
27 | "postCreateCommand": "go install github.com/go-task/task/v3/cmd/task@latest && npm install -g pnpm && task setup",
28 |
29 | // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
30 | "remoteUser": "node",
31 | "features": {
32 | "ghcr.io/devcontainers/features/go:1": "1.21"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/.classpath
2 | **/.dockerignore
3 | **/.env
4 | **/.git
5 | **/.gitignore
6 | **/.project
7 | **/.settings
8 | **/.toolstarget
9 | **/.vs
10 | **/.vscode
11 | **/*.*proj.user
12 | **/*.dbmdl
13 | **/*.jfm
14 | **/bin
15 | **/charts
16 | **/docker-compose*
17 | **/compose*
18 | **/Dockerfile*
19 | **/node_modules
20 | **/npm-debug.log
21 | **/obj
22 | **/secrets.dev.yaml
23 | **/values.dev.yaml
24 | README.md
25 | !Dockerfile.rootless
26 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [tankerkiller125,katosdev,tonyaellie]
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: GitHub Community Support
4 | url: https://github.com/sysadminsmedia/homebox/discussions/categories/support
5 | about: Get support for issues here
6 | - name: Feature Requests
7 | url: https://github.com/sysadminsmedia/homebox/discussions/categories/ideas
8 | about: Have an idea for Homebox? Share it in our discussions forum. If we decide to take it on we will create an issue for it.
9 | - name: Translate
10 | url: https://translate.sysadminsmedia.com
11 | about: Help us translate Homebox! All contributions and all languages welcome!
12 |
--------------------------------------------------------------------------------
/.github/workflows/binaries-publish.yaml:
--------------------------------------------------------------------------------
1 | name: Publish Release Binaries
2 |
3 | on:
4 | push:
5 | tags: [ 'v*.*.*' ]
6 |
7 | jobs:
8 | goreleaser:
9 | name: goreleaser
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout
13 | uses: actions/checkout@v4
14 | with:
15 | fetch-depth: 0
16 |
17 | - name: Set up Go
18 | uses: actions/setup-go@v5
19 |
20 | - uses: pnpm/action-setup@v2
21 | with:
22 | version: 9.15.3
23 |
24 | - name: Build Frontend and Copy to Backend
25 | working-directory: frontend
26 | run: |
27 | pnpm install
28 | pnpm run build
29 | cp -r ./.output/public ../backend/app/api/static/
30 |
31 | - name: Install CoSign
32 | working-directory: backend
33 | run: |
34 | go install github.com/sigstore/cosign/cmd/cosign@latest
35 |
36 | - name: Run GoReleaser
37 | uses: goreleaser/goreleaser-action@v5
38 | with:
39 | workdir: "backend"
40 | distribution: goreleaser
41 | version: "~> v2"
42 | args: release --clean
43 | env:
44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45 |
--------------------------------------------------------------------------------
/.github/workflows/partial-backend.yaml:
--------------------------------------------------------------------------------
1 | name: Go Build/Test
2 |
3 | on:
4 | workflow_call:
5 |
6 | jobs:
7 | Go:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v4
11 |
12 | - name: Set up Go
13 | uses: actions/setup-go@v5
14 | with:
15 | go-version: "1.21"
16 |
17 | - name: Install Task
18 | uses: arduino/setup-task@v1
19 | with:
20 | repo-token: ${{ secrets.GITHUB_TOKEN }}
21 |
22 | - name: golangci-lint
23 | uses: golangci/golangci-lint-action@v7
24 | with:
25 | # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
26 | version: latest
27 |
28 | # Optional: working directory, useful for monorepos
29 | working-directory: backend
30 | args: --timeout=6m
31 |
32 | - name: Build API
33 | run: task go:build
34 |
35 | - name: Test
36 | run: task go:coverage
37 |
38 | - name: Validate OpenAPI definition
39 | uses: swaggerexpert/swagger-editor-validate@v1
40 | with:
41 | definition-file: backend/app/api/static/docs/swagger.json
42 |
--------------------------------------------------------------------------------
/.github/workflows/pull-requests.yaml:
--------------------------------------------------------------------------------
1 | name: Pull Request CI
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | - vnext
8 |
9 | paths:
10 | - 'backend/**'
11 | - 'frontend/**'
12 | - '.github/workflows/**'
13 |
14 | jobs:
15 | backend-tests:
16 | name: "Backend Server Tests"
17 | uses: ./.github/workflows/partial-backend.yaml
18 |
19 | frontend-tests:
20 | name: "Frontend Tests"
21 | uses: ./.github/workflows/partial-frontend.yaml
22 |
23 | e2e-tests:
24 | name: "End-to-End Playwright Tests"
25 | uses: ./.github/workflows/e2e-partial.yaml
--------------------------------------------------------------------------------
/.github/workflows/update-currencies/requirements.txt:
--------------------------------------------------------------------------------
1 | requests
2 |
--------------------------------------------------------------------------------
/.scaffold/model/scaffold.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | # yaml-language-server: $schema=https://hay-kot.github.io/scaffold/schema.json
3 | messages:
4 | pre: |
5 | # Ent Model Generation
6 |
7 | With Boilerplate!
8 | post: |
9 | Complete!
10 |
11 | questions:
12 | - name: "model"
13 | prompt:
14 | message: "What is the name of the model? (PascalCase)"
15 | required: true
16 |
17 | - name: "by_group"
18 | prompt:
19 | confirm: "Include a Group Edge? (group_id -> id)"
20 | required: true
21 |
22 | rewrites:
23 | - from: 'templates/model.go'
24 | to: 'backend/internal/data/ent/schema/{{ lower .Scaffold.model }}.go'
25 |
26 | inject:
27 | - name: "Insert Groups Edge"
28 | path: 'backend/internal/data/ent/schema/group.go'
29 | at: // $scaffold_edge
30 | template: |
31 | {{- if .Scaffold.by_group -}}
32 | owned("{{ lower .Scaffold.model }}s", {{ .Scaffold.model }}.Type),
33 | {{- end -}}
34 |
--------------------------------------------------------------------------------
/.scaffold/model/templates/model.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "entgo.io/ent"
5 |
6 | "github.com/sysadminsmedia/homebox/backend/internal/data/ent/schema/mixins"
7 | )
8 |
9 | type {{ .Scaffold.model }} struct {
10 | ent.Schema
11 | }
12 |
13 | func ({{ .Scaffold.model }}) Mixin() []ent.Mixin {
14 | return []ent.Mixin{
15 | mixins.BaseMixin{},
16 | {{- if .Scaffold.by_group }}
17 | GroupMixin{ref: "{{ snakecase .Scaffold.model }}s"},
18 | {{- end }}
19 | }
20 | }
21 |
22 | // Fields of the {{ .Scaffold.model }}.
23 | func ({{ .Scaffold.model }}) Fields() []ent.Field {
24 | return []ent.Field{
25 | // field.String("name").
26 | }
27 | }
28 |
29 | // Edges of the {{ .Scaffold.model }}.
30 | func ({{ .Scaffold.model }}) Edges() []ent.Edge {
31 | return []ent.Edge{
32 | // edge.From("group", Group.Type).
33 | }
34 | }
35 |
36 | func ({{ .Scaffold.model }}) Indexes() []ent.Index {
37 | return []ent.Index{
38 | // index.Fields("token"),
39 | }
40 | }
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | Since this software is still considered beta/WIP support is always only given for the latest version.
6 |
7 | ## Reporting a Vulnerability
8 |
9 | Please open a normal public issue for minor security issues or general security inquires.
10 |
11 | For major or critical security issues, please open a private github security issue.
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | dist/
3 |
--------------------------------------------------------------------------------
/backend/app/api/app.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/sysadminsmedia/homebox/backend/internal/core/services"
5 | "github.com/sysadminsmedia/homebox/backend/internal/core/services/reporting/eventbus"
6 | "github.com/sysadminsmedia/homebox/backend/internal/data/ent"
7 | "github.com/sysadminsmedia/homebox/backend/internal/data/repo"
8 | "github.com/sysadminsmedia/homebox/backend/internal/sys/config"
9 | "github.com/sysadminsmedia/homebox/backend/pkgs/mailer"
10 | )
11 |
12 | type app struct {
13 | conf *config.Config
14 | mailer mailer.Mailer
15 | db *ent.Client
16 | repos *repo.AllRepos
17 | services *services.AllServices
18 | bus *eventbus.EventBus
19 | }
20 |
21 | func new(conf *config.Config) *app {
22 | s := &app{
23 | conf: conf,
24 | }
25 |
26 | s.mailer = mailer.Mailer{
27 | Host: s.conf.Mailer.Host,
28 | Port: s.conf.Mailer.Port,
29 | Username: s.conf.Mailer.Username,
30 | Password: s.conf.Mailer.Password,
31 | From: s.conf.Mailer.From,
32 | }
33 |
34 | return s
35 | }
36 |
--------------------------------------------------------------------------------
/backend/app/api/bgrunner.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "time"
6 | )
7 |
8 | type BackgroundTask struct {
9 | name string
10 | Interval time.Duration
11 | Fn func(context.Context)
12 | }
13 |
14 | func (tsk *BackgroundTask) Name() string {
15 | return tsk.name
16 | }
17 |
18 | func NewTask(name string, interval time.Duration, fn func(context.Context)) *BackgroundTask {
19 | return &BackgroundTask{
20 | Interval: interval,
21 | Fn: fn,
22 | }
23 | }
24 |
25 | func (tsk *BackgroundTask) Start(ctx context.Context) error {
26 | tsk.Fn(ctx)
27 |
28 | timer := time.NewTimer(tsk.Interval)
29 | for {
30 | select {
31 | case <-ctx.Done():
32 | return nil
33 | case <-timer.C:
34 | timer.Reset(tsk.Interval)
35 | tsk.Fn(ctx)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/backend/app/api/handlers/debughandlers/debug.go:
--------------------------------------------------------------------------------
1 | // Package debughandlers provides handlers for debugging.
2 | package debughandlers
3 |
4 | import (
5 | "expvar"
6 | "net/http"
7 | "net/http/pprof"
8 | )
9 |
10 | func New(mux *http.ServeMux) {
11 | mux.HandleFunc("/debug/pprof", pprof.Index)
12 | mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
13 | mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
14 | mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
15 | mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
16 | mux.Handle("/debug/vars", expvar.Handler())
17 | }
18 |
--------------------------------------------------------------------------------
/backend/app/api/handlers/v1/assets/QRIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sysadminsmedia/homebox/676b87d87e7d1ca1b6914af809b8fe7cec80f741/backend/app/api/handlers/v1/assets/QRIcon.png
--------------------------------------------------------------------------------
/backend/app/api/handlers/v1/helpers.go:
--------------------------------------------------------------------------------
1 | package v1
2 |
3 | import (
4 | "net/url"
5 |
6 | "github.com/rs/zerolog/log"
7 | )
8 |
9 | func GetHBURL(refererHeader, fallback string) (hbURL string) {
10 | hbURL = refererHeader
11 | if hbURL == "" {
12 | hbURL = fallback
13 | }
14 |
15 | return stripPathFromURL(hbURL)
16 | }
17 |
18 | // stripPathFromURL removes the path from a URL.
19 | // ex. https://example.com/tools -> https://example.com
20 | func stripPathFromURL(rawURL string) string {
21 | parsedURL, err := url.Parse(rawURL)
22 | if err != nil {
23 | log.Err(err).Msg("failed to parse URL")
24 | return ""
25 | }
26 |
27 | strippedURL := url.URL{Scheme: parsedURL.Scheme, Host: parsedURL.Host}
28 |
29 | return strippedURL.String()
30 | }
31 |
--------------------------------------------------------------------------------
/backend/app/api/handlers/v1/partials.go:
--------------------------------------------------------------------------------
1 | package v1
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/go-chi/chi/v5"
7 | "github.com/google/uuid"
8 | "github.com/sysadminsmedia/homebox/backend/internal/sys/validate"
9 | )
10 |
11 | // routeID extracts the ID from the request URL. If the ID is not in a valid
12 | // format, an error is returned. If a error is returned, it can be directly returned
13 | // from the handler. the validate.ErrInvalidID error is known by the error middleware
14 | // and will be handled accordingly.
15 | //
16 | // Example: /api/v1/ac614db5-d8b8-4659-9b14-6e913a6eb18a -> uuid.UUID{ac614db5-d8b8-4659-9b14-6e913a6eb18a}
17 | func (ctrl *V1Controller) routeID(r *http.Request) (uuid.UUID, error) {
18 | return ctrl.routeUUID(r, "id")
19 | }
20 |
21 | func (ctrl *V1Controller) routeUUID(r *http.Request, key string) (uuid.UUID, error) {
22 | ID, err := uuid.Parse(chi.URLParam(r, key))
23 | if err != nil {
24 | return uuid.Nil, validate.NewRouteKeyError(key)
25 | }
26 | return ID, nil
27 | }
28 |
--------------------------------------------------------------------------------
/backend/app/api/handlers/v1/query_params.go:
--------------------------------------------------------------------------------
1 | package v1
2 |
3 | import (
4 | "net/url"
5 | "strconv"
6 |
7 | "github.com/google/uuid"
8 | )
9 |
10 | func queryUUIDList(params url.Values, key string) []uuid.UUID {
11 | var ids []uuid.UUID
12 | for _, id := range params[key] {
13 | uid, err := uuid.Parse(id)
14 | if err != nil {
15 | continue
16 | }
17 | ids = append(ids, uid)
18 | }
19 | return ids
20 | }
21 |
22 | func queryIntOrNegativeOne(s string) int {
23 | i, err := strconv.Atoi(s)
24 | if err != nil {
25 | return -1
26 | }
27 | return i
28 | }
29 |
30 | func queryBool(s string) bool {
31 | b, err := strconv.ParseBool(s)
32 | if err != nil {
33 | return false
34 | }
35 | return b
36 | }
37 |
--------------------------------------------------------------------------------
/backend/app/api/handlers/v1/v1_ctrl_reporting.go:
--------------------------------------------------------------------------------
1 | package v1
2 |
3 | import (
4 | "github.com/hay-kot/httpkit/errchain"
5 | "github.com/sysadminsmedia/homebox/backend/internal/core/services"
6 | "net/http"
7 | )
8 |
9 | // HandleBillOfMaterialsExport godoc
10 | //
11 | // @Summary Export Bill of Materials
12 | // @Tags Reporting
13 | // @Produce json
14 | // @Success 200 {string} string "text/csv"
15 | // @Router /v1/reporting/bill-of-materials [GET]
16 | // @Security Bearer
17 | func (ctrl *V1Controller) HandleBillOfMaterialsExport() errchain.HandlerFunc {
18 | return func(w http.ResponseWriter, r *http.Request) error {
19 | actor := services.UseUserCtx(r.Context())
20 |
21 | csv, err := ctrl.svc.Items.ExportBillOfMaterialsCSV(r.Context(), actor.GroupID)
22 | if err != nil {
23 | return err
24 | }
25 |
26 | w.Header().Set("Content-Type", "text/csv")
27 | w.Header().Set("Content-Disposition", "attachment; filename=bill-of-materials.csv")
28 | _, err = w.Write(csv)
29 | return err
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/backend/app/api/logger.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/rs/zerolog"
7 | "github.com/rs/zerolog/log"
8 | "github.com/sysadminsmedia/homebox/backend/internal/sys/config"
9 | )
10 |
11 | // setupLogger initializes the zerolog config
12 | // for the shared logger.
13 | func (a *app) setupLogger() {
14 | // Logger Init
15 | // zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
16 | if a.conf.Log.Format != config.LogFormatJSON {
17 | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).With().Caller().Logger()
18 | }
19 |
20 | level, err := zerolog.ParseLevel(a.conf.Log.Level)
21 | if err == nil {
22 | zerolog.SetGlobalLevel(level)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/backend/app/api/providers/doc.go:
--------------------------------------------------------------------------------
1 | // Package providers provides a authentication abstraction for the backend.
2 | package providers
3 |
--------------------------------------------------------------------------------
/backend/app/api/providers/local.go:
--------------------------------------------------------------------------------
1 | package providers
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/sysadminsmedia/homebox/backend/internal/core/services"
7 | )
8 |
9 | type LocalProvider struct {
10 | service *services.UserService
11 | }
12 |
13 | func NewLocalProvider(service *services.UserService) *LocalProvider {
14 | return &LocalProvider{
15 | service: service,
16 | }
17 | }
18 |
19 | func (p *LocalProvider) Name() string {
20 | return "local"
21 | }
22 |
23 | func (p *LocalProvider) Authenticate(w http.ResponseWriter, r *http.Request) (services.UserAuthTokenDetail, error) {
24 | loginForm, err := getLoginForm(r)
25 | if err != nil {
26 | return services.UserAuthTokenDetail{}, err
27 | }
28 |
29 | return p.service.Login(r.Context(), loginForm.Username, loginForm.Password, loginForm.StayLoggedIn)
30 | }
31 |
--------------------------------------------------------------------------------
/backend/app/api/static/public/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sysadminsmedia/homebox/676b87d87e7d1ca1b6914af809b8fe7cec80f741/backend/app/api/static/public/.gitkeep
--------------------------------------------------------------------------------
/backend/cosign.key:
--------------------------------------------------------------------------------
1 | -----BEGIN ENCRYPTED SIGSTORE PRIVATE KEY-----
2 | eyJrZGYiOnsibmFtZSI6InNjcnlwdCIsInBhcmFtcyI6eyJOIjo2NTUzNiwiciI6
3 | OCwicCI6MX0sInNhbHQiOiJ3bmU3TTd2dndlL2FBS1piUEE2QktsdFNzMkhkSk9v
4 | eXlvOTNLMnByRXdJPSJ9LCJjaXBoZXIiOnsibmFtZSI6Im5hY2wvc2VjcmV0Ym94
5 | Iiwibm9uY2UiOiJoOWdIMHRsYk9zMnZIbVBTYk5zaGxBQU5TYUlkcVZoQiJ9LCJj
6 | aXBoZXJ0ZXh0IjoiTERiQk5ac3ZlVnRMbTlQdkRTa2t6bzRrWGExVGRTTEY5VzVO
7 | cGd6M05GNVJLRWlGRmJQRDJDYzhnTWNkRmkrTU8xd2FTUzFGWWdXU3BIdnI3QXZ3
8 | K0tUTXVWLzhSZ1pnOE9ieHNJY2xKSlZldHRLTzdzWXY2aWgxM09iZlVBV0lQcGpS
9 | ZUQ5UmE3WjJwbWd0SkpBdjl2dlk1RGNNeGRKcFFrOEY1UStLZytSbnhLRUd6Z1ZN
10 | MWUxdjF3UGhsOWhVRGRMSFVSTzE5Z0w3aFE9PSJ9
11 | -----END ENCRYPTED SIGSTORE PRIVATE KEY-----
12 |
--------------------------------------------------------------------------------
/backend/cosign.pub:
--------------------------------------------------------------------------------
1 | -----BEGIN PUBLIC KEY-----
2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2DXKcerPznDayM+rMJ/25w+ubI8g
3 | e3ZTbm07VqLFz6uI2vXqN8X7/72dygtJlUw07FpR0oLXaSia0adaywz1JA==
4 | -----END PUBLIC KEY-----
5 |
--------------------------------------------------------------------------------
/backend/internal/core/services/contexts_test.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/google/uuid"
8 | "github.com/stretchr/testify/assert"
9 | "github.com/sysadminsmedia/homebox/backend/internal/data/repo"
10 | )
11 |
12 | func Test_SetAuthContext(t *testing.T) {
13 | user := &repo.UserOut{
14 | ID: uuid.New(),
15 | }
16 |
17 | token := uuid.New().String()
18 |
19 | ctx := SetUserCtx(context.Background(), user, token)
20 |
21 | ctxUser := UseUserCtx(ctx)
22 |
23 | assert.NotNil(t, ctxUser)
24 | assert.Equal(t, user.ID, ctxUser.ID)
25 |
26 | ctxUserToken := UseTokenCtx(ctx)
27 | assert.NotEmpty(t, ctxUserToken)
28 | }
29 |
30 | func Test_SetAuthContext_Nulls(t *testing.T) {
31 | ctx := SetUserCtx(context.Background(), nil, "")
32 |
33 | ctxUser := UseUserCtx(ctx)
34 |
35 | assert.Nil(t, ctxUser)
36 |
37 | ctxUserToken := UseTokenCtx(ctx)
38 | assert.Empty(t, ctxUserToken)
39 | }
40 |
--------------------------------------------------------------------------------
/backend/internal/core/services/reporting/.testdata/import/fields.csv:
--------------------------------------------------------------------------------
1 | HB.location,HB.name,HB.quantity,HB.description,HB.field.Custom Field 1,HB.field.Custom Field 2,HB.field.Custom Field 3
2 | loc,Item 1,1,Description 1,Value 1[1],Value 1[2],Value 1[3]
3 | loc,Item 2,2,Description 2,Value 2[1],Value 2[2],Value 2[3]
4 | loc,Item 3,3,Description 3,Value 3[1],Value 3[2],Value 3[3]
5 |
6 |
--------------------------------------------------------------------------------
/backend/internal/core/services/reporting/.testdata/import/minimal.csv:
--------------------------------------------------------------------------------
1 | HB.location,HB.name,HB.quantity,HB.description
2 | loc,Item 1,1,Description 1
3 | loc,Item 2,2,Description 2
4 | loc,Item 3,3,Description 3
--------------------------------------------------------------------------------
/backend/internal/core/services/reporting/.testdata/import/types.csv:
--------------------------------------------------------------------------------
1 | HB.name,HB.asset_id,HB.location,HB.labels
2 | Item 1,1,Path / To / Location 1,L1 ; L2 ; L3
3 | Item 2,000-002,Path /To/ Location 2,L1;L2;L3
4 | Item 3,1000-003,Path / To /Location 3 , L1;L2; L3
--------------------------------------------------------------------------------
/backend/internal/core/services/reporting/value_parsers.go:
--------------------------------------------------------------------------------
1 | package reporting
2 |
3 | import (
4 | "strconv"
5 | "strings"
6 | )
7 |
8 | func parseSeparatedString(s string, sep string) ([]string, error) {
9 | list := strings.Split(s, sep)
10 |
11 | csf := make([]string, 0, len(list))
12 | for _, s := range list {
13 | trimmed := strings.TrimSpace(s)
14 | if trimmed != "" {
15 | csf = append(csf, trimmed)
16 | }
17 | }
18 |
19 | return csf, nil
20 | }
21 |
22 | func parseFloat(s string) float64 {
23 | if s == "" {
24 | return 0
25 | }
26 | f, _ := strconv.ParseFloat(s, 64)
27 | return f
28 | }
29 |
30 | func parseBool(s string) bool {
31 | b, _ := strconv.ParseBool(s)
32 | return b
33 | }
34 |
35 | func parseInt(s string) int {
36 | i, _ := strconv.Atoi(s)
37 | return i
38 | }
39 |
--------------------------------------------------------------------------------
/backend/internal/core/services/service_group.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "errors"
5 | "time"
6 |
7 | "github.com/sysadminsmedia/homebox/backend/internal/data/repo"
8 | "github.com/sysadminsmedia/homebox/backend/pkgs/hasher"
9 | )
10 |
11 | type GroupService struct {
12 | repos *repo.AllRepos
13 | }
14 |
15 | func (svc *GroupService) UpdateGroup(ctx Context, data repo.GroupUpdate) (repo.Group, error) {
16 | if data.Name == "" {
17 | data.Name = ctx.User.GroupName
18 | }
19 |
20 | if data.Currency == "" {
21 | return repo.Group{}, errors.New("currency cannot be empty")
22 | }
23 |
24 | return svc.repos.Groups.GroupUpdate(ctx.Context, ctx.GID, data)
25 | }
26 |
27 | func (svc *GroupService) NewInvitation(ctx Context, uses int, expiresAt time.Time) (string, error) {
28 | token := hasher.GenerateToken()
29 |
30 | _, err := svc.repos.Groups.InvitationCreate(ctx, ctx.GID, repo.GroupInvitationCreate{
31 | Token: token.Hash,
32 | Uses: uses,
33 | ExpiresAt: expiresAt,
34 | })
35 | if err != nil {
36 | return "", err
37 | }
38 |
39 | return token.Raw, nil
40 | }
41 |
--------------------------------------------------------------------------------
/backend/internal/core/services/service_user_defaults.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "github.com/sysadminsmedia/homebox/backend/internal/data/repo"
5 | )
6 |
7 | func defaultLocations() []repo.LocationCreate {
8 | return []repo.LocationCreate{
9 | {
10 | Name: "Living Room",
11 | },
12 | {
13 | Name: "Garage",
14 | },
15 | {
16 | Name: "Kitchen",
17 | },
18 | {
19 | Name: "Bedroom",
20 | },
21 | {
22 | Name: "Bathroom",
23 | },
24 | {
25 | Name: "Office",
26 | },
27 | {
28 | Name: "Attic",
29 | },
30 | {
31 | Name: "Basement",
32 | },
33 | }
34 | }
35 |
36 | func defaultLabels() []repo.LabelCreate {
37 | return []repo.LabelCreate{
38 | {
39 | Name: "Appliances",
40 | },
41 | {
42 | Name: "IOT",
43 | },
44 | {
45 | Name: "Electronics",
46 | },
47 | {
48 | Name: "Servers",
49 | },
50 | {
51 | Name: "General",
52 | },
53 | {
54 | Name: "Important",
55 | },
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/backend/internal/data/ent/external.go:
--------------------------------------------------------------------------------
1 | package ent
2 |
3 | import (
4 | "database/sql"
5 |
6 | entsql "entgo.io/ent/dialect/sql"
7 | )
8 |
9 | // Sql exposes the underlying database connection in the ent client
10 | // so that we can use it to perform custom queries.
11 | func (c *Client) Sql() *sql.DB {
12 | return c.driver.(*entsql.Driver).DB()
13 | }
14 |
--------------------------------------------------------------------------------
/backend/internal/data/ent/generate.go:
--------------------------------------------------------------------------------
1 | package ent
2 |
3 | //go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature sql/versioned-migration ./schema --template=./schema/templates/has_id.tmpl
4 |
--------------------------------------------------------------------------------
/backend/internal/data/ent/has_id.go:
--------------------------------------------------------------------------------
1 | // Code generated by ent, DO NOT EDIT.
2 |
3 | package ent
4 |
5 | import "github.com/google/uuid"
6 |
7 | func (a *Attachment) GetID() uuid.UUID {
8 | return a.ID
9 | }
10 |
11 | func (ar *AuthRoles) GetID() int {
12 | return ar.ID
13 | }
14 |
15 | func (at *AuthTokens) GetID() uuid.UUID {
16 | return at.ID
17 | }
18 |
19 | func (gr *Group) GetID() uuid.UUID {
20 | return gr.ID
21 | }
22 |
23 | func (git *GroupInvitationToken) GetID() uuid.UUID {
24 | return git.ID
25 | }
26 |
27 | func (i *Item) GetID() uuid.UUID {
28 | return i.ID
29 | }
30 |
31 | func (_if *ItemField) GetID() uuid.UUID {
32 | return _if.ID
33 | }
34 |
35 | func (l *Label) GetID() uuid.UUID {
36 | return l.ID
37 | }
38 |
39 | func (l *Location) GetID() uuid.UUID {
40 | return l.ID
41 | }
42 |
43 | func (me *MaintenanceEntry) GetID() uuid.UUID {
44 | return me.ID
45 | }
46 |
47 | func (n *Notifier) GetID() uuid.UUID {
48 | return n.ID
49 | }
50 |
51 | func (u *User) GetID() uuid.UUID {
52 | return u.ID
53 | }
54 |
--------------------------------------------------------------------------------
/backend/internal/data/ent/runtime/runtime.go:
--------------------------------------------------------------------------------
1 | // Code generated by ent, DO NOT EDIT.
2 |
3 | package runtime
4 |
5 | // The schema-stitching logic is generated in github.com/sysadminsmedia/homebox/backend/internal/data/ent/runtime.go
6 |
7 | const (
8 | Version = "v0.14.4" // Version of ent codegen.
9 | Sum = "h1:/DhDraSLXIkBhyiVoJeSshr4ZYi7femzhj6/TckzZuI=" // Sum of ent codegen.
10 | )
11 |
--------------------------------------------------------------------------------
/backend/internal/data/ent/schema/attachment.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "entgo.io/ent"
5 | "entgo.io/ent/schema/edge"
6 | "entgo.io/ent/schema/field"
7 | "github.com/sysadminsmedia/homebox/backend/internal/data/ent/schema/mixins"
8 | )
9 |
10 | // Attachment holds the schema definition for the Attachment entity.
11 | type Attachment struct {
12 | ent.Schema
13 | }
14 |
15 | func (Attachment) Mixin() []ent.Mixin {
16 | return []ent.Mixin{
17 | mixins.BaseMixin{},
18 | }
19 | }
20 |
21 | // Fields of the Attachment.
22 | func (Attachment) Fields() []ent.Field {
23 | return []ent.Field{
24 | field.Enum("type").Values("photo", "manual", "warranty", "attachment", "receipt").Default("attachment"),
25 | field.Bool("primary").Default(false),
26 | field.String("title").Default(""),
27 | field.String("path").Default(""),
28 | }
29 | }
30 |
31 | // Edges of the Attachment.
32 | func (Attachment) Edges() []ent.Edge {
33 | return []ent.Edge{
34 | edge.From("item", Item.Type).
35 | Ref("attachments").
36 | Required().
37 | Unique(),
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/backend/internal/data/ent/schema/auth_roles.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "entgo.io/ent"
5 | "entgo.io/ent/schema/edge"
6 | "entgo.io/ent/schema/field"
7 | )
8 |
9 | // AuthRoles holds the schema definition for the AuthRoles entity.
10 | type AuthRoles struct {
11 | ent.Schema
12 | }
13 |
14 | // Fields of the AuthRoles.
15 | func (AuthRoles) Fields() []ent.Field {
16 | return []ent.Field{
17 | field.Enum("role").
18 | Default("user").
19 | Values(
20 | "admin", // can do everything - currently unused
21 | "user", // default login role
22 | "attachments", // Read Attachments
23 | ),
24 | }
25 | }
26 |
27 | // Edges of the AuthRoles.
28 | func (AuthRoles) Edges() []ent.Edge {
29 | return []ent.Edge{
30 | edge.From("token", AuthTokens.Type).
31 | Ref("roles").
32 | Unique(),
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/backend/internal/data/ent/schema/group_invitation_token.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "time"
5 |
6 | "entgo.io/ent"
7 | "entgo.io/ent/schema/edge"
8 | "entgo.io/ent/schema/field"
9 | "github.com/sysadminsmedia/homebox/backend/internal/data/ent/schema/mixins"
10 | )
11 |
12 | // GroupInvitationToken holds the schema definition for the GroupInvitationToken entity.
13 | type GroupInvitationToken struct {
14 | ent.Schema
15 | }
16 |
17 | func (GroupInvitationToken) Mixin() []ent.Mixin {
18 | return []ent.Mixin{
19 | mixins.BaseMixin{},
20 | }
21 | }
22 |
23 | // Fields of the GroupInvitationToken.
24 | func (GroupInvitationToken) Fields() []ent.Field {
25 | return []ent.Field{
26 | field.Bytes("token").
27 | Unique(),
28 | field.Time("expires_at").
29 | Default(func() time.Time { return time.Now().Add(time.Hour * 24 * 7) }),
30 | field.Int("uses").
31 | Default(0),
32 | }
33 | }
34 |
35 | // Edges of the GroupInvitationToken.
36 | func (GroupInvitationToken) Edges() []ent.Edge {
37 | return []ent.Edge{
38 | edge.From("group", Group.Type).
39 | Ref("invitation_tokens").
40 | Unique(),
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/backend/internal/data/ent/schema/item_field.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "time"
5 |
6 | "entgo.io/ent"
7 | "entgo.io/ent/schema/edge"
8 | "entgo.io/ent/schema/field"
9 | "github.com/sysadminsmedia/homebox/backend/internal/data/ent/schema/mixins"
10 | )
11 |
12 | // ItemField holds the schema definition for the ItemField entity.
13 | type ItemField struct {
14 | ent.Schema
15 | }
16 |
17 | func (ItemField) Mixin() []ent.Mixin {
18 | return []ent.Mixin{
19 | mixins.BaseMixin{},
20 | mixins.DetailsMixin{},
21 | }
22 | }
23 |
24 | // Fields of the ItemField.
25 | func (ItemField) Fields() []ent.Field {
26 | return []ent.Field{
27 | field.Enum("type").
28 | Values("text", "number", "boolean", "time"),
29 | field.String("text_value").
30 | MaxLen(500).
31 | Optional(),
32 | field.Int("number_value").
33 | Optional(),
34 | field.Bool("boolean_value").
35 | Default(false),
36 | field.Time("time_value").
37 | Default(time.Now),
38 | }
39 | }
40 |
41 | // Edges of the ItemField.
42 | func (ItemField) Edges() []ent.Edge {
43 | return []ent.Edge{
44 | edge.From("item", Item.Type).
45 | Ref("fields").
46 | Unique(),
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/backend/internal/data/ent/schema/label.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "entgo.io/ent"
5 | "entgo.io/ent/schema/edge"
6 | "entgo.io/ent/schema/field"
7 | "github.com/sysadminsmedia/homebox/backend/internal/data/ent/schema/mixins"
8 | )
9 |
10 | // Label holds the schema definition for the Label entity.
11 | type Label struct {
12 | ent.Schema
13 | }
14 |
15 | func (Label) Mixin() []ent.Mixin {
16 | return []ent.Mixin{
17 | mixins.BaseMixin{},
18 | mixins.DetailsMixin{},
19 | GroupMixin{ref: "labels"},
20 | }
21 | }
22 |
23 | // Fields of the Label.
24 | func (Label) Fields() []ent.Field {
25 | return []ent.Field{
26 | field.String("color").
27 | MaxLen(255).
28 | Optional(),
29 | }
30 | }
31 |
32 | // Edges of the Label.
33 | func (Label) Edges() []ent.Edge {
34 | return []ent.Edge{
35 | edge.To("items", Item.Type),
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/backend/internal/data/ent/schema/location.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "entgo.io/ent"
5 | "entgo.io/ent/dialect/entsql"
6 | "entgo.io/ent/schema/edge"
7 | "github.com/sysadminsmedia/homebox/backend/internal/data/ent/schema/mixins"
8 | )
9 |
10 | // Location holds the schema definition for the Location entity.
11 | type Location struct {
12 | ent.Schema
13 | }
14 |
15 | func (Location) Mixin() []ent.Mixin {
16 | return []ent.Mixin{
17 | mixins.BaseMixin{},
18 | mixins.DetailsMixin{},
19 | GroupMixin{ref: "locations"},
20 | }
21 | }
22 |
23 | // Fields of the Location.
24 | func (Location) Fields() []ent.Field {
25 | return nil
26 | }
27 |
28 | // Edges of the Location.
29 | func (Location) Edges() []ent.Edge {
30 | return []ent.Edge{
31 | edge.To("children", Location.Type).
32 | From("parent").
33 | Unique(),
34 | edge.To("items", Item.Type).
35 | Annotations(entsql.Annotation{
36 | OnDelete: entsql.Cascade,
37 | }),
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/backend/internal/data/ent/schema/maintenance_entry.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "entgo.io/ent"
5 | "entgo.io/ent/schema/edge"
6 | "entgo.io/ent/schema/field"
7 | "github.com/google/uuid"
8 | "github.com/sysadminsmedia/homebox/backend/internal/data/ent/schema/mixins"
9 | )
10 |
11 | type MaintenanceEntry struct {
12 | ent.Schema
13 | }
14 |
15 | func (MaintenanceEntry) Mixin() []ent.Mixin {
16 | return []ent.Mixin{
17 | mixins.BaseMixin{},
18 | }
19 | }
20 |
21 | func (MaintenanceEntry) Fields() []ent.Field {
22 | return []ent.Field{
23 | field.UUID("item_id", uuid.UUID{}),
24 | field.Time("date").
25 | Optional(),
26 | field.Time("scheduled_date").
27 | Optional(),
28 | field.String("name").
29 | MaxLen(255).
30 | NotEmpty(),
31 | field.String("description").
32 | MaxLen(2500).
33 | Optional(),
34 | field.Float("cost").
35 | Default(0.0),
36 | }
37 | }
38 |
39 | // Edges of the ItemField.
40 | func (MaintenanceEntry) Edges() []ent.Edge {
41 | return []ent.Edge{
42 | edge.From("item", Item.Type).
43 | Field("item_id").
44 | Ref("maintenance_entries").
45 | Required().
46 | Unique(),
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/backend/internal/data/ent/schema/mixins/base.go:
--------------------------------------------------------------------------------
1 | package mixins
2 |
3 | import (
4 | "time"
5 |
6 | "entgo.io/ent"
7 | "entgo.io/ent/schema/field"
8 | "entgo.io/ent/schema/mixin"
9 | "github.com/google/uuid"
10 | )
11 |
12 | type BaseMixin struct {
13 | mixin.Schema
14 | }
15 |
16 | func (BaseMixin) Fields() []ent.Field {
17 | return []ent.Field{
18 | field.UUID("id", uuid.UUID{}).
19 | Default(uuid.New),
20 | field.Time("created_at").
21 | Immutable().
22 | Default(time.Now),
23 | field.Time("updated_at").
24 | Default(time.Now).
25 | UpdateDefault(time.Now),
26 | }
27 | }
28 |
29 | type DetailsMixin struct {
30 | mixin.Schema
31 | }
32 |
33 | func (DetailsMixin) Fields() []ent.Field {
34 | return []ent.Field{
35 | field.String("name").
36 | MaxLen(255).
37 | NotEmpty(),
38 | field.String("description").
39 | MaxLen(1000).
40 | Optional(),
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/backend/internal/data/ent/schema/notifier.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "entgo.io/ent"
5 | "entgo.io/ent/schema/field"
6 | "entgo.io/ent/schema/index"
7 |
8 | "github.com/sysadminsmedia/homebox/backend/internal/data/ent/schema/mixins"
9 | )
10 |
11 | type Notifier struct {
12 | ent.Schema
13 | }
14 |
15 | func (Notifier) Mixin() []ent.Mixin {
16 | return []ent.Mixin{
17 | mixins.BaseMixin{},
18 | GroupMixin{
19 | ref: "notifiers",
20 | field: "group_id",
21 | },
22 | UserMixin{
23 | ref: "notifiers",
24 | field: "user_id",
25 | },
26 | }
27 | }
28 |
29 | // Fields of the Notifier.
30 | func (Notifier) Fields() []ent.Field {
31 | return []ent.Field{
32 | field.String("name").
33 | MaxLen(255).
34 | NotEmpty(),
35 | field.String("url").
36 | Sensitive().
37 | MaxLen(2083). // supposed max length of URL
38 | NotEmpty(),
39 | field.Bool("is_active").
40 | Default(true),
41 | }
42 | }
43 |
44 | func (Notifier) Indexes() []ent.Index {
45 | return []ent.Index{
46 | index.Fields("user_id"),
47 | index.Fields("user_id", "is_active"),
48 | index.Fields("group_id"),
49 | index.Fields("group_id", "is_active"),
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/backend/internal/data/ent/schema/templates/has_id.tmpl:
--------------------------------------------------------------------------------
1 | {{/* The line below tells Intellij/GoLand to enable the autocompletion based on the *gen.Graph type. */}}
2 | {{/* gotype: entgo.io/ent/entc/gen.Graph */}}
3 |
4 | {{ define "has_id" }}
5 |
6 | {{/* Add the base header for the generated file */}}
7 | {{ $pkg := base $.Config.Package }}
8 | {{ template "header" $ }}
9 | import "github.com/google/uuid"
10 | {{/* Loop over all nodes and implement the "HasID" interface */}}
11 | {{ range $n := $.Nodes }}
12 | {{ if not $n.ID }}
13 | {{/* If the node doesn't have an ID field, we skip it. */}}
14 | {{ continue }}
15 | {{ end }}
16 | {{/* The "HasID" interface is implemented by the "ID" method. */}}
17 | {{ $receiver := $n.Receiver }}
18 | func ({{ $receiver }} *{{ $n.Name }}) GetID() {{ $n.ID.Type }} {
19 | return {{ $receiver }}.ID
20 | }
21 | {{ end }}
22 |
23 | {{ end }}
24 |
--------------------------------------------------------------------------------
/backend/internal/data/migrations/migrations.go:
--------------------------------------------------------------------------------
1 | // Package migrations
2 | package migrations
3 |
4 | import (
5 | "embed"
6 | "github.com/rs/zerolog/log"
7 | )
8 |
9 | //go:embed all:postgres
10 | var postgresFiles embed.FS
11 |
12 | //go:embed all:sqlite3
13 | var sqliteFiles embed.FS
14 |
15 | // Migrations returns the embedded file system containing the SQL migration files
16 | // for the specified SQL dialect. It uses the "embed" package to include the
17 | // migration files in the binary at build time. The function takes a string
18 | // parameter "dialect" which specifies the SQL dialect to use. It returns an
19 | // embedded file system containing the migration files for the specified dialect.
20 | func Migrations(dialect string) embed.FS {
21 | switch dialect {
22 | case "postgres":
23 | return postgresFiles
24 | case "sqlite3":
25 | return sqliteFiles
26 | default:
27 | log.Fatal().Str("dialect", dialect).Msg("unknown sql dialect")
28 | }
29 | // This should never get hit, but just in case
30 | return sqliteFiles
31 | }
32 |
--------------------------------------------------------------------------------
/backend/internal/data/migrations/postgres/20250112202302_sync_children.go:
--------------------------------------------------------------------------------
1 | package postgres
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 | "fmt"
7 | "github.com/pressly/goose/v3"
8 | )
9 |
10 | //nolint:gochecknoinits
11 | func init() {
12 | goose.AddMigrationContext(Up20250112202302, Down20250112202302)
13 | }
14 |
15 | func Up20250112202302(ctx context.Context, tx *sql.Tx) error {
16 | columnName := "sync_child_items_locations"
17 | query := `
18 | SELECT column_name
19 | FROM information_schema.columns
20 | WHERE table_name = 'items' AND column_name = 'sync_child_items_locations';
21 | `
22 | err := tx.QueryRowContext(ctx, query).Scan(&columnName)
23 | if err != nil {
24 | // Column does not exist, proceed with migration
25 | _, err = tx.ExecContext(ctx, `
26 | ALTER TABLE "items" ADD COLUMN "sync_child_items_locations" boolean NOT NULL DEFAULT false;
27 | `)
28 | if err != nil {
29 | return fmt.Errorf("failed to execute migration: %w", err)
30 | }
31 | }
32 | return nil
33 | }
34 |
35 | func Down20250112202302(ctx context.Context, tx *sql.Tx) error {
36 | // This migration is a no-op for Postgres.
37 | return nil
38 | }
39 |
--------------------------------------------------------------------------------
/backend/internal/data/migrations/postgres/20250419184104_merge_docs_attachments.sql:
--------------------------------------------------------------------------------
1 | -- +goose Up
2 |
3 | -- Step 1: Modify "attachments" table to add new columns
4 | ALTER TABLE "attachments" ADD COLUMN "title" character varying NOT NULL DEFAULT '', ADD COLUMN "path" character varying NOT NULL DEFAULT '';
5 |
6 | -- Update existing rows in "attachments" with data from "documents"
7 | UPDATE "attachments"
8 | SET "title" = d."title",
9 | "path" = d."path"
10 | FROM "documents" d
11 | WHERE "attachments"."document_attachments" = d."id";
12 |
13 | -- Step 3: Drop foreign key constraints referencing "documents"
14 | ALTER TABLE "attachments" DROP CONSTRAINT IF EXISTS "attachments_documents_attachments";
15 |
16 | -- Step 4: Drop the "document_attachments" column
17 | ALTER TABLE "attachments" DROP COLUMN IF EXISTS "document_attachments";
18 |
19 | -- Step 5: Drop the "documents" table
20 | DROP TABLE IF EXISTS "documents";
--------------------------------------------------------------------------------
/backend/internal/data/migrations/postgres/main.go:
--------------------------------------------------------------------------------
1 | // Package postgres provides the PostgreSQL database migration
2 | package postgres
3 |
4 | // This file exists to make Goose happy. It really doesn't do anything else.
5 |
--------------------------------------------------------------------------------
/backend/internal/data/migrations/sqlite3/main.go:
--------------------------------------------------------------------------------
1 | // Package sqlite3 provides the SQLite3 database migration
2 | package sqlite3
3 |
4 | // This file exists to make Goose happy. It really doesn't do anything else.
5 |
--------------------------------------------------------------------------------
/backend/internal/data/repo/automappers.go:
--------------------------------------------------------------------------------
1 | package repo
2 |
3 | type MapFunc[T any, U any] func(T) U
4 |
5 | func (a MapFunc[T, U]) Map(v T) U {
6 | return a(v)
7 | }
8 |
9 | func (a MapFunc[T, U]) MapEach(v []T) []U {
10 | result := make([]U, len(v))
11 | for i, item := range v {
12 | result[i] = a(item)
13 | }
14 | return result
15 | }
16 |
17 | func (a MapFunc[T, U]) MapErr(v T, err error) (U, error) {
18 | if err != nil {
19 | var zero U
20 | return zero, err
21 | }
22 |
23 | return a(v), nil
24 | }
25 |
26 | func (a MapFunc[T, U]) MapEachErr(v []T, err error) ([]U, error) {
27 | if err != nil {
28 | return nil, err
29 | }
30 |
31 | return a.MapEach(v), nil
32 | }
33 |
--------------------------------------------------------------------------------
/backend/internal/data/repo/id_set.go:
--------------------------------------------------------------------------------
1 | package repo
2 |
3 | import (
4 | "github.com/google/uuid"
5 | "github.com/sysadminsmedia/homebox/backend/pkgs/set"
6 | )
7 |
8 | // HasID is an interface to entities that have an ID uuid.UUID field and a GetID() method.
9 | // This interface is fulfilled by all entities generated by entgo.io/ent via a custom template
10 | type HasID interface {
11 | GetID() uuid.UUID
12 | }
13 |
14 | func newIDSet[T HasID](entities []T) set.Set[uuid.UUID] {
15 | uuids := make([]uuid.UUID, 0, len(entities))
16 | for _, e := range entities {
17 | uuids = append(uuids, e.GetID())
18 | }
19 |
20 | return set.New(uuids...)
21 | }
22 |
--------------------------------------------------------------------------------
/backend/internal/data/repo/pagination.go:
--------------------------------------------------------------------------------
1 | package repo
2 |
3 | type PaginationResult[T any] struct {
4 | Page int `json:"page"`
5 | PageSize int `json:"pageSize"`
6 | Total int `json:"total"`
7 | Items []T `json:"items"`
8 | }
9 |
10 | func calculateOffset(page, pageSize int) int {
11 | return (page - 1) * pageSize
12 | }
13 |
--------------------------------------------------------------------------------
/backend/internal/data/repo/query_helpers.go:
--------------------------------------------------------------------------------
1 | package repo
2 |
3 | import "time"
4 |
5 | func sqliteDateFormat(t time.Time) string {
6 | return t.Format("2006-01-02 15:04:05")
7 | }
8 |
9 | // orDefault returns the value of the pointer if it is not nil, otherwise it returns the default value
10 | //
11 | // This is used for nullable or potentially nullable fields (or aggregates) in the database when running
12 | // queries. If the field is null, the pointer will be nil, so we return the default value instead.
13 | func orDefault[T any](v *T, def T) T {
14 | if v == nil {
15 | return def
16 | }
17 | return *v
18 | }
19 |
--------------------------------------------------------------------------------
/backend/internal/sys/config/conf_database.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | const (
4 | DriverSqlite3 = "sqlite3"
5 | )
6 |
7 | type Storage struct {
8 | // Data is the path to the root directory
9 | Data string `yaml:"data" conf:"default:./.data"`
10 | }
11 |
12 | type Database struct {
13 | Driver string `yaml:"driver" conf:"default:sqlite3"`
14 | Username string `yaml:"username"`
15 | Password string `yaml:"password"`
16 | Host string `yaml:"host"`
17 | Port string `yaml:"port"`
18 | Database string `yaml:"database"`
19 | SslMode string `yaml:"ssl_mode"`
20 | SqlitePath string `yaml:"sqlite_path" conf:"default:./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite"`
21 | }
22 |
--------------------------------------------------------------------------------
/backend/internal/sys/config/conf_logger.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | const (
4 | LogFormatJSON = "json"
5 | LogFormatText = "text"
6 | )
7 |
8 | type LoggerConf struct {
9 | Level string `conf:"default:info"`
10 | Format string `conf:"default:text"`
11 | }
12 |
--------------------------------------------------------------------------------
/backend/internal/sys/config/conf_mailer.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | type MailerConf struct {
4 | Host string `conf:""`
5 | Port int `conf:""`
6 | Username string `conf:""`
7 | Password string `conf:""`
8 | From string `conf:""`
9 | }
10 |
11 | // Ready is a simple check to ensure that the configuration is not empty.
12 | // or with it's default state.
13 | func (mc *MailerConf) Ready() bool {
14 | return mc.Host != "" && mc.Port != 0 && mc.Username != "" && mc.Password != "" && mc.From != ""
15 | }
16 |
--------------------------------------------------------------------------------
/backend/internal/sys/config/conf_mailer_test.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func Test_MailerReady_Success(t *testing.T) {
10 | mc := &MailerConf{
11 | Host: "host",
12 | Port: 1,
13 | Username: "username",
14 | Password: "password",
15 | From: "from",
16 | }
17 |
18 | assert.True(t, mc.Ready())
19 | }
20 |
21 | func Test_MailerReady_Failure(t *testing.T) {
22 | mc := &MailerConf{}
23 | assert.False(t, mc.Ready())
24 |
25 | mc.Host = "host"
26 | assert.False(t, mc.Ready())
27 |
28 | mc.Port = 1
29 | assert.False(t, mc.Ready())
30 |
31 | mc.Username = "username"
32 | assert.False(t, mc.Ready())
33 |
34 | mc.Password = "password"
35 | assert.False(t, mc.Ready())
36 |
37 | mc.From = "from"
38 | assert.True(t, mc.Ready())
39 | }
40 |
--------------------------------------------------------------------------------
/backend/internal/web/adapters/adapters.go:
--------------------------------------------------------------------------------
1 | package adapters
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/google/uuid"
7 | )
8 |
9 | type AdapterFunc[T any, Y any] func(*http.Request, T) (Y, error)
10 | type IDFunc[T any, Y any] func(*http.Request, uuid.UUID, T) (Y, error)
11 |
--------------------------------------------------------------------------------
/backend/internal/web/adapters/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Package adapters offers common adapters for turing regular functions into HTTP Handlers
3 | There are three types of adapters
4 |
5 | - Query adapters
6 | - Action adapters
7 | - Command adapters
8 | */
9 | package adapters
10 |
--------------------------------------------------------------------------------
/backend/internal/web/mid/doc.go:
--------------------------------------------------------------------------------
1 | // Package mid provides web middleware.
2 | package mid
3 |
--------------------------------------------------------------------------------
/backend/pkgs/faker/random.go:
--------------------------------------------------------------------------------
1 | // Package faker provides a simple interface for generating fake data for testing.
2 | package faker
3 |
4 | import (
5 | "math/rand"
6 | "time"
7 | )
8 |
9 | var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
10 |
11 | type Faker struct{}
12 |
13 | func NewFaker() *Faker {
14 | return &Faker{}
15 | }
16 |
17 | func (f *Faker) Time() time.Time {
18 | return time.Now().Add(time.Duration(f.Num(1, 100)) * time.Hour)
19 | }
20 |
21 | func (f *Faker) Str(length int) string {
22 | b := make([]rune, length)
23 | for i := range b {
24 | b[i] = letters[rand.Intn(len(letters))]
25 | }
26 | return string(b)
27 | }
28 |
29 | func (f *Faker) Path() string {
30 | return "/" + f.Str(10) + "/" + f.Str(10) + "/" + f.Str(10)
31 | }
32 |
33 | func (f *Faker) Email() string {
34 | return f.Str(10) + "@example.com"
35 | }
36 |
37 | func (f *Faker) Bool() bool {
38 | return rand.Intn(2) == 1
39 | }
40 |
41 | func (f *Faker) Num(min, max int) int {
42 | return rand.Intn(max-min) + min
43 | }
44 |
--------------------------------------------------------------------------------
/backend/pkgs/hasher/doc.go:
--------------------------------------------------------------------------------
1 | // Package hasher provides a simple interface for hashing and verifying passwords.
2 | package hasher
3 |
--------------------------------------------------------------------------------
/backend/pkgs/hasher/password.go:
--------------------------------------------------------------------------------
1 | package hasher
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "golang.org/x/crypto/bcrypt"
8 | )
9 |
10 | var enabled = true
11 |
12 | func init() { // nolint: gochecknoinits
13 | disableHas := os.Getenv("UNSAFE_DISABLE_PASSWORD_PROJECTION") == "yes_i_am_sure"
14 |
15 | if disableHas {
16 | fmt.Println("WARNING: Password protection is disabled. This is unsafe in production.")
17 | enabled = false
18 | }
19 | }
20 |
21 | func HashPassword(password string) (string, error) {
22 | if !enabled {
23 | return password, nil
24 | }
25 |
26 | bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
27 | return string(bytes), err
28 | }
29 |
30 | func CheckPasswordHash(password, hash string) bool {
31 | if !enabled {
32 | return password == hash
33 | }
34 |
35 | err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
36 | return err == nil
37 | }
38 |
--------------------------------------------------------------------------------
/backend/pkgs/hasher/password_test.go:
--------------------------------------------------------------------------------
1 | package hasher
2 |
3 | import "testing"
4 |
5 | func TestHashPassword(t *testing.T) {
6 | t.Parallel()
7 | type args struct {
8 | password string
9 | }
10 | tests := []struct {
11 | name string
12 | args args
13 | wantErr bool
14 | }{
15 | {
16 | name: "letters_and_numbers",
17 | args: args{
18 | password: "password123456788",
19 | },
20 | },
21 | {
22 | name: "letters_number_and_special",
23 | args: args{
24 | password: "!2afj3214pofajip3142j;fa",
25 | },
26 | },
27 | }
28 | for _, tt := range tests {
29 | t.Run(tt.name, func(t *testing.T) {
30 | got, err := HashPassword(tt.args.password)
31 | if (err != nil) != tt.wantErr {
32 | t.Errorf("HashPassword() error = %v, wantErr %v", err, tt.wantErr)
33 | return
34 | }
35 | if !CheckPasswordHash(tt.args.password, got) {
36 | t.Errorf("CheckPasswordHash() failed to validate password=%v against hash=%v", tt.args.password, got)
37 | }
38 | })
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/backend/pkgs/hasher/token.go:
--------------------------------------------------------------------------------
1 | package hasher
2 |
3 | import (
4 | "crypto/rand"
5 | "crypto/sha256"
6 | "encoding/base32"
7 | )
8 |
9 | type Token struct {
10 | Raw string
11 | Hash []byte
12 | }
13 |
14 | func GenerateToken() Token {
15 | randomBytes := make([]byte, 16)
16 | _, _ = rand.Read(randomBytes)
17 |
18 | plainText := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(randomBytes)
19 | hash := HashToken(plainText)
20 |
21 | return Token{
22 | Raw: plainText,
23 | Hash: hash,
24 | }
25 | }
26 |
27 | func HashToken(plainTextToken string) []byte {
28 | hash := sha256.Sum256([]byte(plainTextToken))
29 | return hash[:]
30 | }
31 |
--------------------------------------------------------------------------------
/backend/pkgs/hasher/token_test.go:
--------------------------------------------------------------------------------
1 | package hasher
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | const ITERATIONS = 200
10 |
11 | func Test_NewToken(t *testing.T) {
12 | t.Parallel()
13 | tokens := make([]Token, ITERATIONS)
14 | for i := 0; i < ITERATIONS; i++ {
15 | tokens[i] = GenerateToken()
16 | }
17 |
18 | // Check if they are unique
19 | for i := 0; i < 5; i++ {
20 | for j := i + 1; j < 5; j++ {
21 | if tokens[i].Raw == tokens[j].Raw {
22 | t.Errorf("NewToken() failed to generate unique tokens")
23 | }
24 | }
25 | }
26 | }
27 |
28 | func Test_HashToken_CheckTokenHash(t *testing.T) {
29 | t.Parallel()
30 | for i := 0; i < ITERATIONS; i++ {
31 | token := GenerateToken()
32 |
33 | // Check raw text is reltively random
34 | for j := 0; j < 5; j++ {
35 | assert.NotEqual(t, token.Raw, GenerateToken().Raw)
36 | }
37 |
38 | // Check token length is less than 32 characters
39 | assert.Less(t, len(token.Raw), 32)
40 |
41 | // Check hash is the same
42 | assert.Equal(t, token.Hash, HashToken(token.Raw))
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/backend/pkgs/mailer/message.go:
--------------------------------------------------------------------------------
1 | package mailer
2 |
3 | import "net/mail"
4 |
5 | type Message struct {
6 | Subject string
7 | To mail.Address
8 | From mail.Address
9 | Body string
10 | }
11 |
12 | type MessageBuilder struct {
13 | subject string
14 | to mail.Address
15 | from mail.Address
16 | body string
17 | }
18 |
19 | func NewMessageBuilder() *MessageBuilder {
20 | return &MessageBuilder{}
21 | }
22 |
23 | func (mb *MessageBuilder) Build() *Message {
24 | return &Message{
25 | Subject: mb.subject,
26 | To: mb.to,
27 | From: mb.from,
28 | Body: mb.body,
29 | }
30 | }
31 |
32 | func (mb *MessageBuilder) SetSubject(subject string) *MessageBuilder {
33 | mb.subject = subject
34 | return mb
35 | }
36 |
37 | func (mb *MessageBuilder) SetTo(name, to string) *MessageBuilder {
38 | mb.to = mail.Address{
39 | Name: name,
40 | Address: to,
41 | }
42 | return mb
43 | }
44 |
45 | func (mb *MessageBuilder) SetFrom(name, from string) *MessageBuilder {
46 | mb.from = mail.Address{
47 | Name: name,
48 | Address: from,
49 | }
50 | return mb
51 | }
52 |
53 | func (mb *MessageBuilder) SetBody(body string) *MessageBuilder {
54 | mb.body = body
55 | return mb
56 | }
57 |
--------------------------------------------------------------------------------
/backend/pkgs/mailer/message_test.go:
--------------------------------------------------------------------------------
1 | package mailer
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func Test_MessageBuilder(t *testing.T) {
10 | t.Parallel()
11 |
12 | mb := NewMessageBuilder().
13 | SetBody("Hello World!").
14 | SetSubject("Hello").
15 | SetTo("John Doe", "john@doe.com").
16 | SetFrom("Jane Doe", "jane@doe.com")
17 |
18 | msg := mb.Build()
19 |
20 | assert.Equal(t, "Hello", msg.Subject)
21 | assert.Equal(t, "Hello World!", msg.Body)
22 | assert.Equal(t, "John Doe", msg.To.Name)
23 | assert.Equal(t, "john@doe.com", msg.To.Address)
24 | assert.Equal(t, "Jane Doe", msg.From.Name)
25 | assert.Equal(t, "jane@doe.com", msg.From.Address)
26 | }
27 |
--------------------------------------------------------------------------------
/backend/pkgs/mailer/test-mailer-template.json:
--------------------------------------------------------------------------------
1 | {
2 | "host": "",
3 | "port": 465,
4 | "username": "",
5 | "password": "",
6 | "from": ""
7 | }
--------------------------------------------------------------------------------
/backend/pkgs/set/set.go:
--------------------------------------------------------------------------------
1 | // Package set provides a simple set implementation.
2 | package set
3 |
4 | type key interface {
5 | comparable
6 | }
7 |
8 | type Set[T key] struct {
9 | mp map[T]struct{}
10 | }
11 |
12 | func Make[T key](size int) Set[T] {
13 | return Set[T]{
14 | mp: make(map[T]struct{}, size),
15 | }
16 | }
17 |
18 | func New[T key](v ...T) Set[T] {
19 | mp := make(map[T]struct{}, len(v))
20 |
21 | s := Set[T]{mp}
22 |
23 | s.Insert(v...)
24 | return s
25 | }
26 |
27 | func (s Set[T]) Insert(v ...T) {
28 | for _, e := range v {
29 | s.mp[e] = struct{}{}
30 | }
31 | }
32 |
33 | func (s Set[T]) Remove(v ...T) {
34 | for _, e := range v {
35 | delete(s.mp, e)
36 | }
37 | }
38 |
39 | func (s Set[T]) Contains(v T) bool {
40 | _, ok := s.mp[v]
41 | return ok
42 | }
43 |
44 | func (s Set[T]) ContainsAll(v ...T) bool {
45 | for _, e := range v {
46 | if !s.Contains(e) {
47 | return false
48 | }
49 | }
50 | return true
51 | }
52 |
53 | func (s Set[T]) Slice() []T {
54 | slice := make([]T, 0, len(s.mp))
55 | for k := range s.mp {
56 | slice = append(slice, k)
57 | }
58 | return slice
59 | }
60 |
61 | func (s Set[T]) Len() int {
62 | return len(s.mp)
63 | }
64 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | homebox:
3 | image: homebox
4 | build:
5 | context: .
6 | dockerfile: ./Dockerfile
7 | args:
8 | - COMMIT=head
9 | - BUILD_TIME=0001-01-01T00:00:00Z
10 | ports:
11 | - 3100:7745
12 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/index.ts:
--------------------------------------------------------------------------------
1 | // https://vitepress.dev/guide/custom-theme
2 | import { h } from 'vue'
3 | import type { Theme } from 'vitepress'
4 | import DefaultTheme from 'vitepress/theme'
5 | import './style.css'
6 |
7 | export default {
8 | extends: DefaultTheme,
9 | Layout: () => {
10 | return h(DefaultTheme.Layout, null, {
11 | // https://vitepress.dev/guide/extending-default-theme#layout-slots
12 | })
13 | },
14 | enhanceApp({ app, router, siteData }) {
15 | // ...
16 | }
17 | } satisfies Theme
18 |
--------------------------------------------------------------------------------
/docs/en/analytics/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | aside: false
3 | ---
4 |
5 | ## What is This?
6 | We collect non-identifying information from users of Homebox that have opted in to analytics collection. By default users do not send us anything, however once opted in that data gets sent to our own Plausibe instance and the data below is live from that instance.
7 |
8 | We make this data public so that everyone knows exactly what's being collected, and so that they can see the data we see as it helps us make some decisions.
9 |
10 | ## Current Analytics Collected
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/docs/en/images/home-screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sysadminsmedia/homebox/676b87d87e7d1ca1b6914af809b8fe7cec80f741/docs/en/images/home-screen.png
--------------------------------------------------------------------------------
/docs/en/quick-start.md:
--------------------------------------------------------------------------------
1 | # Quick Start
2 |
3 | > [!TIP]
4 | > If you're currently running the original version of Homebox ([https://github.com/hay-kot/homebox](https://github.com/hay-kot/homebox)) switching is easy, just follow the instructions in the [Migration Guide](./migration) to switch to the new version.
5 | 1. Install Homebox either by using [the latest Docker image](./installation#docker), or by downloading the correct executable for your Operating System from the [Releases](https://github.com/sysadminsmedia/homebox/releases). (See [Installation](./installation) for more details)
6 |
7 | 2. Browse to `http://SERVER_IP:3100` (if Using Docker) or `http://SERVER_IP:7745` (if installed locally) to access the included web User Interface.
8 |
9 | 3. Register your first user.
10 |
11 | 4. Login with the user you just created and start adding your locations and items!
12 |
13 | > [!TIP]
14 | > If you want other users to see your items and locations, they will need to sign up using your invite link, otherwise they will only see their own items. Go to the **Profile** section in the left navigation bar and under **User Profile**, click **Generate Invite Link**.
--------------------------------------------------------------------------------
/docs/en/upgrade.md:
--------------------------------------------------------------------------------
1 | # Upgrade
2 |
3 | ## From v0.17.x to v0.18+
4 |
5 | ::: danger Breaking Changes
6 | This upgrade process involves some potentially breaking changes, please review this documentation carefully before beginning the upgrade process, and follow it closely during your upgrade.
7 | :::
8 |
9 | ### Configuration Changes
10 | #### Database Configuration
11 | - `HBOX_STORAGE_SQLITE_URL` has been replaced by `HBOX_DATABASE_SQLITE_PATH`
12 | - `HBOX_DATABASE_DRIVER` has been added to set the database type, valid options are `sqlite3` and `postgres`
13 | - `HBOX_DATABASE_HOST`, `HBOX_DATABASE_PORT`, `HBOX_DATABASE_USERNAME`, `HBOX_DATABASE_DATABASE`, and `HBOX_DATABASE_SSL_MODE` have been added to configure postgres connection options.
14 |
15 | ::: tip
16 | If you don't have `HBOX_STORAGE_SQLITE_URL` set, you can ignore this change, as the default value for `HBOX_DATABASE_DRIVER` is `sqlite3`, and the default value for `HBOX_DATABASE_SQLITE_PATH` is the same as the old `HBOX_STORAGE_SQLITE_URL` value.
17 | :::
--------------------------------------------------------------------------------
/docs/public/_redirects:
--------------------------------------------------------------------------------
1 | / /en/ 302
2 |
3 | # This is an example for a french redirect
4 | # /* /fr/:splat 302 Language=fr
--------------------------------------------------------------------------------
/docs/public/homebox-email-banner.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sysadminsmedia/homebox/676b87d87e7d1ca1b6914af809b8fe7cec80f741/docs/public/homebox-email-banner.jpg
--------------------------------------------------------------------------------
/frontend/.nuxtignore:
--------------------------------------------------------------------------------
1 | pages/**/*.ts
--------------------------------------------------------------------------------
/frontend/app.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
28 |
--------------------------------------------------------------------------------
/frontend/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://shadcn-vue.com/schema.json",
3 | "style": "default",
4 | "typescript": true,
5 | "tailwind": {
6 | "config": "tailwind.config.js",
7 | "css": "assets/css/main.css",
8 | "baseColor": "slate",
9 | "cssVariables": true,
10 | "prefix": ""
11 | },
12 | "aliases": {
13 | "components": "@/components",
14 | "utils": "@/lib/utils"
15 | }
16 | }
--------------------------------------------------------------------------------
/frontend/components/Base/Container.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/components/Base/SectionHeader.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
18 |
--------------------------------------------------------------------------------
/frontend/components/DetailAction.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
40 |
--------------------------------------------------------------------------------
/frontend/components/Form/Checkbox.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ label }}
6 |
7 |
8 |
9 |
10 | {{ label }}
11 |
12 |
13 |
14 |
15 |
16 |
39 |
--------------------------------------------------------------------------------
/frontend/components/Item/View/Table.types.ts:
--------------------------------------------------------------------------------
1 | import type { ItemSummary } from "~~/lib/api/types/data-contracts";
2 |
3 | export type TableHeaderType = {
4 | text: string;
5 | value: keyof ItemSummary;
6 | sortable?: boolean;
7 | align?: "left" | "center" | "right";
8 | enabled: boolean;
9 | type?: "price" | "boolean" | "name" | "location" | "date";
10 | };
11 |
12 | export type TableData = Record;
13 |
--------------------------------------------------------------------------------
/frontend/components/Location/Tree/Root.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 | {{ $t("location.tree.no_locations") }}
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/frontend/components/Location/Tree/tree-state.ts:
--------------------------------------------------------------------------------
1 | import type { Ref } from "vue";
2 |
3 | type TreeState = Record;
4 |
5 | const store: Record> = {};
6 |
7 | export function newTreeKey(): string {
8 | return Math.random().toString(36).substring(2);
9 | }
10 |
11 | export function useTreeState(key: string): Ref {
12 | if (!store[key]) {
13 | store[key] = ref({});
14 | }
15 |
16 | return store[key];
17 | }
18 |
--------------------------------------------------------------------------------
/frontend/components/ModalConfirm.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ $t("global.confirm") }}
6 | {{ text || $t("global.delete_confirm") }}
7 |
8 |
9 |
10 | {{ $t("global.cancel") }}
11 |
12 |
13 | {{ $t("global.confirm") }}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
38 |
--------------------------------------------------------------------------------
/frontend/components/global/Currency.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ formattedValue }}
5 |
6 | {{ $t("global.loading") }}
7 |
8 |
9 |
10 |
46 |
--------------------------------------------------------------------------------
/frontend/components/global/DateTime.vue:
--------------------------------------------------------------------------------
1 |
2 | {{ value }}
3 |
4 |
5 |
28 |
--------------------------------------------------------------------------------
/frontend/components/global/DropZone.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/frontend/components/global/PasswordScore.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ $t("components.global.password_score.password_strength") }}: {{ message }}
4 |
5 |
6 |
7 |
8 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/frontend/components/global/Spacer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/frontend/components/global/StatCard/StatCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ title }}
5 |
6 |
7 |
8 | {{ value }}
9 |
10 | {{ subtitle }}
11 |
12 |
13 |
14 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/frontend/components/global/StatCard/types.ts:
--------------------------------------------------------------------------------
1 | export type StatsFormat = "currency" | "number" | "percent";
2 |
--------------------------------------------------------------------------------
/frontend/components/global/Subtitle.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/frontend/components/global/Table.types.ts:
--------------------------------------------------------------------------------
1 | export type TableHeader = {
2 | text: string;
3 | value: string;
4 | sortable?: boolean;
5 | align?: "left" | "center" | "right";
6 | };
7 |
8 | export type TableData = Record;
9 |
--------------------------------------------------------------------------------
/frontend/components/ui/alert-dialog/AlertDialog.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/components/ui/alert-dialog/AlertDialogAction.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/frontend/components/ui/alert-dialog/AlertDialogCancel.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/frontend/components/ui/alert-dialog/AlertDialogDescription.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/frontend/components/ui/alert-dialog/AlertDialogFooter.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/frontend/components/ui/alert-dialog/AlertDialogHeader.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/frontend/components/ui/alert-dialog/AlertDialogTitle.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/frontend/components/ui/alert-dialog/AlertDialogTrigger.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/frontend/components/ui/alert-dialog/index.ts:
--------------------------------------------------------------------------------
1 | export { default as AlertDialog } from './AlertDialog.vue'
2 | export { default as AlertDialogAction } from './AlertDialogAction.vue'
3 | export { default as AlertDialogCancel } from './AlertDialogCancel.vue'
4 | export { default as AlertDialogContent } from './AlertDialogContent.vue'
5 | export { default as AlertDialogDescription } from './AlertDialogDescription.vue'
6 | export { default as AlertDialogFooter } from './AlertDialogFooter.vue'
7 | export { default as AlertDialogHeader } from './AlertDialogHeader.vue'
8 | export { default as AlertDialogTitle } from './AlertDialogTitle.vue'
9 | export { default as AlertDialogTrigger } from './AlertDialogTrigger.vue'
10 |
--------------------------------------------------------------------------------
/frontend/components/ui/badge/Badge.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/frontend/components/ui/badge/index.ts:
--------------------------------------------------------------------------------
1 | import { cva, type VariantProps } from 'class-variance-authority'
2 |
3 | export { default as Badge } from './Badge.vue'
4 |
5 | export const badgeVariants = cva(
6 | 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
7 | {
8 | variants: {
9 | variant: {
10 | default:
11 | 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
12 | secondary:
13 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
14 | destructive:
15 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
16 | outline: 'text-foreground',
17 | },
18 | },
19 | defaultVariants: {
20 | variant: 'default',
21 | },
22 | },
23 | )
24 |
25 | export type BadgeVariants = VariantProps
26 |
--------------------------------------------------------------------------------
/frontend/components/ui/breadcrumb/Breadcrumb.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/frontend/components/ui/breadcrumb/BreadcrumbEllipsis.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
17 |
18 |
19 |
20 | More
21 |
22 |
23 |
--------------------------------------------------------------------------------
/frontend/components/ui/breadcrumb/BreadcrumbItem.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/frontend/components/ui/breadcrumb/BreadcrumbLink.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/frontend/components/ui/breadcrumb/BreadcrumbList.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/frontend/components/ui/breadcrumb/BreadcrumbPage.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/frontend/components/ui/breadcrumb/BreadcrumbSeparator.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/frontend/components/ui/breadcrumb/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Breadcrumb } from './Breadcrumb.vue'
2 | export { default as BreadcrumbEllipsis } from './BreadcrumbEllipsis.vue'
3 | export { default as BreadcrumbItem } from './BreadcrumbItem.vue'
4 | export { default as BreadcrumbLink } from './BreadcrumbLink.vue'
5 | export { default as BreadcrumbList } from './BreadcrumbList.vue'
6 | export { default as BreadcrumbPage } from './BreadcrumbPage.vue'
7 | export { default as BreadcrumbSeparator } from './BreadcrumbSeparator.vue'
8 |
--------------------------------------------------------------------------------
/frontend/components/ui/button/Button.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/frontend/components/ui/button/ButtonGroup.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/frontend/components/ui/card/Card.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/frontend/components/ui/card/CardContent.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/components/ui/card/CardDescription.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/components/ui/card/CardFooter.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/components/ui/card/CardHeader.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/components/ui/card/CardTitle.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/frontend/components/ui/card/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Card } from './Card.vue'
2 | export { default as CardContent } from './CardContent.vue'
3 | export { default as CardDescription } from './CardDescription.vue'
4 | export { default as CardFooter } from './CardFooter.vue'
5 | export { default as CardHeader } from './CardHeader.vue'
6 | export { default as CardTitle } from './CardTitle.vue'
7 |
--------------------------------------------------------------------------------
/frontend/components/ui/checkbox/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Checkbox } from './Checkbox.vue'
2 |
--------------------------------------------------------------------------------
/frontend/components/ui/command/Command.vue:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/frontend/components/ui/command/CommandDialog.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/frontend/components/ui/command/CommandEmpty.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/frontend/components/ui/command/CommandGroup.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
24 |
25 | {{ heading }}
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/frontend/components/ui/command/CommandInput.vue:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
26 |
27 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/frontend/components/ui/command/CommandItem.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/frontend/components/ui/command/CommandList.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/frontend/components/ui/command/CommandSeparator.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/frontend/components/ui/command/CommandShortcut.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/components/ui/command/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Command } from './Command.vue'
2 | export { default as CommandDialog } from './CommandDialog.vue'
3 | export { default as CommandEmpty } from './CommandEmpty.vue'
4 | export { default as CommandGroup } from './CommandGroup.vue'
5 | export { default as CommandInput } from './CommandInput.vue'
6 | export { default as CommandItem } from './CommandItem.vue'
7 | export { default as CommandList } from './CommandList.vue'
8 | export { default as CommandSeparator } from './CommandSeparator.vue'
9 | export { default as CommandShortcut } from './CommandShortcut.vue'
10 |
--------------------------------------------------------------------------------
/frontend/components/ui/dialog-provider/index.ts:
--------------------------------------------------------------------------------
1 | export { useDialog, useDialogHotkey } from "./utils";
2 | export { default as DialogProvider } from "./DialogProvider.vue";
3 |
--------------------------------------------------------------------------------
/frontend/components/ui/dialog/Dialog.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/frontend/components/ui/dialog/DialogClose.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/frontend/components/ui/dialog/DialogDescription.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/frontend/components/ui/dialog/DialogFooter.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/frontend/components/ui/dialog/DialogHeader.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/frontend/components/ui/dialog/DialogTitle.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/frontend/components/ui/dialog/DialogTrigger.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/frontend/components/ui/dialog/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Dialog } from './Dialog.vue'
2 | export { default as DialogClose } from './DialogClose.vue'
3 | export { default as DialogContent } from './DialogContent.vue'
4 | export { default as DialogDescription } from './DialogDescription.vue'
5 | export { default as DialogFooter } from './DialogFooter.vue'
6 | export { default as DialogHeader } from './DialogHeader.vue'
7 | export { default as DialogScrollContent } from './DialogScrollContent.vue'
8 | export { default as DialogTitle } from './DialogTitle.vue'
9 | export { default as DialogTrigger } from './DialogTrigger.vue'
10 |
--------------------------------------------------------------------------------
/frontend/components/ui/drawer/Drawer.vue:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/frontend/components/ui/drawer/DrawerContent.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/frontend/components/ui/drawer/DrawerDescription.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/frontend/components/ui/drawer/DrawerFooter.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/components/ui/drawer/DrawerHeader.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/components/ui/drawer/DrawerOverlay.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/frontend/components/ui/drawer/DrawerTitle.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/frontend/components/ui/drawer/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Drawer } from './Drawer.vue'
2 | export { default as DrawerContent } from './DrawerContent.vue'
3 | export { default as DrawerDescription } from './DrawerDescription.vue'
4 | export { default as DrawerFooter } from './DrawerFooter.vue'
5 | export { default as DrawerHeader } from './DrawerHeader.vue'
6 | export { default as DrawerOverlay } from './DrawerOverlay.vue'
7 | export { default as DrawerTitle } from './DrawerTitle.vue'
8 | export { DrawerClose, DrawerPortal, DrawerTrigger } from 'vaul-vue'
9 |
--------------------------------------------------------------------------------
/frontend/components/ui/dropdown-menu/DropdownMenu.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/frontend/components/ui/dropdown-menu/DropdownMenuGroup.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/frontend/components/ui/dropdown-menu/DropdownMenuItem.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/frontend/components/ui/dropdown-menu/DropdownMenuLabel.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/frontend/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/frontend/components/ui/dropdown-menu/DropdownMenuSeparator.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/frontend/components/ui/dropdown-menu/DropdownMenuShortcut.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/components/ui/dropdown-menu/DropdownMenuSub.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/frontend/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/frontend/components/ui/dropdown-menu/DropdownMenuTrigger.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/frontend/components/ui/dropdown-menu/index.ts:
--------------------------------------------------------------------------------
1 | export { default as DropdownMenu } from "./DropdownMenu.vue";
2 |
3 | export { default as DropdownMenuCheckboxItem } from "./DropdownMenuCheckboxItem.vue";
4 | export { default as DropdownMenuContent } from "./DropdownMenuContent.vue";
5 | export { default as DropdownMenuGroup } from "./DropdownMenuGroup.vue";
6 | export { default as DropdownMenuItem } from "./DropdownMenuItem.vue";
7 | export { default as DropdownMenuLabel } from "./DropdownMenuLabel.vue";
8 | export { default as DropdownMenuRadioGroup } from "./DropdownMenuRadioGroup.vue";
9 | export { default as DropdownMenuRadioItem } from "./DropdownMenuRadioItem.vue";
10 | export { default as DropdownMenuSeparator } from "./DropdownMenuSeparator.vue";
11 | export { default as DropdownMenuShortcut } from "./DropdownMenuShortcut.vue";
12 | export { default as DropdownMenuSub } from "./DropdownMenuSub.vue";
13 | export { default as DropdownMenuSubContent } from "./DropdownMenuSubContent.vue";
14 | export { default as DropdownMenuSubTrigger } from "./DropdownMenuSubTrigger.vue";
15 | export { default as DropdownMenuTrigger } from "./DropdownMenuTrigger.vue";
16 | export { DropdownMenuPortal } from "reka-ui";
17 |
--------------------------------------------------------------------------------
/frontend/components/ui/input/Input.vue:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
32 |
33 |
--------------------------------------------------------------------------------
/frontend/components/ui/input/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Input } from "./Input.vue";
2 |
--------------------------------------------------------------------------------
/frontend/components/ui/label/Label.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/frontend/components/ui/label/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Label } from './Label.vue'
2 |
--------------------------------------------------------------------------------
/frontend/components/ui/pagination/PaginationEllipsis.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/frontend/components/ui/pagination/PaginationFirst.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/frontend/components/ui/pagination/PaginationLast.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/frontend/components/ui/pagination/PaginationNext.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/frontend/components/ui/pagination/PaginationPrev.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/frontend/components/ui/pagination/index.ts:
--------------------------------------------------------------------------------
1 | export { default as PaginationEllipsis } from './PaginationEllipsis.vue'
2 | export { default as PaginationFirst } from './PaginationFirst.vue'
3 | export { default as PaginationLast } from './PaginationLast.vue'
4 | export { default as PaginationNext } from './PaginationNext.vue'
5 | export { default as PaginationPrev } from './PaginationPrev.vue'
6 | export {
7 | PaginationRoot as Pagination,
8 | PaginationList,
9 | PaginationListItem,
10 | } from 'reka-ui'
11 |
--------------------------------------------------------------------------------
/frontend/components/ui/popover/Popover.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/frontend/components/ui/popover/PopoverTrigger.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/frontend/components/ui/popover/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Popover } from './Popover.vue'
2 | export { default as PopoverContent } from './PopoverContent.vue'
3 | export { default as PopoverTrigger } from './PopoverTrigger.vue'
4 |
--------------------------------------------------------------------------------
/frontend/components/ui/progress/Progress.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
26 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/frontend/components/ui/progress/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Progress } from './Progress.vue'
2 |
--------------------------------------------------------------------------------
/frontend/components/ui/select/Select.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/frontend/components/ui/select/SelectGroup.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/frontend/components/ui/select/SelectItemText.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/frontend/components/ui/select/SelectLabel.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/frontend/components/ui/select/SelectScrollDownButton.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/frontend/components/ui/select/SelectScrollUpButton.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/frontend/components/ui/select/SelectSeparator.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/frontend/components/ui/select/SelectTrigger.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/frontend/components/ui/select/SelectValue.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/frontend/components/ui/select/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Select } from './Select.vue'
2 | export { default as SelectContent } from './SelectContent.vue'
3 | export { default as SelectGroup } from './SelectGroup.vue'
4 | export { default as SelectItem } from './SelectItem.vue'
5 | export { default as SelectItemText } from './SelectItemText.vue'
6 | export { default as SelectLabel } from './SelectLabel.vue'
7 | export { default as SelectScrollDownButton } from './SelectScrollDownButton.vue'
8 | export { default as SelectScrollUpButton } from './SelectScrollUpButton.vue'
9 | export { default as SelectSeparator } from './SelectSeparator.vue'
10 | export { default as SelectTrigger } from './SelectTrigger.vue'
11 | export { default as SelectValue } from './SelectValue.vue'
12 |
--------------------------------------------------------------------------------
/frontend/components/ui/separator/Separator.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
22 | {{ props.label }}
32 |
33 |
34 |
--------------------------------------------------------------------------------
/frontend/components/ui/separator/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Separator } from "./Separator.vue";
2 |
--------------------------------------------------------------------------------
/frontend/components/ui/sheet/Sheet.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/components/ui/sheet/SheetClose.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/frontend/components/ui/sheet/SheetDescription.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/frontend/components/ui/sheet/SheetFooter.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/frontend/components/ui/sheet/SheetHeader.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/frontend/components/ui/sheet/SheetTitle.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/frontend/components/ui/sheet/SheetTrigger.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/frontend/components/ui/shortcut/Shortcut.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
27 |
32 | {{ key }}
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/frontend/components/ui/shortcut/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Shortcut } from './Shortcut.vue'
2 |
--------------------------------------------------------------------------------
/frontend/components/ui/sidebar/SidebarContent.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/frontend/components/ui/sidebar/SidebarFooter.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/components/ui/sidebar/SidebarGroup.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/components/ui/sidebar/SidebarGroupAction.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/frontend/components/ui/sidebar/SidebarGroupContent.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/components/ui/sidebar/SidebarGroupLabel.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/frontend/components/ui/sidebar/SidebarHeader.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/components/ui/sidebar/SidebarInput.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/frontend/components/ui/sidebar/SidebarInset.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/frontend/components/ui/sidebar/SidebarMenu.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/components/ui/sidebar/SidebarMenuBadge.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/frontend/components/ui/sidebar/SidebarMenuButtonChild.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/frontend/components/ui/sidebar/SidebarMenuItem.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/components/ui/sidebar/SidebarMenuLink.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/frontend/components/ui/sidebar/SidebarMenuSkeleton.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/frontend/components/ui/sidebar/SidebarMenuSub.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
23 |
24 |
--------------------------------------------------------------------------------
/frontend/components/ui/sidebar/SidebarMenuSubItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/frontend/components/ui/sidebar/SidebarSeparator.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/frontend/components/ui/sidebar/SidebarTrigger.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 | Toggle Sidebar
20 |
21 |
22 |
--------------------------------------------------------------------------------
/frontend/components/ui/sidebar/utils.ts:
--------------------------------------------------------------------------------
1 | import type { ComputedRef, Ref } from "vue";
2 | import { createContext } from "reka-ui";
3 |
4 | export const SIDEBAR_COOKIE_NAME = "sidebar:state";
5 | export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
6 | export const SIDEBAR_WIDTH = "16rem";
7 | export const SIDEBAR_WIDTH_MOBILE = "18rem";
8 | export const SIDEBAR_WIDTH_ICON = "3.5rem";
9 | export const SIDEBAR_KEYBOARD_SHORTCUT = "b";
10 |
11 | export const [useSidebar, provideSidebarContext] = createContext<{
12 | state: ComputedRef<"expanded" | "collapsed">;
13 | open: Ref;
14 | setOpen: (value: boolean) => void;
15 | isMobile: Ref;
16 | openMobile: Ref;
17 | setOpenMobile: (value: boolean) => void;
18 | toggleSidebar: () => void;
19 | }>("Sidebar");
20 |
--------------------------------------------------------------------------------
/frontend/components/ui/skeleton/Skeleton.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/components/ui/skeleton/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Skeleton } from "./Skeleton.vue";
2 |
--------------------------------------------------------------------------------
/frontend/components/ui/sonner/Sonner.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
24 |
25 |
--------------------------------------------------------------------------------
/frontend/components/ui/sonner/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Toaster } from './Sonner.vue'
2 | export { toast } from './toast'
--------------------------------------------------------------------------------
/frontend/components/ui/sonner/toast.ts:
--------------------------------------------------------------------------------
1 | import { toast as internalToast } from "vue-sonner";
2 |
3 | // triggering too many toasts at once can cause the toaster to not render properly https://github.com/xiaoluoboding/vue-sonner/issues/98
4 |
5 | const wrapToast = any>(fn: T): ((...args: Parameters) => Promise>) => {
6 | return (...args: Parameters) =>
7 | new Promise(resolve => {
8 | setTimeout(() => resolve(fn(...args)), 0);
9 | });
10 | };
11 |
12 | const toast = (...args: Parameters) => internalToast(...args);
13 |
14 | toast.success = wrapToast(internalToast.success);
15 | toast.info = wrapToast(internalToast.info);
16 | toast.warning = wrapToast(internalToast.warning);
17 | toast.error = wrapToast(internalToast.error);
18 | toast.message = wrapToast(internalToast.message);
19 |
20 | export { toast };
21 |
--------------------------------------------------------------------------------
/frontend/components/ui/switch/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Switch } from './Switch.vue'
2 |
--------------------------------------------------------------------------------
/frontend/components/ui/table/Table.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
16 |
17 |
--------------------------------------------------------------------------------
/frontend/components/ui/table/TableBody.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/components/ui/table/TableCaption.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/components/ui/table/TableCell.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/frontend/components/ui/table/TableEmpty.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/frontend/components/ui/table/TableFooter.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/components/ui/table/TableHead.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/components/ui/table/TableHeader.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/components/ui/table/TableRow.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/components/ui/table/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Table } from './Table.vue'
2 | export { default as TableBody } from './TableBody.vue'
3 | export { default as TableCaption } from './TableCaption.vue'
4 | export { default as TableCell } from './TableCell.vue'
5 | export { default as TableEmpty } from './TableEmpty.vue'
6 | export { default as TableFooter } from './TableFooter.vue'
7 | export { default as TableHead } from './TableHead.vue'
8 | export { default as TableHeader } from './TableHeader.vue'
9 | export { default as TableRow } from './TableRow.vue'
10 |
--------------------------------------------------------------------------------
/frontend/components/ui/tags-input/TagsInput.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/frontend/components/ui/tags-input/TagsInputInput.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/frontend/components/ui/tags-input/TagsInputItem.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/frontend/components/ui/tags-input/TagsInputItemDelete.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/frontend/components/ui/tags-input/TagsInputItemText.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/frontend/components/ui/tags-input/index.ts:
--------------------------------------------------------------------------------
1 | export { default as TagsInput } from './TagsInput.vue'
2 | export { default as TagsInputInput } from './TagsInputInput.vue'
3 | export { default as TagsInputItem } from './TagsInputItem.vue'
4 | export { default as TagsInputItemDelete } from './TagsInputItemDelete.vue'
5 | export { default as TagsInputItemText } from './TagsInputItemText.vue'
6 |
--------------------------------------------------------------------------------
/frontend/components/ui/textarea/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Textarea } from './Textarea.vue'
2 |
--------------------------------------------------------------------------------
/frontend/components/ui/tooltip/Tooltip.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/components/ui/tooltip/TooltipProvider.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/frontend/components/ui/tooltip/TooltipTrigger.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/frontend/components/ui/tooltip/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Tooltip } from "./Tooltip.vue";
2 | export { default as TooltipContent } from "./TooltipContent.vue";
3 | export { default as TooltipProvider } from "./TooltipProvider.vue";
4 | export { default as TooltipTrigger } from "./TooltipTrigger.vue";
5 |
--------------------------------------------------------------------------------
/frontend/composables/use-defer.ts:
--------------------------------------------------------------------------------
1 | type DeferFunction = (...args: TArgs) => TReturn;
2 |
3 | // useDefer is a function that takes a function and returns a function that
4 | // calls the original function and then calls the onComplete function.
5 | export function useDefer(
6 | onComplete: (...args: TArgs) => void,
7 | func: DeferFunction
8 | ): DeferFunction {
9 | return (...args: TArgs) => {
10 | let result: TReturn;
11 | try {
12 | result = func(...args);
13 | } finally {
14 | onComplete(...args);
15 | }
16 |
17 | return result;
18 | };
19 | }
20 |
--------------------------------------------------------------------------------
/frontend/composables/use-item-search.ts:
--------------------------------------------------------------------------------
1 | import type { ItemSummary, LabelSummary, LocationSummary } from "~~/lib/api/types/data-contracts";
2 | import type { UserClient } from "~~/lib/api/user";
3 |
4 | type SearchOptions = {
5 | immediate?: boolean;
6 | };
7 |
8 | export function useItemSearch(client: UserClient, opts?: SearchOptions) {
9 | const query = ref("");
10 | const locations = ref([]);
11 | const labels = ref([]);
12 | const results = ref([]);
13 | const includeArchived = ref(false);
14 |
15 | watchDebounced(query, search, { debounce: 250, maxWait: 1000 });
16 | async function search() {
17 | const locIds = locations.value.map(l => l.id);
18 | const labelIds = labels.value.map(l => l.id);
19 |
20 | const { data, error } = await client.items.getAll({
21 | q: query.value,
22 | locations: locIds,
23 | labels: labelIds,
24 | includeArchived: includeArchived.value,
25 | });
26 | if (error) {
27 | return;
28 | }
29 | results.value = data.items;
30 | }
31 |
32 | if (opts?.immediate) {
33 | search();
34 | }
35 |
36 | return {
37 | query,
38 | results,
39 | locations,
40 | labels,
41 | };
42 | }
43 |
--------------------------------------------------------------------------------
/frontend/composables/use-min-loader.ts:
--------------------------------------------------------------------------------
1 | import type { WritableComputedRef } from "vue";
2 |
3 | export function useMinLoader(ms = 500): WritableComputedRef {
4 | const loading = ref(false);
5 |
6 | const locked = ref(false);
7 |
8 | const minLoading = computed({
9 | get: () => loading.value,
10 | set: value => {
11 | if (value) {
12 | loading.value = true;
13 |
14 | if (!locked.value) {
15 | locked.value = true;
16 | setTimeout(() => {
17 | locked.value = false;
18 | }, ms);
19 | }
20 | }
21 |
22 | if (!value && !locked.value) {
23 | loading.value = false;
24 | } else if (!value && locked.value) {
25 | setTimeout(() => {
26 | loading.value = false;
27 | }, ms);
28 | }
29 | },
30 | });
31 | return minLoading;
32 | }
33 |
--------------------------------------------------------------------------------
/frontend/composables/use-password-score.ts:
--------------------------------------------------------------------------------
1 | import type { ComputedRef, Ref } from "vue";
2 | import { scorePassword } from "~~/lib/passwords";
3 |
4 | export interface PasswordScore {
5 | score: ComputedRef;
6 | message: ComputedRef;
7 | isValid: ComputedRef;
8 | }
9 |
10 | export function usePasswordScore(pw: Ref, min = 30): PasswordScore {
11 | const score = computed(() => {
12 | return scorePassword(pw.value) || 0;
13 | });
14 |
15 | const message = computed(() => {
16 | if (score.value < 20) {
17 | return "Very weak";
18 | } else if (score.value < 40) {
19 | return "Weak";
20 | } else if (score.value < 60) {
21 | return "Good";
22 | } else if (score.value < 80) {
23 | return "Strong";
24 | }
25 | return "Very strong";
26 | });
27 |
28 | const isValid = computed(() => {
29 | return score.value >= min;
30 | });
31 |
32 | return {
33 | score,
34 | isValid,
35 | message,
36 | };
37 | }
38 |
--------------------------------------------------------------------------------
/frontend/composables/utils.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 | import { maybeUrl } from "./utils";
3 |
4 | describe("maybeURL works as expected", () => {
5 | test("basic valid URL case", () => {
6 | const result = maybeUrl("https://example.com");
7 | expect(result.isUrl).toBe(true);
8 | expect(result.url).toBe("https://example.com");
9 | expect(result.text).toBe("https://example.com");
10 | });
11 |
12 | test("special URL syntax", () => {
13 | const result = maybeUrl("[My Text](http://example.com)");
14 | expect(result.isUrl).toBe(true);
15 | expect(result.url).toBe("http://example.com");
16 | expect(result.text).toBe("My Text");
17 | });
18 |
19 | test("not a url", () => {
20 | const result = maybeUrl("not a url");
21 | expect(result.isUrl).toBe(false);
22 | expect(result.url).toBe("");
23 | expect(result.text).toBe("");
24 | });
25 |
26 | test("malformed special syntax", () => {
27 | const result = maybeUrl("[My Text(http://example.com)");
28 | expect(result.isUrl).toBe(false);
29 | expect(result.url).toBe("");
30 | expect(result.text).toBe("");
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/frontend/error.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/frontend/global.d.ts:
--------------------------------------------------------------------------------
1 | ///
--------------------------------------------------------------------------------
/frontend/layouts/empty.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/frontend/lib/api/__test__/public.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect } from "vitest";
2 | import { factories } from "./factories";
3 |
4 | describe("[GET] /api/v1/status", () => {
5 | test("server should respond", async () => {
6 | const api = factories.client.public();
7 | const { response, data } = await api.status();
8 | expect(response.status).toBe(200);
9 | expect(data.health).toBe(true);
10 | });
11 | });
12 |
13 | describe("first time user workflow (register, login, join group)", () => {
14 | const api = factories.client.public();
15 | const userData = factories.user();
16 |
17 | test("user should be able to register", async () => {
18 | const { response } = await api.register(userData);
19 | expect(response.status).toBe(204);
20 | });
21 |
22 | test("user should be able to login", async () => {
23 | const { response, data } = await api.login(userData.email, userData.password);
24 | expect(response.status).toBe(200);
25 | expect(data.token).toBeTruthy();
26 |
27 | // Cleanup
28 | const userApi = factories.client.user(data.token);
29 | {
30 | const { response } = await userApi.user.delete();
31 | expect(response.status).toBe(204);
32 | }
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/frontend/lib/api/__test__/user/user.test.ts:
--------------------------------------------------------------------------------
1 | import { faker } from "@faker-js/faker";
2 | import { describe, expect, test } from "vitest";
3 | import { factories } from "../factories";
4 |
5 | describe("basic user workflows", () => {
6 | test("user should be able to change password", async () => {
7 | const { client, user } = await factories.client.singleUse();
8 | const password = faker.internet.password();
9 |
10 | // Change Password
11 | {
12 | const response = await client.user.changePassword(user.password, password);
13 | expect(response.error).toBeFalsy();
14 | expect(response.status).toBe(204);
15 | }
16 |
17 | // Ensure New Login is Valid
18 | {
19 | const pub = factories.client.public();
20 | const response = await pub.login(user.email, password);
21 | expect(response.error).toBeFalsy();
22 | expect(response.status).toBe(200);
23 | }
24 |
25 | await client.user.delete();
26 | }, 20000);
27 | });
28 |
--------------------------------------------------------------------------------
/frontend/lib/api/base/base-api.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 | import { hasKey, parseDate } from "./base-api";
3 |
4 | describe("hasKey works as expected", () => {
5 | test("hasKey returns true if the key exists", () => {
6 | const obj = { createdAt: "2021-01-01" };
7 | expect(hasKey(obj, "createdAt")).toBe(true);
8 | });
9 |
10 | test("hasKey returns false if the key does not exist", () => {
11 | const obj = { createdAt: "2021-01-01" };
12 | expect(hasKey(obj, "updatedAt")).toBe(false);
13 | });
14 | });
15 |
16 | describe("parseDate should work as expected", () => {
17 | test("parseDate should set defaults", () => {
18 | const obj = { createdAt: "2021-01-01", updatedAt: "2021-01-01" };
19 | const result = parseDate(obj);
20 | expect(result.createdAt).toBeInstanceOf(Date);
21 | expect(result.updatedAt).toBeInstanceOf(Date);
22 | });
23 |
24 | test("parseDate should set passed in types", () => {
25 | const obj = { key1: "2021-01-01", key2: "2021-01-01" };
26 | const result = parseDate(obj, ["key1", "key2"]);
27 | expect(result.key1).toBeInstanceOf(Date);
28 | expect(result.key2).toBeInstanceOf(Date);
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/frontend/lib/api/base/index.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from "vitest";
2 | import { route } from ".";
3 |
4 | describe("UrlBuilder", () => {
5 | it("basic query parameter", () => {
6 | const result = route("/test", { a: "b" });
7 | expect(result).toBe("/api/v1/test?a=b");
8 | });
9 |
10 | it("multiple query parameters", () => {
11 | const result = route("/test", { a: "b", c: "d" });
12 | expect(result).toBe("/api/v1/test?a=b&c=d");
13 | });
14 |
15 | it("no query parameters", () => {
16 | const result = route("/test");
17 | expect(result).toBe("/api/v1/test");
18 | });
19 |
20 | it("list-like query parameters", () => {
21 | const result = route("/test", { a: ["b", "c"] });
22 | expect(result).toBe("/api/v1/test?a=b&a=c");
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/frontend/lib/api/base/index.ts:
--------------------------------------------------------------------------------
1 | export { BaseAPI } from "./base-api";
2 | export { route } from "./urls";
3 |
--------------------------------------------------------------------------------
/frontend/lib/api/classes/actions.ts:
--------------------------------------------------------------------------------
1 | import { BaseAPI, route } from "../base";
2 | import type { ActionAmountResult } from "../types/data-contracts";
3 |
4 | export class ActionsAPI extends BaseAPI {
5 | ensureAssetIDs() {
6 | return this.http.post({
7 | url: route("/actions/ensure-asset-ids"),
8 | });
9 | }
10 |
11 | resetItemDateTimes() {
12 | return this.http.post({
13 | url: route("/actions/zero-item-time-fields"),
14 | });
15 | }
16 |
17 | ensureImportRefs() {
18 | return this.http.post({
19 | url: route("/actions/ensure-import-refs"),
20 | });
21 | }
22 |
23 | setPrimaryPhotos() {
24 | return this.http.post({
25 | url: route("/actions/set-primary-photos"),
26 | });
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/frontend/lib/api/classes/assets.ts:
--------------------------------------------------------------------------------
1 | import { BaseAPI, route } from "../base";
2 | import type { ItemSummary } from "../types/data-contracts";
3 | import type { PaginationResult } from "../types/non-generated";
4 |
5 | export class AssetsApi extends BaseAPI {
6 | async get(id: string, page = 1, pageSize = 50) {
7 | return await this.http.get>({
8 | url: route(`/assets/${id}`, { page, pageSize }),
9 | });
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/lib/api/classes/group.ts:
--------------------------------------------------------------------------------
1 | import { BaseAPI, route } from "../base";
2 | import type {
3 | CurrenciesCurrency,
4 | Group,
5 | GroupInvitation,
6 | GroupInvitationCreate,
7 | GroupUpdate,
8 | } from "../types/data-contracts";
9 |
10 | export class GroupApi extends BaseAPI {
11 | createInvitation(data: GroupInvitationCreate) {
12 | return this.http.post({
13 | url: route("/groups/invitations"),
14 | body: data,
15 | });
16 | }
17 |
18 | update(data: GroupUpdate) {
19 | return this.http.put({
20 | url: route("/groups"),
21 | body: data,
22 | });
23 | }
24 |
25 | get() {
26 | return this.http.get({
27 | url: route("/groups"),
28 | });
29 | }
30 |
31 | currencies() {
32 | return this.http.get({
33 | url: route("/currencies"),
34 | });
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/frontend/lib/api/classes/labels.ts:
--------------------------------------------------------------------------------
1 | import { BaseAPI, route } from "../base";
2 | import type { LabelCreate, LabelOut } from "../types/data-contracts";
3 |
4 | export class LabelsApi extends BaseAPI {
5 | getAll() {
6 | return this.http.get({ url: route("/labels") });
7 | }
8 |
9 | create(body: LabelCreate) {
10 | return this.http.post({ url: route("/labels"), body });
11 | }
12 |
13 | get(id: string) {
14 | return this.http.get({ url: route(`/labels/${id}`) });
15 | }
16 |
17 | delete(id: string) {
18 | return this.http.delete({ url: route(`/labels/${id}`) });
19 | }
20 |
21 | update(id: string, body: LabelCreate) {
22 | return this.http.put({ url: route(`/labels/${id}`), body });
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/frontend/lib/api/classes/locations.ts:
--------------------------------------------------------------------------------
1 | import { BaseAPI, route } from "../base";
2 | import type { LocationOutCount, LocationCreate, LocationOut, LocationUpdate, TreeItem } from "../types/data-contracts";
3 |
4 | export type LocationsQuery = {
5 | filterChildren: boolean;
6 | };
7 |
8 | export type TreeQuery = {
9 | withItems: boolean;
10 | };
11 |
12 | export class LocationsApi extends BaseAPI {
13 | getAll(q: LocationsQuery = { filterChildren: false }) {
14 | return this.http.get({ url: route("/locations", q) });
15 | }
16 |
17 | getTree(tq = { withItems: false }) {
18 | return this.http.get({ url: route("/locations/tree", tq) });
19 | }
20 |
21 | create(body: LocationCreate) {
22 | return this.http.post({ url: route("/locations"), body });
23 | }
24 |
25 | get(id: string) {
26 | return this.http.get({ url: route(`/locations/${id}`) });
27 | }
28 |
29 | delete(id: string) {
30 | return this.http.delete({ url: route(`/locations/${id}`) });
31 | }
32 |
33 | update(id: string, body: LocationUpdate) {
34 | return this.http.put({ url: route(`/locations/${id}`), body });
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/frontend/lib/api/classes/maintenance.ts:
--------------------------------------------------------------------------------
1 | import { BaseAPI, route } from "../base";
2 | import type {
3 | MaintenanceEntry,
4 | MaintenanceEntryWithDetails,
5 | MaintenanceEntryUpdate,
6 | MaintenanceFilterStatus,
7 | } from "../types/data-contracts";
8 |
9 | export interface MaintenanceFilters {
10 | status?: MaintenanceFilterStatus;
11 | }
12 |
13 | export class MaintenanceAPI extends BaseAPI {
14 | getAll(filters: MaintenanceFilters) {
15 | return this.http.get({
16 | url: route(`/maintenance`, { status: filters.status?.toString() }),
17 | });
18 | }
19 |
20 | delete(id: string) {
21 | return this.http.delete({ url: route(`/maintenance/${id}`) });
22 | }
23 |
24 | update(id: string, data: MaintenanceEntryUpdate) {
25 | return this.http.put({
26 | url: route(`/maintenance/${id}`),
27 | body: data,
28 | });
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/frontend/lib/api/classes/notifiers.ts:
--------------------------------------------------------------------------------
1 | import { BaseAPI, route } from "../base";
2 | import type { NotifierCreate, NotifierOut, NotifierUpdate } from "../types/data-contracts";
3 |
4 | export class NotifiersAPI extends BaseAPI {
5 | getAll() {
6 | return this.http.get({ url: route("/notifiers") });
7 | }
8 |
9 | create(body: NotifierCreate) {
10 | return this.http.post({ url: route("/notifiers"), body });
11 | }
12 |
13 | update(id: string, body: NotifierUpdate) {
14 | if (body.url === "") {
15 | body.url = null;
16 | }
17 |
18 | return this.http.put({ url: route(`/notifiers/${id}`), body });
19 | }
20 |
21 | delete(id: string) {
22 | return this.http.delete({ url: route(`/notifiers/${id}`) });
23 | }
24 |
25 | test(url: string) {
26 | return this.http.post<{ url: string }, null>({ url: route(`/notifiers/test`), body: { url } });
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/frontend/lib/api/classes/reports.ts:
--------------------------------------------------------------------------------
1 | import { BaseAPI, route } from "../base";
2 |
3 | export class ReportsAPI extends BaseAPI {
4 | billOfMaterialsURL(): string {
5 | return route("/reporting/bill-of-materials");
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/frontend/lib/api/classes/users.ts:
--------------------------------------------------------------------------------
1 | import { BaseAPI, route } from "../base";
2 | import type { ChangePassword, UserOut } from "../types/data-contracts";
3 | import type { Result } from "../types/non-generated";
4 |
5 | export class UserApi extends BaseAPI {
6 | public self() {
7 | return this.http.get>({ url: route("/users/self") });
8 | }
9 |
10 | public logout() {
11 | return this.http.post({ url: route("/users/logout") });
12 | }
13 |
14 | public delete() {
15 | return this.http.delete({ url: route("/users/self") });
16 | }
17 |
18 | public changePassword(current: string, newPassword: string) {
19 | return this.http.put({
20 | url: route("/users/self/change-password"),
21 | body: {
22 | current,
23 | new: newPassword,
24 | },
25 | });
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/frontend/lib/api/public.ts:
--------------------------------------------------------------------------------
1 | import { BaseAPI, route } from "./base";
2 | import type { APISummary, LoginForm, TokenResponse, UserRegistration } from "./types/data-contracts";
3 |
4 | export type StatusResult = {
5 | health: boolean;
6 | versions: string[];
7 | title: string;
8 | message: string;
9 | };
10 |
11 | export class PublicApi extends BaseAPI {
12 | public status() {
13 | return this.http.get({ url: route("/status") });
14 | }
15 |
16 | public login(username: string, password: string, stayLoggedIn = false) {
17 | return this.http.post({
18 | url: route("/users/login"),
19 | body: {
20 | username,
21 | password,
22 | stayLoggedIn,
23 | },
24 | });
25 | }
26 |
27 | public register(body: UserRegistration) {
28 | return this.http.post({ url: route("/users/register"), body });
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/frontend/lib/api/types/non-generated.ts:
--------------------------------------------------------------------------------
1 | export enum AttachmentTypes {
2 | Photo = "photo",
3 | Manual = "manual",
4 | Warranty = "warranty",
5 | Attachment = "attachment",
6 | Receipt = "receipt",
7 | }
8 |
9 | export type Result = {
10 | item: T;
11 | };
12 |
13 | export interface PaginationResult {
14 | items: T[];
15 | page: number;
16 | pageSize: number;
17 | total: number;
18 | }
19 |
20 | export interface ItemSummaryPaginationResult extends PaginationResult {
21 | totalPrice: number;
22 | }
23 |
--------------------------------------------------------------------------------
/frontend/lib/datelib/datelib.ts:
--------------------------------------------------------------------------------
1 | import { addDays } from "date-fns";
2 |
3 | /*
4 | * Formats a date as a string
5 | * */
6 | export function format(date: Date | string): string {
7 | if (typeof date === "string") {
8 | return date;
9 | }
10 | return date.toISOString().split("T")[0];
11 | }
12 |
13 | export function zeroTime(date: Date): Date {
14 | return new Date(
15 | new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime() - date.getTimezoneOffset() * 60000
16 | );
17 | }
18 |
19 | export function factorRange(offset: number = 7): [Date, Date] {
20 | const date = zeroTime(new Date());
21 |
22 | return [date, addDays(date, offset)];
23 | }
24 |
25 | export function factory(offset = 0): Date {
26 | if (offset) {
27 | return addDays(zeroTime(new Date()), offset);
28 | }
29 |
30 | return zeroTime(new Date());
31 | }
32 |
33 | export function parse(yyyyMMdd: string): Date {
34 | const parts = yyyyMMdd.split("-");
35 | return new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
36 | }
37 |
--------------------------------------------------------------------------------
/frontend/lib/passwords/index.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect } from "vitest";
2 | import { scorePassword } from ".";
3 |
4 | describe("scorePassword tests", () => {
5 | test("flagged words should return negative number", () => {
6 | const flaggedWords = ["password", "homebox", "admin", "qwerty", "login"];
7 |
8 | for (const word of flaggedWords) {
9 | expect(scorePassword(word)).toBe(0);
10 | }
11 | });
12 |
13 | test("should return 0 for empty string", () => {
14 | expect(scorePassword("")).toBe(0);
15 | });
16 |
17 | test("should return 0 for strings less than 6", () => {
18 | expect(scorePassword("12345")).toBe(0);
19 | });
20 |
21 | test("should return positive number for long string", () => {
22 | const result = expect(scorePassword("123456"));
23 | result.toBeGreaterThan(0);
24 | result.toBeLessThan(31);
25 | });
26 |
27 | test("should return max number for long string with all variations", () => {
28 | expect(scorePassword("3bYWcfYOwqxljqeOmQXTLlBwkrH6HV")).toBe(100);
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/frontend/lib/requests/index.ts:
--------------------------------------------------------------------------------
1 | export { Requests, type TResponse } from "./requests";
2 |
--------------------------------------------------------------------------------
/frontend/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/frontend/locales/lb-LU.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/frontend/locales/ta-IN.json:
--------------------------------------------------------------------------------
1 | {
2 | "components": {
3 | "global": {
4 | "date_time": {
5 | "days": "நாட்கள்",
6 | "hour": "மணி",
7 | "hours": "மணிகள்",
8 | "in": "{0} இல்",
9 | "just-now": "இப்பொழுது",
10 | "last-month": "கடந்த மாதம்",
11 | "last-week": "கடந்த வாரம்"
12 | }
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/frontend/middleware/auth.ts:
--------------------------------------------------------------------------------
1 | export default defineNuxtRouteMiddleware(async () => {
2 | const ctx = useAuthContext();
3 | const api = useUserApi();
4 | const redirectTo = useState("authRedirect");
5 |
6 | if (!ctx.isAuthorized()) {
7 | if (window.location.pathname !== "/") {
8 | console.debug("[middleware/auth] isAuthorized returned false, redirecting to /");
9 | redirectTo.value = window.location.pathname;
10 | return navigateTo("/");
11 | }
12 | }
13 |
14 | if (!ctx.user) {
15 | console.log("Fetching user data");
16 | const { data, error } = await api.user.self();
17 | if (error) {
18 | if (window.location.pathname !== "/") {
19 | console.debug("[middleware/user] user is null and fetch failed, redirecting to /");
20 | redirectTo.value = window.location.pathname;
21 | return navigateTo("/");
22 | }
23 | }
24 |
25 | ctx.user = data.item;
26 | }
27 | });
28 |
--------------------------------------------------------------------------------
/frontend/pages/a/[id].vue:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/frontend/pages/home/statistics.ts:
--------------------------------------------------------------------------------
1 | import { useI18n } from "vue-i18n";
2 | import type { UserClient } from "~~/lib/api/user";
3 |
4 | type StatCard = {
5 | label: string;
6 | value: number;
7 | type: "currency" | "number";
8 | };
9 |
10 | export function statCardData(api: UserClient) {
11 | const { t } = useI18n();
12 |
13 | const { data: statistics } = useAsyncData(async () => {
14 | const { data } = await api.stats.group();
15 | return data;
16 | });
17 |
18 | return computed(() => {
19 | return [
20 | {
21 | label: t("home.total_value"),
22 | value: statistics.value?.totalItemPrice || 0,
23 | type: "currency",
24 | },
25 | {
26 | label: t("home.total_items"),
27 | value: statistics.value?.totalItems || 0,
28 | type: "number",
29 | },
30 | {
31 | label: t("home.total_locations"),
32 | value: statistics.value?.totalLocations || 0,
33 | type: "number",
34 | },
35 | {
36 | label: t("home.total_labels"),
37 | value: statistics.value?.totalLabels || 0,
38 | type: "number",
39 | },
40 | ] as StatCard[];
41 | });
42 | }
43 |
--------------------------------------------------------------------------------
/frontend/pages/home/table.ts:
--------------------------------------------------------------------------------
1 | import type { UserClient } from "~~/lib/api/user";
2 |
3 | export function itemsTable(api: UserClient) {
4 | const { data: items, refresh } = useAsyncData(async () => {
5 | const { data } = await api.items.getAll({
6 | page: 1,
7 | pageSize: 5,
8 | orderBy: "createdAt",
9 | });
10 | return data.items;
11 | });
12 |
13 | onServerEvent(ServerEvent.ItemMutation, () => {
14 | console.log("item mutation");
15 | refresh();
16 | });
17 |
18 | return computed(() => {
19 | return {
20 | items: items.value || [],
21 | };
22 | });
23 | }
24 |
--------------------------------------------------------------------------------
/frontend/pages/item/[id]/index/maintenance.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/frontend/pages/maintenance.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 | {{ $t("menu.maintenance") }}
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/frontend/plugins/scroll.client.ts:
--------------------------------------------------------------------------------
1 | export default defineNuxtPlugin(nuxtApp => {
2 | nuxtApp.hook("page:finish", () => {
3 | document.body.scrollTo({ top: 0 });
4 | });
5 | });
6 |
--------------------------------------------------------------------------------
/frontend/public/no-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sysadminsmedia/homebox/676b87d87e7d1ca1b6914af809b8fe7cec80f741/frontend/public/no-image.jpg
--------------------------------------------------------------------------------
/frontend/public/pwa-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sysadminsmedia/homebox/676b87d87e7d1ca1b6914af809b8fe7cec80f741/frontend/public/pwa-192x192.png
--------------------------------------------------------------------------------
/frontend/public/pwa-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sysadminsmedia/homebox/676b87d87e7d1ca1b6914af809b8fe7cec80f741/frontend/public/pwa-512x512.png
--------------------------------------------------------------------------------
/frontend/public/set-theme.js:
--------------------------------------------------------------------------------
1 | try {
2 | console.log('Setting theme');
3 | const theme = JSON.parse(
4 | localStorage.getItem('homebox/preferences/location')
5 | ).theme;
6 | if (theme) {
7 | document.documentElement.setAttribute('data-theme', theme);
8 | document.documentElement.classList.add('theme-' + theme);
9 | }
10 | } catch (e) {
11 | console.error('Failed to set theme', e);
12 | }
13 |
--------------------------------------------------------------------------------
/frontend/stores/labels.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from "pinia";
2 | import type { LabelOut } from "~~/lib/api/types/data-contracts";
3 |
4 | export const useLabelStore = defineStore("labels", {
5 | state: () => ({
6 | allLabels: null as LabelOut[] | null,
7 | client: useUserApi(),
8 | }),
9 | getters: {
10 | /**
11 | * labels represents the labels that are currently in the store. The store is
12 | * synched with the server by intercepting the API calls and updating on the
13 | * response.
14 | */
15 | labels(state): LabelOut[] {
16 | if (state.allLabels === null) {
17 | this.client.labels.getAll().then(result => {
18 | if (result.error) {
19 | console.error(result.error);
20 | }
21 |
22 | this.allLabels = result.data;
23 | });
24 | }
25 | return state.allLabels ?? [];
26 | },
27 | },
28 | actions: {
29 | async refresh() {
30 | const result = await this.client.labels.getAll();
31 | if (result.error) {
32 | return result;
33 | }
34 |
35 | this.allLabels = result.data;
36 | return result;
37 | },
38 | },
39 | });
40 |
--------------------------------------------------------------------------------
/frontend/test/config.ts:
--------------------------------------------------------------------------------
1 | export const PORT = "7745";
2 | export const HOST = "http://127.0.0.1";
3 | export const BASE_URL = HOST + ":" + PORT;
4 |
--------------------------------------------------------------------------------
/frontend/test/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, devices } from "@playwright/test";
2 |
3 | export default defineConfig({
4 | testDir: "./e2e",
5 | fullyParallel: true,
6 | forbidOnly: !!process.env.CI,
7 | retries: process.env.CI ? 2 : 1,
8 | reporter: process.env.CI ? "blob" : "html",
9 | use: {
10 | baseURL: process.env.E2E_BASE_URL || "http://localhost:3000",
11 | trace: "on-all-retries",
12 | video: "retry-with-video",
13 | },
14 | projects: [
15 | {
16 | name: "chromium",
17 | use: { ...devices["Desktop Chrome"] },
18 | },
19 | {
20 | name: "firefox",
21 | use: { ...devices["Desktop Firefox"] },
22 | },
23 | {
24 | name: "webkit",
25 | use: { ...devices["Desktop Safari"] },
26 | },
27 | ],
28 | globalTeardown: require.resolve("./playwright.teardown"),
29 | });
30 |
--------------------------------------------------------------------------------
/frontend/test/playwright.teardown.ts:
--------------------------------------------------------------------------------
1 | import { exec } from "child_process";
2 |
3 | function globalTeardown() {
4 | if (process.env.TEST_SHUTDOWN_API_SERVER) {
5 | const pc = exec("pkill -SIGTERM api"); // Kill background API process
6 | const fr = exec("pkill -SIGTERM task"); // Kill background Frontend process
7 | pc.stdout?.on("data", (data: void) => {
8 | console.log(`stdout: ${data}`);
9 | });
10 | fr.stdout?.on("data", (data: void) => {
11 | console.log(`stdout: ${data}`);
12 | });
13 | }
14 | }
15 |
16 | export default globalTeardown;
17 |
--------------------------------------------------------------------------------
/frontend/test/setup.ts:
--------------------------------------------------------------------------------
1 | import { exec } from "child_process";
2 | import * as config from "./config";
3 |
4 | export const setup = () => {
5 | console.log("Starting Client Tests");
6 | console.log({
7 | PORT: config.PORT,
8 | HOST: config.HOST,
9 | BASE_URL: config.BASE_URL,
10 | });
11 | };
12 |
13 | export const teardown = () => {
14 | if (process.env.TEST_SHUTDOWN_API_SERVER) {
15 | const pc = exec("pkill -SIGTERM api"); // Kill background API process
16 | pc.stdout?.on("data", data => {
17 | console.log(`stdout: ${data}`);
18 | });
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/frontend/test/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import { defineConfig } from "vite";
3 |
4 | export default defineConfig({
5 | // @ts-ignore
6 | test: {
7 | globalSetup: "./test/setup.ts",
8 | include: ["**/*.test.ts"],
9 | },
10 | resolve: {
11 | alias: {
12 | "@": path.resolve(__dirname, ".."),
13 | "~~": path.resolve(__dirname, ".."),
14 | },
15 | },
16 | });
17 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // https://v3.nuxtjs.org/concepts/typescript
3 | "extends": "./.nuxt/tsconfig.json"
4 | }
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "homebox",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "docs:dev": "vitepress dev docs",
9 | "docs:build": "vitepress build docs",
10 | "docs:preview": "vitepress preview docs"
11 | },
12 | "keywords": [],
13 | "author": "",
14 | "license": "AGPLv3",
15 | "devDependencies": {
16 | "vitepress": "^1.6.3"
17 | },
18 | "packageManager": "pnpm@9.1.4+sha512.9df9cf27c91715646c7d675d1c9c8e41f6fce88246f1318c1aa6a1ed1aeb3c4f032fcdf4ba63cc69c4fe6d634279176b5358727d8f2cc1e65b65f43ce2f8bfb0"
19 | }
20 |
--------------------------------------------------------------------------------