Compare commits

...

65 Commits

Author SHA1 Message Date
Katos
809b0db5e5 Remove ... another ... redundant function. 2024-07-12 22:21:01 +00:00
Katos
ae8f568bfa Remove redundant reference in repo_items 2024-07-12 22:10:42 +00:00
Katos
87725348be Fix code formatting issue. 2024-07-12 22:04:06 +00:00
Katos
da78f13513 Begin adding Purchase Method to item purchase details 2024-07-12 22:44:29 +01:00
Katos
625730f37c Merge pull request #123 from sysadminsmedia/katos/fix-location-pricing
Update location pricing to include quantity in calculation
2024-07-12 21:38:09 +01:00
Katos
0a72fa95b3 Rectify issues with mismatched types float64 and int 2024-07-12 21:15:07 +01:00
Katos
c39a65ec21 Update location pricing to include quantity in calculation 2024-07-12 20:59:01 +01:00
Ikko Eltociear Ashimine
9403fb27e0 docs: update get-started.md (#106)
* docs: update get-started.md

seperated -> separated

* Apply suggestions from code review

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: Matt Kilgore <tankerkiller125@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2024-07-08 11:50:28 -04:00
Matt Kilgore
ab34791737 fix(docker): fixes the health check wget (#104) 2024-07-08 11:48:46 -04:00
Michael Manganiello
d03e60d580 fix: Use subrouter for API and correctly handle 404 errors (#105)
Currently, the implementation for API v1 routes has the main drawback
that any unknown path gets the fallback `notFoundHandler`, trying to
access filesystem paths.

However, for API routes specifically, we can have a subrouter, and a
default NotFound handler that returns 404.

With this change, requests to `api/v1/<unknown>` now correctly returns
status code 404 instead of 200.
2024-07-08 11:44:55 -04:00
Katos
6fcf9965bb Merge pull request #101 from sysadminsmedia/katosdev-Readme-Screenshots
Update README
2024-07-07 22:38:38 +01:00
Matt Kilgore
d53dcd37e6 Update README.md
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2024-07-07 17:35:55 -04:00
Katos
f3531cacb3 Update README.md 2024-07-07 22:35:19 +01:00
Katos
10cdca94dc Update README
Add screenshots to Readme file
2024-07-07 22:31:37 +01:00
Katos
0b2b7bc4fd Merge pull request #98 from sysadminsmedia/fix/docs-discord
Update Discord link to new
2024-07-07 21:55:52 +01:00
Katos
105f63487b Merge pull request #97 from 101br03k/main
set documentation url to homebox.sysadminmedia.com
2024-07-07 21:52:59 +01:00
Katos
f3c745e42e Update Discord link to new 2024-07-07 21:52:24 +01:00
A3
f3e7d7a19b set documentation url to homebox.sysadminmedia.com 2024-07-07 21:27:29 +02:00
Katos
af1ab9d1af Merge pull request #95 from sysadminsmedia/katos/docker-healthcheck
Update WGET from Busybox to APK
2024-07-07 17:40:53 +01:00
Katos
69e5a877c0 Update WGET from Busybox to APK 2024-07-07 17:18:18 +01:00
Matt Kilgore
5c0d161eb4 fix(ci): remove linting temp, and always build on docker changes (#94)
* fix(ci): remove linting temp, and always build on docker changes

* fix(ci): always re-build on workflow changes
2024-07-07 11:32:22 -04:00
Katos
91cd0d1bca Merge pull request #93 from sysadminsmedia/fix/dockerbuild
Rectify Docker build errors due to broken imports
2024-07-07 16:18:51 +01:00
Katos
b12011f4a6 Rectify Docker build errors due to broken imports 2024-07-07 16:13:02 +01:00
Katos
0223fbbb3f Add docker healthcheck
Add Dockerfile healthcheck endpoint
2024-07-06 14:24:55 +01:00
Katos
a709d38946 Update Dockerfile with Healthcheck endpoint 2024-07-06 14:20:29 +01:00
Katos
bd2eb8b5ac Update Dockerfile.rootless to include explicitly version tags
Tag the version of the base image explicitly on the Dockerfile
2024-07-06 14:20:29 +01:00
Katos
5a015b9581 Add Dockerfile healthcheck 2024-07-06 14:20:29 +01:00
Matt Kilgore
552cb0bf53 chore: General cleanup tasks (#88)
* chore: cleanup

* chore: remove uneeded pull request info

* chore: update security policy

* Update docs/en/quick-start.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update docs/en/quick-start.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update docs/en/quick-start.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2024-07-02 14:03:21 -04:00
dependabot[bot]
a93f4ff1ad chore(deps): bump github.com/gorilla/schema (#87)
Bumps the go_modules group with 1 update in the /backend directory: [github.com/gorilla/schema](https://github.com/gorilla/schema).


Updates `github.com/gorilla/schema` from 1.2.1 to 1.4.1
- [Release notes](https://github.com/gorilla/schema/releases)
- [Commits](https://github.com/gorilla/schema/compare/v1.2.1...v1.4.1)

---
updated-dependencies:
- dependency-name: github.com/gorilla/schema
  dependency-type: direct:production
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-01 19:55:27 -04:00
Matt Kilgore
3d283c2d81 Update README.md (#83) 2024-06-30 16:47:59 -04:00
Katos
ea1d92207a Merge pull request #81 from sysadminsmedia/katos/currency
Update currencies to include more currencies
2024-06-30 16:24:10 +01:00
Katos
bcd826ed4f Merge pull request #70 from zebrapurring/negate-labels
Implement filter option for negated labels
2024-06-30 16:02:04 +01:00
Katos
499bb90b09 Update currencies to include more currencies 2024-06-30 15:50:16 +01:00
Katos
4f00822849 Merge pull request #77 from zebrapurring/fix-tooltip-overflow-top
Move the link tooltip to the top to prevent overflows
2024-06-29 19:53:21 +01:00
ff28175838 Move the link tooltip to the top to prevent overflows 2024-06-29 19:51:35 +01:00
Katos
0f482aebad Merge pull request #42 from sysadminsmedia/katos/location-prices
Add total pricing to Homebox locations
2024-06-29 18:14:01 +01:00
Katos
76fe0d3522 Merge pull request #69 from sysadminsmedia/docs/i18n
feat(docs): i18n support added to docs
2024-06-29 18:12:29 +01:00
Katos
4995e04cf0 Appease the bot overlords. 2024-06-29 18:08:42 +01:00
Katos
76123e00d6 Minor rewording 2024-06-29 18:08:42 +01:00
Katos
0b7de9557d Minor grammar change 2024-06-29 18:08:42 +01:00
Katos
0f25983278 Improve link readability in Markdown 2024-06-29 18:08:42 +01:00
Matt Kilgore
40a093656b chore(docs): remove home link (it's broken) 2024-06-29 18:08:42 +01:00
Matt Kilgore
8b8a96f93b fix(docs): redirect default to english 2024-06-29 18:08:42 +01:00
Matt Kilgore
7b9c3d52cd fix(docs): redirect default to english 2024-06-29 18:08:42 +01:00
Matt Kilgore
a0198fb66f fix(docs): broken link 2024-06-29 18:08:42 +01:00
Matt Kilgore
04eb136ab0 feat(docs): i18n support added to docs 2024-06-29 18:08:42 +01:00
zebrapurring
16f3fb19e8 Merge branch 'main' into negate-labels 2024-06-29 18:05:35 +02:00
Harrison Conlin
6a1ffd7700 Add redirect functionality after login (#76)
Gone is the frustration of scanning a qr code to only be sent to the homepage
2024-06-29 09:02:23 -04:00
Matt Kilgore
24dc182c0e Merge branch 'main' into negate-labels 2024-06-28 11:36:12 -04:00
9ed618d45e Fix quoting for GitHub Action 2024-06-28 17:22:01 +02:00
e79905b608 Implement filtering feature to negate selected labels 2024-06-28 17:21:05 +02:00
e929c38e37 Ignore Go debug binary 2024-06-28 17:21:05 +02:00
e406bb2d04 Fix VSCode debug configuration 2024-06-28 17:21:05 +02:00
Matt Kilgore
30abdd4d36 chore(backend): removed extra unneeded property to pagination 2024-06-22 16:15:22 -04:00
Katos
8a57ca41bf Update error handling
Code review suggestion

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2024-06-22 17:26:39 +01:00
Katos
b57efa02ff Update location to include total pricing 2024-06-22 17:16:32 +01:00
Katos
d7bf64742e Update label to include total pricing 2024-06-22 17:16:32 +01:00
Katos
7e150aa8d7 Update to a 2024-06-22 17:16:32 +01:00
Katos
4d916a69af Update to add total price 2024-06-22 17:16:32 +01:00
Katos
7096616414 Update docs for new location pricing 2024-06-22 17:16:32 +01:00
Katos
2292d72802 Update repo_locations 2024-06-22 17:16:32 +01:00
Katos
a8d21f0465 Add totalprice to pagination 2024-06-22 17:16:32 +01:00
Katos
53d542413b Update swagger to include total location cost 2024-06-22 17:16:32 +01:00
Katos
aaeac8ca9d Add Total Price Calculation to locations 2024-06-22 17:16:32 +01:00
Katos
a780c6fac4 Add Total Price Calculation to ctrl_items 2024-06-22 17:16:32 +01:00
49 changed files with 7510 additions and 5427 deletions

View File

@@ -55,18 +55,4 @@ _(fill-in or delete this section)_
<!--
Describe how you tested this change.
-->
## Release Notes
_(REQUIRED)_
<!--
If this PR makes user facing changes, please describe them here. This
description will be copied into the release notes/changelog, whenever the
next version is released. Keep this section short, and focus on high level
changes.
Put your text between the block. To omit notes, use NONE within the block.
-->
```release-note
```
-->

View File

@@ -5,13 +5,13 @@ on:
tags: [ 'v*.*.*' ]
jobs:
backend-tests:
name: "Backend Server Tests"
uses: sysadminsmedia/homebox/.github/workflows/partial-backend.yaml@main
# backend-tests:
# name: "Backend Server Tests"
# uses: sysadminsmedia/homebox/.github/workflows/partial-backend.yaml@main
frontend-tests:
name: "Frontend and End-to-End Tests"
uses: sysadminsmedia/homebox/.github/workflows/partial-frontend.yaml@main
# frontend-tests:
# name: "Frontend and End-to-End Tests"
# uses: sysadminsmedia/homebox/.github/workflows/partial-frontend.yaml@main
goreleaser:
name: goreleaser

View File

@@ -8,6 +8,10 @@ on:
paths:
- 'backend/**'
- 'frontend/**'
- 'Dockerfile'
- 'Dockerfile.rootless'
- '.dockerignore'
- '.github/workflows'
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
pull_request:
@@ -15,6 +19,10 @@ on:
paths:
- 'backend/**'
- 'frontend/**'
- 'Dockerfile'
- 'Dockerfile.rootless'
- '.dockerignore'
- '.github/workflows'
env:
@@ -84,8 +92,8 @@ jobs:
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
platforms: linux/amd64,linux/arm64,linux/arm/v7
cache-from: type=gha
cache-to: type=gha,mode=max
# cache-from: type=gha
# cache-to: type=gha,mode=max
build-args: |
VERSION=${{ github.ref_name }}
COMMIT=${{ github.sha }}

View File

@@ -8,6 +8,10 @@ on:
paths:
- 'backend/**'
- 'frontend/**'
- 'Dockerfile'
- 'Dockerfile.rootless'
- '.dockerignore'
- '.github/workflows'
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
pull_request:
@@ -15,6 +19,10 @@ on:
paths:
- 'backend/**'
- 'frontend/**'
- 'Dockerfile'
- 'Dockerfile.rootless'
- '.dockerignore'
- '.github/workflows'
env:
# Use docker.io for Docker Hub if empty
@@ -81,8 +89,8 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64,linux/arm/v7
cache-from: type=gha
cache-to: type=gha,mode=max
# cache-from: type=gha
# cache-to: type=gha,mode=max
build-args: |
VERSION=${{ github.ref_name }}
COMMIT=${{ github.sha }}

2
.gitignore vendored
View File

@@ -48,7 +48,7 @@ dist
.pnpm-store
backend/app/api/app
backend/app/api/__debug_bin
backend/app/api/__debug_bin*
dist/
# Nuxt Publish Dir

6
.vscode/launch.json vendored
View File

@@ -25,6 +25,7 @@
"HBOX_STORAGE_DATA": "${workspaceRoot}/backend/.data",
"HBOX_STORAGE_SQLITE_URL": "${workspaceRoot}/backend/.data/homebox.db?_fk=1"
},
"console": "integratedTerminal",
},
{
"name": "Launch Frontend",
@@ -38,10 +39,11 @@
"cwd": "${workspaceFolder}/frontend",
"serverReadyAction": {
"action": "debugWithChrome",
"pattern": "Local: http://localhost:([0-9]+)",
"pattern": "Local: +http://localhost:([0-9]+)",
"uriFormat": "http://localhost:%s",
"webRoot": "${workspaceFolder}/frontend"
}
},
"console": "integratedTerminal",
}
]
}

View File

@@ -1,6 +1,6 @@
# Build Nuxt
FROM node:18-alpine as frontend-builder
FROM node:18-alpine AS frontend-builder
WORKDIR /app
RUN npm install -g pnpm
COPY frontend/package.json frontend/pnpm-lock.yaml ./
@@ -27,6 +27,8 @@ RUN CGO_ENABLED=0 GOOS=linux go build \
-o /go/bin/api \
-v ./app/api/*.go
FROM gcr.io/distroless/java:latest
# Production Stage
FROM alpine:latest
@@ -39,11 +41,17 @@ RUN mkdir /app
COPY --from=builder /go/bin/api /app
RUN chmod +x /app/api
RUN apk add --no-cache wget
LABEL Name=homebox Version=0.0.1
LABEL org.opencontainers.image.source="https://github.com/sysadminsmedia/homebox"
EXPOSE 7745
WORKDIR /app
HEALTHCHECK --interval=30s \
--timeout=5s \
--start-period=5s \
--retries=3 \
CMD [ "/usr/bin/wget", "--no-verbose", "--tries=1", "-O -", "http://localhost:7745/api/v1/status" ]
VOLUME [ "/data" ]
ENTRYPOINT [ "/app/api" ]

View File

@@ -1,6 +1,6 @@
# Build Nuxt
FROM node:18-alpine as frontend-builder
FROM node:18-alpine AS frontend-builder
WORKDIR /app
RUN npm install -g pnpm
COPY frontend/package.json frontend/pnpm-lock.yaml ./
@@ -13,6 +13,7 @@ FROM golang:alpine AS builder
ARG BUILD_TIME
ARG COMMIT
ARG VERSION
ARG BUSYBOX_VERSION=1.36.1-r31
RUN apk update && \
apk upgrade && \
apk add --update git build-base gcc g++
@@ -30,8 +31,10 @@ RUN CGO_ENABLED=0 GOOS=linux go build \
# create a directory so that we can copy it in the next stage
mkdir /data
FROM gcr.io/distroless/java:latest
# Production Stage
FROM gcr.io/distroless/static
FROM gcr.io/distroless/static:latest
ENV HBOX_MODE=production
ENV HBOX_STORAGE_DATA=/data/
@@ -42,9 +45,16 @@ ENV HBOX_STORAGE_SQLITE_URL=/data/homebox.db?_fk=1
COPY --from=builder --chown=nonroot /go/bin/api /app
COPY --from=builder --chown=nonroot /data /data
RUN apk add --no-cache wget
LABEL Name=homebox Version=0.0.1
LABEL org.opencontainers.image.source="https://github.com/sysadminsmedia/homebox"
EXPOSE 7745
HEALTHCHECK --interval=30s \
--timeout=5s \
--start-period=5s \
--retries=3 \
CMD [ "/usr/bin/wget", "--no-verbose", "--tries=1", "-O -", "http://localhost:7745/api/v1/status" ]
VOLUME [ "/data" ]
# Drop root and run as low-privileged user

View File

@@ -16,13 +16,15 @@
Homebox is the inventory and organization system built for the Home User! With a focus on simplicity and ease of use, Homebox is the perfect solution for your home inventory, organization, and management needs. While developing this project, I've tried to keep the following principles in mind:
- _Simple_ - Homebox is designed to be simple and easy to use. No complicated setup or configuration required. Use either a single docker container, or deploy yourself by compiling the binary for your platform of choice.
- _Blazingly Fast_ - Homebox is written in Go, which makes it extremely fast and requires minimal resources to deploy. In general idle memory usage is less than 50MB for the whole container.
- _Blazingly Fast_ - Homebox is written in Go, which makes it extremely fast and requires minimal resources to deploy. In general, idle memory usage is less than 50MB for the whole container.
- _Portable_ - Homebox is designed to be portable and run on anywhere. We use SQLite and an embedded Web UI to make it easy to deploy, use, and backup.
# Screenshots
Check out screenshots of the project [here](https://imgur.com/a/5gLWt2j).
## Quick Start
[Configuration & Docker Compose](https://homebox.sysadminsmedia.com/quick-start.html)
[Configuration & Docker Compose](https://homebox.sysadminsmedia.com/en/quick-start.html)
```bash
# If using the rootless image, ensure data

View File

@@ -6,4 +6,6 @@ Since this software is still considered beta/WIP support is always only given fo
## Reporting a Vulnerability
Please open a normal public issue if you have any security related concerns.
Please open a normal public issue for minor security issues or general security inquires.
For major or critical security issues, please open a private github security issue.

View File

@@ -87,12 +87,6 @@ type (
}
)
func BaseURLFunc(prefix string) func(s string) string {
return func(s string) string {
return prefix + "/v1" + s
}
}
func NewControllerV1(svc *services.AllServices, repos *repo.AllRepos, bus *eventbus.EventBus, options ...func(*V1Controller)) *V1Controller {
ctrl := &V1Controller{
repo: repos,

View File

@@ -4,6 +4,7 @@ import (
"database/sql"
"encoding/csv"
"errors"
"math/big"
"net/http"
"strings"
@@ -57,6 +58,7 @@ func (ctrl *V1Controller) HandleItemsGetAll() errchain.HandlerFunc {
Search: params.Get("q"),
LocationIDs: queryUUIDList(params, "locations"),
LabelIDs: queryUUIDList(params, "labels"),
NegateLabels: queryBool(params.Get("negateLabels")),
ParentItemIDs: queryUUIDList(params, "parentIds"),
IncludeArchived: queryBool(params.Get("includeArchived")),
Fields: filterFieldItems(params["fields"]),
@@ -80,6 +82,14 @@ func (ctrl *V1Controller) HandleItemsGetAll() errchain.HandlerFunc {
ctx := services.NewContext(r.Context())
items, err := ctrl.repo.Items.QueryByGroup(ctx, ctx.GID, extractQuery(r))
totalPrice := new(big.Int)
for _, item := range items.Items {
totalPrice.Add(totalPrice, big.NewInt(int64(item.PurchasePrice*100)))
}
totalPriceFloat := new(big.Float).SetInt(totalPrice)
totalPriceFloat.Quo(totalPriceFloat, big.NewFloat(100))
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return server.JSON(w, http.StatusOK, repo.PaginationResult[repo.ItemSummary]{

View File

@@ -1,6 +1,8 @@
package v1
import (
"context"
"math/big"
"net/http"
"github.com/google/uuid"
@@ -83,6 +85,43 @@ func (ctrl *V1Controller) HandleLocationDelete() errchain.HandlerFunc {
return adapters.CommandID("id", fn, http.StatusNoContent)
}
func (ctrl *V1Controller) GetLocationWithPrice(auth context.Context, GID uuid.UUID, ID uuid.UUID) (repo.LocationOut, error) {
var location, err = ctrl.repo.Locations.GetOneByGroup(auth, GID, ID)
if err != nil {
return repo.LocationOut{}, err
}
// Add direct child items price
totalPrice := new(big.Int)
items, err := ctrl.repo.Items.QueryByGroup(auth, GID, repo.ItemQuery{LocationIDs: []uuid.UUID{ID}})
if err != nil {
return repo.LocationOut{}, err
}
for _, item := range items.Items {
// Convert item.Quantity to float64 for multiplication
quantity := float64(item.Quantity)
itemTotal := big.NewInt(int64(item.PurchasePrice * quantity * 100))
totalPrice.Add(totalPrice, itemTotal)
}
totalPriceFloat := new(big.Float).SetInt(totalPrice)
totalPriceFloat.Quo(totalPriceFloat, big.NewFloat(100))
location.TotalPrice, _ = totalPriceFloat.Float64()
// Add price from child locations
for _, childLocation := range location.Children {
var childLocationWithPrice repo.LocationOut
childLocationWithPrice, err = ctrl.GetLocationWithPrice(auth, GID, childLocation.ID)
if err != nil {
return repo.LocationOut{}, err
}
location.TotalPrice += childLocationWithPrice.TotalPrice
}
return location, nil
}
// HandleLocationGet godoc
//
// @Summary Get Location
@@ -95,7 +134,9 @@ func (ctrl *V1Controller) HandleLocationDelete() errchain.HandlerFunc {
func (ctrl *V1Controller) HandleLocationGet() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID) (repo.LocationOut, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.Locations.GetOneByGroup(auth, auth.GID, ID)
var location, err = ctrl.GetLocationWithPrice(auth, auth.GID, ID)
return location, err
}
return adapters.CommandID("id", fn, http.StatusOK)

View File

@@ -47,8 +47,6 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
// =========================================================================
// API Version 1
v1Base := v1.BaseURLFunc(prefix)
v1Ctrl := v1.NewControllerV1(
a.services,
a.repos,
@@ -58,110 +56,111 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
v1.WithDemoStatus(a.conf.Demo), // Disable Password Change in Demo Mode
)
r.Get(v1Base("/status"), chain.ToHandlerFunc(v1Ctrl.HandleBase(func() bool { return true }, v1.Build{
Version: version,
Commit: commit,
BuildTime: buildTime,
})))
r.Route(prefix+"/v1", func(r chi.Router) {
r.Get("/status", chain.ToHandlerFunc(v1Ctrl.HandleBase(func() bool { return true }, v1.Build{
Version: version,
Commit: commit,
BuildTime: buildTime,
})))
r.Get(v1Base("/currencies"), chain.ToHandlerFunc(v1Ctrl.HandleCurrency()))
r.Get("/currencies", chain.ToHandlerFunc(v1Ctrl.HandleCurrency()))
providers := []v1.AuthProvider{
providers.NewLocalProvider(a.services.User),
}
providers := []v1.AuthProvider{
providers.NewLocalProvider(a.services.User),
}
r.Post(v1Base("/users/register"), chain.ToHandlerFunc(v1Ctrl.HandleUserRegistration()))
r.Post(v1Base("/users/login"), chain.ToHandlerFunc(v1Ctrl.HandleAuthLogin(providers...)))
r.Post("/users/register", chain.ToHandlerFunc(v1Ctrl.HandleUserRegistration()))
r.Post("/users/login", chain.ToHandlerFunc(v1Ctrl.HandleAuthLogin(providers...)))
userMW := []errchain.Middleware{
a.mwAuthToken,
a.mwRoles(RoleModeOr, authroles.RoleUser.String()),
}
userMW := []errchain.Middleware{
a.mwAuthToken,
a.mwRoles(RoleModeOr, authroles.RoleUser.String()),
}
r.Get(v1Base("/ws/events"), chain.ToHandlerFunc(v1Ctrl.HandleCacheWS(), userMW...))
r.Get(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelf(), userMW...))
r.Put(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfUpdate(), userMW...))
r.Delete(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfDelete(), userMW...))
r.Post(v1Base("/users/logout"), chain.ToHandlerFunc(v1Ctrl.HandleAuthLogout(), userMW...))
r.Get(v1Base("/users/refresh"), chain.ToHandlerFunc(v1Ctrl.HandleAuthRefresh(), userMW...))
r.Put(v1Base("/users/self/change-password"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfChangePassword(), userMW...))
r.Get("/ws/events", chain.ToHandlerFunc(v1Ctrl.HandleCacheWS(), userMW...))
r.Get("/users/self", chain.ToHandlerFunc(v1Ctrl.HandleUserSelf(), userMW...))
r.Put("/users/self", chain.ToHandlerFunc(v1Ctrl.HandleUserSelfUpdate(), userMW...))
r.Delete("/users/self", chain.ToHandlerFunc(v1Ctrl.HandleUserSelfDelete(), userMW...))
r.Post("/users/logout", chain.ToHandlerFunc(v1Ctrl.HandleAuthLogout(), userMW...))
r.Get("/users/refresh", chain.ToHandlerFunc(v1Ctrl.HandleAuthRefresh(), userMW...))
r.Put("/users/self/change-password", chain.ToHandlerFunc(v1Ctrl.HandleUserSelfChangePassword(), userMW...))
r.Post(v1Base("/groups/invitations"), chain.ToHandlerFunc(v1Ctrl.HandleGroupInvitationsCreate(), userMW...))
r.Get(v1Base("/groups/statistics"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatistics(), userMW...))
r.Get(v1Base("/groups/statistics/purchase-price"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsPriceOverTime(), userMW...))
r.Get(v1Base("/groups/statistics/locations"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsLocations(), userMW...))
r.Get(v1Base("/groups/statistics/labels"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsLabels(), userMW...))
r.Post("/groups/invitations", chain.ToHandlerFunc(v1Ctrl.HandleGroupInvitationsCreate(), userMW...))
r.Get("/groups/statistics", chain.ToHandlerFunc(v1Ctrl.HandleGroupStatistics(), userMW...))
r.Get("/groups/statistics/purchase-price", chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsPriceOverTime(), userMW...))
r.Get("/groups/statistics/locations", chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsLocations(), userMW...))
r.Get("/groups/statistics/labels", chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsLabels(), userMW...))
// TODO: I don't like /groups being the URL for users
r.Get(v1Base("/groups"), chain.ToHandlerFunc(v1Ctrl.HandleGroupGet(), userMW...))
r.Put(v1Base("/groups"), chain.ToHandlerFunc(v1Ctrl.HandleGroupUpdate(), userMW...))
// TODO: I don't like /groups being the URL for users
r.Get("/groups", chain.ToHandlerFunc(v1Ctrl.HandleGroupGet(), userMW...))
r.Put("/groups", chain.ToHandlerFunc(v1Ctrl.HandleGroupUpdate(), userMW...))
r.Post(v1Base("/actions/ensure-asset-ids"), chain.ToHandlerFunc(v1Ctrl.HandleEnsureAssetID(), userMW...))
r.Post(v1Base("/actions/zero-item-time-fields"), chain.ToHandlerFunc(v1Ctrl.HandleItemDateZeroOut(), userMW...))
r.Post(v1Base("/actions/ensure-import-refs"), chain.ToHandlerFunc(v1Ctrl.HandleEnsureImportRefs(), userMW...))
r.Post(v1Base("/actions/set-primary-photos"), chain.ToHandlerFunc(v1Ctrl.HandleSetPrimaryPhotos(), userMW...))
r.Post("/actions/ensure-asset-ids", chain.ToHandlerFunc(v1Ctrl.HandleEnsureAssetID(), userMW...))
r.Post("/actions/zero-item-time-fields", chain.ToHandlerFunc(v1Ctrl.HandleItemDateZeroOut(), userMW...))
r.Post("/actions/ensure-import-refs", chain.ToHandlerFunc(v1Ctrl.HandleEnsureImportRefs(), userMW...))
r.Post("/actions/set-primary-photos", chain.ToHandlerFunc(v1Ctrl.HandleSetPrimaryPhotos(), userMW...))
r.Get(v1Base("/locations"), chain.ToHandlerFunc(v1Ctrl.HandleLocationGetAll(), userMW...))
r.Post(v1Base("/locations"), chain.ToHandlerFunc(v1Ctrl.HandleLocationCreate(), userMW...))
r.Get(v1Base("/locations/tree"), chain.ToHandlerFunc(v1Ctrl.HandleLocationTreeQuery(), userMW...))
r.Get(v1Base("/locations/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLocationGet(), userMW...))
r.Put(v1Base("/locations/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLocationUpdate(), userMW...))
r.Delete(v1Base("/locations/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLocationDelete(), userMW...))
r.Get("/locations", chain.ToHandlerFunc(v1Ctrl.HandleLocationGetAll(), userMW...))
r.Post("/locations", chain.ToHandlerFunc(v1Ctrl.HandleLocationCreate(), userMW...))
r.Get("/locations/tree", chain.ToHandlerFunc(v1Ctrl.HandleLocationTreeQuery(), userMW...))
r.Get("/locations/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLocationGet(), userMW...))
r.Put("/locations/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLocationUpdate(), userMW...))
r.Delete("/locations/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLocationDelete(), userMW...))
r.Get(v1Base("/labels"), chain.ToHandlerFunc(v1Ctrl.HandleLabelsGetAll(), userMW...))
r.Post(v1Base("/labels"), chain.ToHandlerFunc(v1Ctrl.HandleLabelsCreate(), userMW...))
r.Get(v1Base("/labels/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLabelGet(), userMW...))
r.Put(v1Base("/labels/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLabelUpdate(), userMW...))
r.Delete(v1Base("/labels/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLabelDelete(), userMW...))
r.Get("/labels", chain.ToHandlerFunc(v1Ctrl.HandleLabelsGetAll(), userMW...))
r.Post("/labels", chain.ToHandlerFunc(v1Ctrl.HandleLabelsCreate(), userMW...))
r.Get("/labels/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLabelGet(), userMW...))
r.Put("/labels/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLabelUpdate(), userMW...))
r.Delete("/labels/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLabelDelete(), userMW...))
r.Get(v1Base("/items"), chain.ToHandlerFunc(v1Ctrl.HandleItemsGetAll(), userMW...))
r.Post(v1Base("/items"), chain.ToHandlerFunc(v1Ctrl.HandleItemsCreate(), userMW...))
r.Post(v1Base("/items/import"), chain.ToHandlerFunc(v1Ctrl.HandleItemsImport(), userMW...))
r.Get(v1Base("/items/export"), chain.ToHandlerFunc(v1Ctrl.HandleItemsExport(), userMW...))
r.Get(v1Base("/items/fields"), chain.ToHandlerFunc(v1Ctrl.HandleGetAllCustomFieldNames(), userMW...))
r.Get(v1Base("/items/fields/values"), chain.ToHandlerFunc(v1Ctrl.HandleGetAllCustomFieldValues(), userMW...))
r.Get("/items", chain.ToHandlerFunc(v1Ctrl.HandleItemsGetAll(), userMW...))
r.Post("/items", chain.ToHandlerFunc(v1Ctrl.HandleItemsCreate(), userMW...))
r.Post("/items/import", chain.ToHandlerFunc(v1Ctrl.HandleItemsImport(), userMW...))
r.Get("/items/export", chain.ToHandlerFunc(v1Ctrl.HandleItemsExport(), userMW...))
r.Get("/items/fields", chain.ToHandlerFunc(v1Ctrl.HandleGetAllCustomFieldNames(), userMW...))
r.Get("/items/fields/values", chain.ToHandlerFunc(v1Ctrl.HandleGetAllCustomFieldValues(), userMW...))
r.Get(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemGet(), userMW...))
r.Get(v1Base("/items/{id}/path"), chain.ToHandlerFunc(v1Ctrl.HandleItemFullPath(), userMW...))
r.Put(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemUpdate(), userMW...))
r.Patch(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemPatch(), userMW...))
r.Delete(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemDelete(), userMW...))
r.Get("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemGet(), userMW...))
r.Get("/items/{id}/path", chain.ToHandlerFunc(v1Ctrl.HandleItemFullPath(), userMW...))
r.Put("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemUpdate(), userMW...))
r.Patch("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemPatch(), userMW...))
r.Delete("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemDelete(), userMW...))
r.Post(v1Base("/items/{id}/attachments"), chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentCreate(), userMW...))
r.Put(v1Base("/items/{id}/attachments/{attachment_id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentUpdate(), userMW...))
r.Delete(v1Base("/items/{id}/attachments/{attachment_id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentDelete(), userMW...))
r.Post("/items/{id}/attachments", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentCreate(), userMW...))
r.Put("/items/{id}/attachments/{attachment_id}", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentUpdate(), userMW...))
r.Delete("/items/{id}/attachments/{attachment_id}", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentDelete(), userMW...))
r.Get(v1Base("/items/{id}/maintenance"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceLogGet(), userMW...))
r.Post(v1Base("/items/{id}/maintenance"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryCreate(), userMW...))
r.Put(v1Base("/items/{id}/maintenance/{entry_id}"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryUpdate(), userMW...))
r.Delete(v1Base("/items/{id}/maintenance/{entry_id}"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryDelete(), userMW...))
r.Get("/items/{id}/maintenance", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceLogGet(), userMW...))
r.Post("/items/{id}/maintenance", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryCreate(), userMW...))
r.Put("/items/{id}/maintenance/{entry_id}", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryUpdate(), userMW...))
r.Delete("/items/{id}/maintenance/{entry_id}", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryDelete(), userMW...))
r.Get(v1Base("/assets/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleAssetGet(), userMW...))
r.Get("/assets/{id}", chain.ToHandlerFunc(v1Ctrl.HandleAssetGet(), userMW...))
// Notifiers
r.Get(v1Base("/notifiers"), chain.ToHandlerFunc(v1Ctrl.HandleGetUserNotifiers(), userMW...))
r.Post(v1Base("/notifiers"), chain.ToHandlerFunc(v1Ctrl.HandleCreateNotifier(), userMW...))
r.Put(v1Base("/notifiers/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleUpdateNotifier(), userMW...))
r.Delete(v1Base("/notifiers/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleDeleteNotifier(), userMW...))
r.Post(v1Base("/notifiers/test"), chain.ToHandlerFunc(v1Ctrl.HandlerNotifierTest(), userMW...))
// Notifiers
r.Get("/notifiers", chain.ToHandlerFunc(v1Ctrl.HandleGetUserNotifiers(), userMW...))
r.Post("/notifiers", chain.ToHandlerFunc(v1Ctrl.HandleCreateNotifier(), userMW...))
r.Put("/notifiers/{id}", chain.ToHandlerFunc(v1Ctrl.HandleUpdateNotifier(), userMW...))
r.Delete("/notifiers/{id}", chain.ToHandlerFunc(v1Ctrl.HandleDeleteNotifier(), userMW...))
r.Post("/notifiers/test", chain.ToHandlerFunc(v1Ctrl.HandlerNotifierTest(), userMW...))
// Asset-Like endpoints
assetMW := []errchain.Middleware{
a.mwAuthToken,
a.mwRoles(RoleModeOr, authroles.RoleUser.String(), authroles.RoleAttachments.String()),
}
// Asset-Like endpoints
assetMW := []errchain.Middleware{
a.mwAuthToken,
a.mwRoles(RoleModeOr, authroles.RoleUser.String(), authroles.RoleAttachments.String()),
}
r.Get(
v1Base("/qrcode"),
chain.ToHandlerFunc(v1Ctrl.HandleGenerateQRCode(), assetMW...),
)
r.Get(
v1Base("/items/{id}/attachments/{attachment_id}"),
chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentGet(), assetMW...),
)
r.Get("/qrcode", chain.ToHandlerFunc(v1Ctrl.HandleGenerateQRCode(), assetMW...))
r.Get(
"/items/{id}/attachments/{attachment_id}",
chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentGet(), assetMW...),
)
// Reporting Services
r.Get(v1Base("/reporting/bill-of-materials"), chain.ToHandlerFunc(v1Ctrl.HandleBillOfMaterialsExport(), userMW...))
// Reporting Services
r.Get("/reporting/bill-of-materials", chain.ToHandlerFunc(v1Ctrl.HandleBillOfMaterialsExport(), userMW...))
r.NotFound(http.NotFound)
})
r.NotFound(chain.ToHandlerFunc(notFoundHandler()))
}

View File

@@ -2145,6 +2145,9 @@
"purchaseFrom": {
"type": "string"
},
"purchaseFrom": {
"type": "string"
},
"purchasePrice": {
"type": "string",
"example": "0"
@@ -2469,6 +2472,9 @@
"parent": {
"$ref": "#/definitions/repo.LocationSummary"
},
"totalPrice": {
"type": "number"
},
"updatedAt": {
"type": "string"
}
@@ -2707,6 +2713,9 @@
},
"total": {
"type": "integer"
},
"totalPrice": {
"type": "number"
}
}
},
@@ -2989,4 +2998,4 @@
"in": "header"
}
}
}
}

View File

@@ -170,6 +170,8 @@ definitions:
x-omitempty: true
purchaseFrom:
type: string
purchaseMethod:
type: string
purchasePrice:
example: "0"
type: string

View File

@@ -13,7 +13,7 @@ require (
github.com/go-playground/validator/v10 v10.18.0
github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a
github.com/google/uuid v1.6.0
github.com/gorilla/schema v1.2.1
github.com/gorilla/schema v1.4.1
github.com/hay-kot/httpkit v0.0.9
github.com/mattn/go-sqlite3 v1.14.22
github.com/olahol/melody v1.1.4

View File

@@ -74,8 +74,8 @@ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbu
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/schema v1.2.1 h1:tjDxcmdb+siIqkTNoV+qRH2mjYdr2hHe5MKXbp61ziM=
github.com/gorilla/schema v1.2.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=

View File

@@ -17,6 +17,18 @@
"symbol": "؋",
"name": "Afghan Afghani"
},
{
"code": "XCD",
"local": "Eastern Carribean",
"symbol": "$",
"name": "Eastern Carribean Dollar"
},
{
"code": "XOF",
"local": "CFA Franc",
"symbol": "CFA",
"name": "CFA Franc"
},
{
"code": "ALL",
"local": "Albania",
@@ -167,12 +179,24 @@
"symbol": "FC",
"name": "Congolese Franc"
},
{
"code": "XAF",
"local": "CFA",
"symbol": "FCFA",
"name": "CFA Franc BEAC"
},
{
"code": "CHF",
"local": "Switzerland",
"symbol": "CHF",
"name": "Swiss Franc"
},
{
"code": "NZD",
"local": "New Zealand",
"symbol": "NZ$",
"name": "New Zealand Dollar"
},
{
"code": "CLP",
"local": "Chile",

View File

@@ -202,9 +202,10 @@ func (s *IOSheet) ReadItems(ctx context.Context, items []repo.ItemOut, GID uuid.
Insured: item.Insured,
Archived: item.Archived,
PurchasePrice: item.PurchasePrice,
PurchaseFrom: item.PurchaseFrom,
PurchaseTime: item.PurchaseTime,
PurchasePrice: item.PurchasePrice,
PurchaseMethod: item.PurchaseMethod,
PurchaseFrom: item.PurchaseFrom,
PurchaseTime: item.PurchaseTime,
Manufacturer: item.Manufacturer,
ModelNumber: item.ModelNumber,

View File

@@ -298,6 +298,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data io.Re
Archived: row.Archived,
PurchasePrice: row.PurchasePrice,
PurchaseFrom: row.PurchaseMethod,
PurchaseFrom: row.PurchaseFrom,
PurchaseTime: row.PurchaseTime,

View File

@@ -76,6 +76,8 @@ func (Item) Fields() []ent.Field {
// ------------------------------------
// item purchase
field.String("purchase_method").
Optional(),
field.Time("purchase_time").
Optional(),
field.String("purchase_from").

View File

@@ -17,7 +17,7 @@ CREATE INDEX `documenttoken_token` ON `document_tokens` (`token`);
-- create "groups" table
CREATE TABLE `groups` (`id` uuid NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `name` text NOT NULL, `currency` text NOT NULL DEFAULT 'usd', PRIMARY KEY (`id`));
-- create "items" table
CREATE TABLE `items` (`id` uuid NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `name` text NOT NULL, `description` text NULL, `import_ref` text NULL, `notes` text NULL, `quantity` integer NOT NULL DEFAULT 1, `insured` bool NOT NULL DEFAULT false, `serial_number` text NULL, `model_number` text NULL, `manufacturer` text NULL, `lifetime_warranty` bool NOT NULL DEFAULT false, `warranty_expires` datetime NULL, `warranty_details` text NULL, `purchase_time` datetime NULL, `purchase_from` text NULL, `purchase_price` real NOT NULL DEFAULT 0, `sold_time` datetime NULL, `sold_to` text NULL, `sold_price` real NOT NULL DEFAULT 0, `sold_notes` text NULL, `group_items` uuid NOT NULL, `location_items` uuid NULL, PRIMARY KEY (`id`), CONSTRAINT `items_groups_items` FOREIGN KEY (`group_items`) REFERENCES `groups` (`id`) ON DELETE CASCADE, CONSTRAINT `items_locations_items` FOREIGN KEY (`location_items`) REFERENCES `locations` (`id`) ON DELETE CASCADE);
CREATE TABLE `items` (`id` uuid NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `name` text NOT NULL, `description` text NULL, `import_ref` text NULL, `notes` text NULL, `quantity` integer NOT NULL DEFAULT 1, `insured` bool NOT NULL DEFAULT false, `serial_number` text NULL, `model_number` text NULL, `manufacturer` text NULL, `lifetime_warranty` bool NOT NULL DEFAULT false, `warranty_expires` datetime NULL, `warranty_details` text NULL, `purchase_method` text NULL, `purchase_time` datetime NULL, `purchase_from` text NULL, `purchase_price` real NOT NULL DEFAULT 0, `sold_time` datetime NULL, `sold_to` text NULL, `sold_price` real NOT NULL DEFAULT 0, `sold_notes` text NULL, `group_items` uuid NOT NULL, `location_items` uuid NULL, PRIMARY KEY (`id`), CONSTRAINT `items_groups_items` FOREIGN KEY (`group_items`) REFERENCES `groups` (`id`) ON DELETE CASCADE, CONSTRAINT `items_locations_items` FOREIGN KEY (`location_items`) REFERENCES `locations` (`id`) ON DELETE CASCADE);
-- create index "item_name" to table: "items"
CREATE INDEX `item_name` ON `items` (`name`);
-- create index "item_manufacturer" to table: "items"

View File

@@ -36,6 +36,7 @@ type (
AssetID AssetID `json:"assetId"`
LocationIDs []uuid.UUID `json:"locationIds"`
LabelIDs []uuid.UUID `json:"labelIds"`
NegateLabels bool `json:"negateLabels"`
ParentItemIDs []uuid.UUID `json:"parentIds"`
SortBy string `json:"sortBy"`
IncludeArchived bool `json:"includeArchived"`
@@ -90,9 +91,10 @@ type (
WarrantyDetails string `json:"warrantyDetails"`
// Purchase
PurchaseTime types.Date `json:"purchaseTime"`
PurchaseFrom string `json:"purchaseFrom"`
PurchasePrice float64 `json:"purchasePrice,string"`
PurchaseMethod string `json:"purchaseMethod"`
PurchaseTime types.Date `json:"purchaseTime"`
PurchaseFrom string `json:"purchaseFrom"`
PurchasePrice float64 `json:"purchasePrice,string"`
// Sold
SoldTime types.Date `json:"soldTime"`
@@ -146,8 +148,9 @@ type (
WarrantyDetails string `json:"warrantyDetails"`
// Purchase
PurchaseTime types.Date `json:"purchaseTime"`
PurchaseFrom string `json:"purchaseFrom"`
PurchaseMethod string `json:"purchaseMethod"`
PurchaseTime types.Date `json:"purchaseTime"`
PurchaseFrom string `json:"purchaseFrom"`
// Sold
SoldTime types.Date `json:"soldTime"`
@@ -260,8 +263,8 @@ func mapItemOut(item *ent.Item) ItemOut {
Manufacturer: item.Manufacturer,
// Purchase
PurchaseTime: types.DateFromTime(item.PurchaseTime),
PurchaseFrom: item.PurchaseFrom,
PurchaseTime: types.DateFromTime(item.PurchaseTime),
PurchaseFrom: item.PurchaseFrom,
// Sold
SoldTime: types.DateFromTime(item.SoldTime),
@@ -365,10 +368,17 @@ func (e *ItemsRepository) QueryByGroup(ctx context.Context, gid uuid.UUID, q Ite
if len(q.LabelIDs) > 0 {
labelPredicates := make([]predicate.Item, 0, len(q.LabelIDs))
for _, l := range q.LabelIDs {
labelPredicates = append(labelPredicates, item.HasLabelWith(label.ID(l)))
if !q.NegateLabels {
labelPredicates = append(labelPredicates, item.HasLabelWith(label.ID(l)))
} else {
labelPredicates = append(labelPredicates, item.Not(item.HasLabelWith(label.ID(l))))
}
}
if !q.NegateLabels {
andPredicates = append(andPredicates, item.Or(labelPredicates...))
} else {
andPredicates = append(andPredicates, item.And(labelPredicates...))
}
andPredicates = append(andPredicates, item.Or(labelPredicates...))
}
if len(q.LocationIDs) > 0 {

View File

@@ -236,6 +236,7 @@ func TestItemsRepository_Update(t *testing.T) {
ModelNumber: fk.Str(10),
Manufacturer: fk.Str(10),
PurchaseTime: types.DateFromTime(time.Now()),
PurchaseMethod: fk.Str(10),
PurchaseFrom: fk.Str(10),
PurchasePrice: 300.99,
SoldTime: types.DateFromTime(time.Now()),
@@ -262,6 +263,7 @@ func TestItemsRepository_Update(t *testing.T) {
assert.Equal(t, updateData.Manufacturer, got.Manufacturer)
// assert.Equal(t, updateData.PurchaseTime, got.PurchaseTime)
assert.Equal(t, updateData.PurchaseFrom, got.PurchaseFrom)
assert.Equal(t, updateData.PurchaseMethod, got.PurchaseMethod)
assert.InDelta(t, updateData.PurchasePrice, got.PurchasePrice, 0.01)
// assert.Equal(t, updateData.SoldTime, got.SoldTime)
assert.Equal(t, updateData.SoldTo, got.SoldTo)

View File

@@ -49,6 +49,7 @@ type (
Parent *LocationSummary `json:"parent,omitempty"`
LocationSummary
Children []LocationSummary `json:"children"`
TotalPrice float64 `json:"totalPrice"`
}
)

View File

@@ -8,6 +8,14 @@ export default defineConfig({
sitemap: {
hostname: 'https://homebox.sysadminsmedia.com',
},
locales: {
en: {
label: 'English',
lang: 'en',
}
},
themeConfig: {
logo: '/lilbox.svg',
@@ -19,26 +27,33 @@ export default defineConfig({
},
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: 'Home', link: '/' },
{ text: 'API', link: 'https://redocly.github.io/redoc/?url=https://raw.githubusercontent.com/sysadminsmedia/homebox/main/docs/docs/api/openapi-2.0.json' }
],
sidebar: [
{
text: 'Getting Started',
items: [
{ text: 'Quick Start', link: '/quick-start' },
{ text: 'Tips and Tricks', link: '/tips-tricks' }
]
},
{
text: 'Advanced',
items: [
{ text: 'Import CSV', link: '/import-csv' },
{ text: 'Build from Source', link: '/build' }
]
},
],
sidebar: {
'/en/': [
{
text: 'Getting Started',
items: [
{ text: 'Quick Start', link: '/en/quick-start' },
{ text: 'Tips and Tricks', link: '/en/tips-tricks' }
]
},
{
text: 'Advanced',
items: [
{ text: 'Import CSV', link: '/en/import-csv' },
]
},
{
text: 'Contributing',
items: [
{ text: 'Get Started', link: '/en/contribute/get-started' },
{ text: 'Bounty Program', link: '/en/contribute/bounty' }
]
}
]
},
socialLinks: [
{ icon: 'discord', link: 'https://discord.gg/aY4DCkpNA9' },

View File

@@ -1,15 +0,0 @@
# Building The Binary
This document describes how to build the project from source code.
## Prerequisites
TODO
## Building
TODO
## Running
TODO

View File

@@ -1683,14 +1683,12 @@
"parameters": [
{
"type": "string",
"example": "admin@admin.com",
"description": "string",
"name": "username",
"in": "formData"
},
{
"type": "string",
"example": "admin",
"description": "string",
"name": "password",
"in": "formData"
@@ -2142,6 +2140,9 @@
"x-nullable": true,
"x-omitempty": true
},
"purchaseMethod": {
"type": "string"
},
"purchaseFrom": {
"type": "string"
},
@@ -2469,6 +2470,9 @@
"parent": {
"$ref": "#/definitions/repo.LocationSummary"
},
"totalPrice": {
"type": "number"
},
"updatedAt": {
"type": "string"
}
@@ -2707,6 +2711,9 @@
},
"total": {
"type": "integer"
},
"totalPrice": {
"type": "number"
}
}
},
@@ -2989,4 +2996,4 @@
"in": "header"
}
}
}
}

View File

@@ -0,0 +1,19 @@
# Bounty Program
## About
As part of our commitment to open source, and building an active community around Homebox (and hopefully active pool of developers), we are enabling bounties on issues.
After digging through several platforms, we ended up settling on [boss.dev](https://www.boss.dev/) as it has some of the lowest fees we could possibly find for any of these platforms other than spinning one up ourselves (which we currently aren't in a position to do).
While it's not the perfect solution, we think it's about the best one we could find at the moment to lower the rates as much as possible to make sure everyone get's the highest payouts possible. (Some we found were as high as a combined 16%!!!)
We hope that by enabling bounties on issues, people who have the means and want certain features implemented quicker can now sponsor issues, and in turn everyone contributing code can potentially earn some money for their hard work.
## Contributor
As a contributor wanting to accept money from bounties all you need to do is simply register for an account via GitHub, and attach a bank account (or debit card in the USA).
## Sponsor
Sign in with a GitHub account, and then attach a credit card to your account.
## Commands to use boss.dev
There is documentation on their website regarding commands that you can put in comments to use the bounty system. [boss.dev Documentation](https://www.boss.dev/doc)

View File

@@ -0,0 +1,70 @@
# Getting Started With Contributing
## Get Started
### Prerequisites
There is a devcontainer available for this project. If you are using VSCode, you can use the devcontainer to get started. If you are not using VSCode, you need to ensure that you have the following tools installed:
- [Go 1.19+](https://golang.org/doc/install)
- [Swaggo](https://github.com/swaggo/swag)
- [Node.js 16+](https://nodejs.org/en/download/)
- [pnpm](https://pnpm.io/installation)
- [Taskfile](https://taskfile.dev/#/installation) (Optional but recommended)
- For code generation, you'll need to have `python3` available on your path. In most cases, this is already installed and available.
If you're using `taskfile` you can run `task --list-all` for a list of all commands and their descriptions.
### Setup
If you're using the taskfile, you can use the `task setup` command to run the required setup commands. Otherwise, you can review the commands required in the `Taskfile.yml` file.
Note that when installing dependencies with pnpm, you must use the `--shamefully-hoist` flag. If you don't use this flag, you will get an error when running the frontend server.
### API Development Notes
start command `task go:run`
1. API Server does not auto reload. You'll need to restart the server after making changes.
2. Unit tests should be written in Go, however, end-to-end or user story tests should be written in TypeScript using the client library in the frontend directory.
test command `task go:test`
lint command `task go:lint`
swagger update command `task swag`
### Frontend Development Notes
start command `task: ui:dev`
1. The frontend is a Vue 3 app with Nuxt.js that uses Tailwind and DaisyUI for styling.
2. We're using Vitest for our automated testing. You can run these with `task ui:watch`.
3. Tests require the API server to be running, and in some cases the first run will fail due to a race condition. If this happens, just run the tests again and they should pass.
fix/lint code `task ui:fix`
type checking `task ui:check`
## Documentation
We use [Vitepress](https://vitepress.dev/) for the web documentation of homebox. Anyone is welcome to contribute the documentation if they wish.
Anyone is welcome to contribute the documentation if they wish. For documentation contributions, you only need Node.js and PNPM.
::: info Notes
- Languages are separated by folder (e.g `/en`, `/fr`, etc.)
- The Sidebar must be updated on a per language basis
+ The Sidebar must be updated on a per-language basis
- Each languages files can be named independently (slugs can match the language)
- The `public/_redirects` file is used to redirect the default to english
- Redirects can also be configured per language by adding `Language=` after the redirect code
:::
## Branch Flow
We use the `main` branch as the development branch. All PRs should be made to the `main` branch form a feature branch.
To create a pull request you can use the following steps:
1. Fork the repo and create a new branch from `main`
2. If you added code that should be tested, add tests
3. If you've changed APIs update the documentation
4. Ensure that the test suite and linters pass
5. Create your PR

View File

@@ -63,6 +63,7 @@ Specifying import refs also allows you to update existing items via the CSV impo
| HB.model_number | String | Model of the item |
| HB.manufacturer | String | Manufacturer of the item |
| HB.notes | String (1000) | General notes about the product |
| HB.purchase_method | String | Method of how the item was purchased |
| HB.purchase_from | String | Name of the place the item was purchased from |
| HB.purchase_price | Float64 | |
| HB.purchase_time | Date | Date the item was purchased |

View File

@@ -11,10 +11,10 @@ hero:
actions:
- theme: brand
text: Quick Start
link: /quick-start
link: /en/quick-start
- theme: alt
text: Tips and Tricks
link: /tips-tricks
link: /en/tips-tricks
features:
- title: Add/Update/Delete Items

View File

@@ -46,11 +46,14 @@ volumes:
homebox-data:
driver: local
```
::: info
If you use the `rootless` image, and instead of using named volumes you would prefer using a hostMount directly (e.g., `volumes: [ /path/to/data/folder:/data ]`) you need to `chown` the chosen directory in advance to the `65532` user (as shown in the Docker example above).
:::
::: warning
If you have previously set up docker compose with the `HBOX_WEB_READ_TIMEOUT`, `HBOX_WEB_WRITE_TIMEOUT`, or `HBOX_IDLE_TIMEOUT` options, and you were previously using the hay-kot image, please note that you will have to add an `s` for seconds or `m` for minutes to the end of the integers. A dependency update removed the defaultation to seconds and it now requires an explicit duration time.
:::
## Env Variables & Configuration
| Variable | Default | Description |
@@ -59,15 +62,15 @@ If you use the `rootless` image, and instead of using named volumes you would pr
| HBOX_WEB_PORT | 7745 | port to run the web server on, if you're using docker do not change this |
| HBOX_WEB_HOST | | host to run the web server on, if you're using docker do not change this |
| HBOX_OPTIONS_ALLOW_REGISTRATION | true | allow users to register themselves |
| HBOX_OPTIONS_AUTO_INCREMENT_ASSET_ID | true | auto increments the asset_id field for new items |
| HBOX_OPTIONS_AUTO_INCREMENT_ASSET_ID | true | auto-increments the asset_id field for new items |
| HBOX_OPTIONS_CURRENCY_CONFIG | | json configuration file containing additional currencie |
| HBOX_WEB_MAX_UPLOAD_SIZE | 10 | maximum file upload size supported in MB |
| HBOX_WEB_READ_TIMEOUT | 10 | Read timeout of HTTP sever |
| HBOX_WEB_WRITE_TIMEOUT | 10 | Write timeout of HTTP server |
| HBOX_WEB_IDLE_TIMEOUT | 30 | Idle timeout of HTTP server |
| HBOX_WEB_READ_TIMEOUT | 10s | Read timeout of HTTP sever |
| HBOX_WEB_WRITE_TIMEOUT | 10s | Write timeout of HTTP server |
| HBOX_WEB_IDLE_TIMEOUT | 30s | Idle timeout of HTTP server |
| HBOX_STORAGE_DATA | /data/ | path to the data directory, do not change this if you're using docker |
| HBOX_STORAGE_SQLITE_URL | /data/homebox.db?_fk=1 | sqlite database url, if you're using docker do not change this |
| HBOX_LOG_LEVEL | info | log level to use, can be one of: trace, debug, info, warn, error, critical |
| HBOX_LOG_LEVEL | info | log level to use, can be one of trace, debug, info, warn, error, critical |
| HBOX_LOG_FORMAT | text | log format to use, can be one of: text, json |
| HBOX_MAILER_HOST | | email host to use, if not set no email provider will be used |
| HBOX_MAILER_PORT | 587 | email port to use |

View File

@@ -17,7 +17,7 @@ Homebox Custom Fields also have special support for URLs. Provide a URL (`https:
## Managing Asset IDs
Homebox provides the option to auto-set asset IDs, this is the default behavior. These can be used for tracking assets with printable tags or labels. You can disable this behavior via a command line flag or ENV variable. See [configuration](/quick-start#env-variables-configuration) for more details.
Homebox provides the option to auto-set asset IDs, this is the default behavior. These can be used for tracking assets with printable tags or labels. You can disable this behavior via a command line flag or ENV variable. See [configuration](/en/quick-start.md#env-variables-configuration) for more details.
Example ID: `000-001`

4
docs/public/_redirects Normal file
View File

@@ -0,0 +1,4 @@
/ /en/ 302
# This is an example for a french redirect
# /* /fr/:splat 302 Language=fr

View File

@@ -1,44 +0,0 @@
# fly.toml file generated for homebox on 2022-09-08T16:00:08-08:00
app = "homebox"
kill_signal = "SIGINT"
kill_timeout = 5
processes = []
[build.args]
COMMIT = "HEAD"
VERSION = "nightly"
[env]
PORT = "7745"
HBOX_DEMO = "true"
[experimental]
allowed_public_ports = []
auto_rollback = true
[[services]]
http_checks = []
internal_port = 7745
processes = ["app"]
protocol = "tcp"
script_checks = []
[services.concurrency]
hard_limit = 25
soft_limit = 20
type = "connections"
[[services.ports]]
force_https = true
handlers = ["http"]
port = 80
[[services.ports]]
handlers = ["tls", "http"]
port = 443
[[services.tcp_checks]]
grace_period = "1s"
interval = "15s"
restart_limit = 0
timeout = "2s"

View File

@@ -14,7 +14,7 @@
/>
<Currency v-else-if="detail.type == 'currency'" :amount="detail.text" />
<template v-else-if="detail.type === 'link'">
<div class="tooltip tooltip-primary tooltip-right" :data-tip="detail.href">
<div class="tooltip tooltip-primary tooltip-top" :data-tip="detail.href">
<a class="btn btn-primary btn-xs" :href="detail.href" target="_blank">
<MdiOpenInNew class="mr-2 swap-on" />
{{ detail.text }}

View File

@@ -16,6 +16,7 @@ type ImportObj = {
[`HB.manufacturer`]: string;
[`HB.notes`]: string;
[`HB.purchase_price`]: number;
[`HB.purchase_method`]: string;
[`HB.purchase_from`]: string;
[`HB.purchase_time`]: string;
[`HB.lifetime_warranty`]: boolean;
@@ -62,6 +63,7 @@ function importFileGenerator(entries: number): ImportObj[] {
[`HB.manufacturer`]: faker.string.alphanumeric(5),
[`HB.notes`]: "",
[`HB.purchase_from`]: faker.person.fullName(),
[`HB.purchase_method`]: faker.string.alphanumeric(5),
[`HB.purchase_price`]: faker.number.int(100),
[`HB.purchase_time`]: faker.date.past().toDateString(),
[`HB.lifetime_warranty`]: half > i,

View File

@@ -23,6 +23,7 @@ export type ItemsQuery = {
pageSize?: number;
locations?: string[];
labels?: string[];
negateLabels?: boolean;
parentIds?: string[];
q?: string;
fields?: string[];

View File

@@ -232,6 +232,7 @@ export interface LocationOut {
id: string;
name: string;
parent: LocationSummary;
totalPrice: number;
updatedAt: Date | string;
}
@@ -329,6 +330,7 @@ export interface PaginationResultItemSummary {
page: number;
pageSize: number;
total: number;
totalPrice: number;
}
export interface TotalsByOrganizer {

View File

@@ -1,10 +1,12 @@
export default defineNuxtRouteMiddleware(async () => {
const ctx = useAuthContext();
const api = useUserApi();
const redirectTo = useState("authRedirect");
if (!ctx.isAuthorized()) {
if (window.location.pathname !== "/") {
console.debug("[middleware/auth] isAuthorized returned false, redirecting to /");
redirectTo.value = window.location.pathname;
return navigateTo("/");
}
}
@@ -15,6 +17,7 @@ export default defineNuxtRouteMiddleware(async () => {
if (error) {
if (window.location.pathname !== "/") {
console.debug("[middleware/user] user is null and fetch failed, redirecting to /");
redirectTo.value = window.location.pathname;
return navigateTo("/");
}
}

View File

@@ -103,6 +103,7 @@
const loading = ref(false);
const loginPassword = ref("");
const redirectTo = useState("authRedirect");
async function login() {
loading.value = true;
@@ -116,7 +117,8 @@
toast.success("Logged in successfully");
navigateTo("/home");
navigateTo(redirectTo.value || "/home");
redirectTo.value = null;
loading.value = false;
}
@@ -156,10 +158,10 @@
<a href="https://twitter.com/haybytes" class="tooltip" data-tip="Follow The Developer" target="_blank">
<MdiTwitter class="h-8 w-8" />
</a>
<a href="https://discord.gg/tuncmNrE4z" class="tooltip" data-tip="Join The Discord" target="_blank">
<a href="https://discord.gg/aY4DCkpNA9" class="tooltip" data-tip="Join The Discord" target="_blank">
<MdiDiscord class="h-8 w-8" />
</a>
<a href="https://hay-kot.github.io/homebox/" class="tooltip" data-tip="Read The Docs" target="_blank">
<a href="https://homebox.sysadminsmedia.com/en/" class="tooltip" data-tip="Read The Docs" target="_blank">
<MdiFolder class="h-8 w-8" />
</a>
</div>

View File

@@ -39,6 +39,7 @@
const advanced = useRouteQuery("advanced", false);
const includeArchived = useRouteQuery("archived", false);
const fieldSelector = useRouteQuery("fieldSelector", false);
const negateLabels = useRouteQuery("negateLabels", false);
const totalPages = computed(() => Math.ceil(total.value / pageSize.value));
const hasNext = computed(() => page.value * pageSize.value < total.value);
@@ -162,6 +163,12 @@
}
});
watch(negateLabels, (newV, oldV) => {
if (newV !== oldV) {
search();
}
});
async function fetchValues(field: string): Promise<string[]> {
if (fieldValuesCache.value[field]) {
return fieldValuesCache.value[field];
@@ -193,6 +200,7 @@
page: page.value,
pageSize: pageSize.value,
includeArchived: includeArchived.value ? "true" : "false",
negateLabels: negateLabels.value ? "true" : "false",
},
});
}
@@ -219,6 +227,7 @@
q: query.value || "",
locations: locIDs.value,
labels: labIDs.value,
negateLabels: negateLabels.value,
includeArchived: includeArchived.value,
page: page.value,
pageSize: pageSize.value,
@@ -268,6 +277,7 @@
advanced: "true",
archived: includeArchived.value ? "true" : "false",
fieldSelector: fieldSelector.value ? "true" : "false",
negateLabels: negateLabels.value ? "true" : "false",
pageSize: pageSize.value,
page: page.value,
q: query.value,
@@ -359,6 +369,10 @@
<input v-model="fieldSelector" type="checkbox" class="toggle toggle-sm toggle-primary" />
<span class="label-text ml-4"> Field Selector </span>
</label>
<label class="label cursor-pointer mr-auto">
<input v-model="negateLabels" type="checkbox" class="toggle toggle-sm toggle-primary" />
<span class="label-text ml-4"> Negate selected labels </span>
</label>
<hr class="my-2" />
<BaseButton class="btn-block btn-sm" @click="reset"> Reset Search</BaseButton>
</div>

View File

@@ -88,7 +88,7 @@
return [];
}
return resp.data.items;
return resp.data;
});
</script>
@@ -115,8 +115,17 @@
</div>
</div>
<div>
<h1 class="text-2xl pb-1">
<h1 class="text-2xl pb-1 flex items-center gap-3">
{{ label ? label.name : "" }}
<div
v-if="items && items.totalPrice"
class="text-xs bg-secondary text-secondary-content rounded-full px-2 py-1"
>
<div>
<Currency :amount="items.totalPrice" />
</div>
</div>
</h1>
<div class="flex gap-1 flex-wrap text-xs">
<div>
@@ -144,7 +153,7 @@
<Markdown v-if="label && label.description" class="text-base" :source="label.description"> </Markdown>
</div>
<section v-if="label && items">
<ItemViewSelectable :items="items" />
<ItemViewSelectable :items="items.items" />
</section>
</BaseContainer>
</BaseContainer>

View File

@@ -138,8 +138,17 @@
<li>{{ location.name }}</li>
</ul>
</div>
<h1 class="text-2xl pb-1">
<h1 class="text-2xl pb-1 flex items-center gap-3">
{{ location ? location.name : "" }}
<div
v-if="location && location.totalPrice"
class="text-xs bg-secondary text-secondary-content rounded-full px-2 py-1"
>
<div>
<Currency :amount="location.totalPrice" />
</div>
</div>
</h1>
<div class="flex gap-1 flex-wrap text-xs">
<div>

12218
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,5 +14,6 @@
"license": "ISC",
"devDependencies": {
"vitepress": "^1.2.3"
}
}
},
"packageManager": "pnpm@9.1.4+sha512.9df9cf27c91715646c7d675d1c9c8e41f6fce88246f1318c1aa6a1ed1aeb3c4f032fcdf4ba63cc69c4fe6d634279176b5358727d8f2cc1e65b65f43ce2f8bfb0"
}

View File

@@ -1,6 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
]
}