Compare commits

..

1 Commits

Author SHA1 Message Date
tonya
646a80f494 feat: begin work on language updates 2025-01-04 13:11:23 +00:00
250 changed files with 7703 additions and 17361 deletions

View File

@@ -1,3 +1,5 @@
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:18-bullseye
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster
ARG VARIANT=16-bullseye
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT}
RUN sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b ~/.local/bin
RUN sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b ~/.local/bin

View File

@@ -3,7 +3,13 @@
{
"name": "Node.js & TypeScript",
"build": {
"dockerfile": "Dockerfile"
"dockerfile": "Dockerfile",
// Update 'VARIANT' to pick a Node version: 18, 16, 14.
// Append -bullseye or -buster to pin to an OS version.
// Use -bullseye variants on local on arm64/Apple Silicon.
"args": {
"VARIANT": "18-bullseye"
}
},
// Configure tool-specific properties.

View File

@@ -4,8 +4,11 @@ import os
def fetch_currencies():
try:
response = requests.get('https://restcountries.com/v3.1/all?fields=name,common,currencies')
response = requests.get('https://restcountries.com/v3.1/all')
response.raise_for_status()
except requests.exceptions.Timeout:
print("Request to the API timed out.")
return []
except requests.exceptions.RequestException as e:
print(f"An error occurred while making the request: {e}")
return []
@@ -32,12 +35,10 @@ def fetch_currencies():
return currencies_list
def save_currencies(currencies, file_path):
# Sort the list by the "local" field
sorted_currencies = sorted(currencies, key=lambda x: x['local'].lower() if x['local'] else "")
try:
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(sorted_currencies, f, ensure_ascii=False, indent=4)
json.dump(currencies, f, ensure_ascii=False, indent=4)
except IOError as e:
print(f"An error occurred while writing to the file: {e}")
@@ -61,4 +62,4 @@ def main():
print("Currencies updated and saved.")
if __name__ == "__main__":
main()
main()

View File

@@ -1,52 +0,0 @@
name: Docker Cleanup
on:
schedule:
- cron: '00 0 * * *'
workflow_dispatch:
jobs:
delete-old-images-main:
name: Delete Untagged Images
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- name: Fetch multi-platform package version SHAs
id: multi-arch-digests
run: |
package1=$(docker manifest inspect ghcr.io/sysadminsmedia/homebox | jq -r '.manifests.[] | .digest' | paste -s -d ' ' -)
echo "multi-arch-digests=$package1" >> $GITHUB_OUTPUT
- uses: snok/container-retention-policy@v3.0.0
with:
skip-shas: ${{ steps.multi-arch-digests.outputs.multi-arch-digests }}
# The type of account. Can be either 'org' or 'personal'.
account: sysadminsmedia
# Image name to delete. Supports passing several names as a comma-separated list.
image-names: homebox
# The cut-off for which to delete images older than. For example '2 days ago UTC'. Timezone is required.
cut-off: 90d
# Personal access token with read and delete scopes.
token: ${{ secrets.CLEANUP_PAT }}
# Restrict deletions to images without specific tags. Supports Unix-shell style wildcards
skip-tags: "!latest,!latest-rootless,!0.*,!0.*-rootless,!main,!main-rootless,!vnext,!vnext-rootless,!0,!0-rootless" # optional
# Do not actually delete images. Print output showing what would have been deleted.
dry-run: true # optional, default is false
delete-old-images-devcache:
name: Delete Cache Old Images
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- uses: snok/container-retention-policy@v3.0.0
with:
# The type of account. Can be either 'org' or 'personal'.
account: sysadminsmedia
image-names: devcache
# The cut-off for which to delete images older than. For example '2 days ago UTC'. Timezone is required.
cut-off: 90d
# Personal access token with read and delete scopes.
token: ${{ secrets.CLEANUP_PAT }}
# Do not actually delete images. Print output showing what would have been deleted.
dry-run: true # optional, default is false

View File

@@ -0,0 +1,105 @@
name: Docker publish ARM
on:
schedule:
- cron: '00 0 * * *'
push:
branches: [ "main" ]
paths:
- 'backend/**'
- 'frontend/**'
- 'Dockerfile'
- 'Dockerfile.rootless'
- '.dockerignore'
- '.github/workflows'
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
pull_request:
branches: [ "main" ]
paths:
- 'backend/**'
- 'frontend/**'
- 'Dockerfile'
- 'Dockerfile.rootless'
- '.dockerignore'
- '.github/workflows'
env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
# Step 1: Checkout repository
- name: Checkout repository
uses: actions/checkout@v4
# Step 2: Set up Buildx without specifying driver
# Let it use default settings to avoid the 'no remote endpoint' issue
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
with:
install: true # Ensure Buildx is installed and set up properly
use: true # Use Buildx instance directly for this job
# Step 3: Login against Docker registry except on PR
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v3.0.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Step 4: Extract metadata for Docker images
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5.0.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=schedule,pattern=nightly
flavor: |
suffix=-arm,onlatest=true
# Step 5: Build and push the Docker image
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v5.0.0
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/arm64,linux/arm/v7
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
VERSION=${{ github.ref_name }}
COMMIT=${{ github.sha }}
# Step 6: Attest built image to prove build provenance
- name: Attest
uses: actions/attest-build-provenance@v1
id: attest
if: ${{ github.event_name != 'pull_request' }}
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.build-and-push.outputs.digest }}
push-to-registry: true

View File

@@ -0,0 +1,104 @@
name: Docker publish rootless ARM
on:
schedule:
- cron: '00 0 * * *'
push:
branches: [ "main" ]
paths:
- 'backend/**'
- 'frontend/**'
- 'Dockerfile'
- 'Dockerfile.rootless'
- '.dockerignore'
- '.github/workflows'
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
pull_request:
branches: [ "main" ]
paths:
- 'backend/**'
- 'frontend/**'
- 'Dockerfile'
- 'Dockerfile.rootless'
- '.dockerignore'
- '.github/workflows'
env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
jobs:
build-rootless:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
# Step 1: Checkout repository
- name: Checkout repository
uses: actions/checkout@v4
# Step 2: Set up Buildx without specifying driver
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
with:
install: true # Ensure Buildx is installed and set up properly
use: true # Use Buildx instance directly for this job
# Step 3: Login to Docker registry except on PR
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v3.0.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Step 4: Extract metadata for Docker images
- name: Extract Docker metadata
id: metadata
uses: docker/metadata-action@v5.0.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=schedule,pattern=nightly
flavor: |
suffix=-rootless-arm,onlatest=true
# Step 5: Build and push the Docker image
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v5.0.0
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
platforms: linux/arm64,linux/arm/v7
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
VERSION=${{ github.ref_name }}
COMMIT=${{ github.sha }}
# Step 6: Attest built image to prove build provenance
- name: Attest
uses: actions/attest-build-provenance@v1
id: attest
if: ${{ github.event_name != 'pull_request' }}
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.build-and-push.outputs.digest }}
push-to-registry: true

View File

@@ -9,10 +9,10 @@ on:
- 'backend/**'
- 'frontend/**'
- 'Dockerfile'
- 'Dockerfile.rootless'
- '.dockerignore'
- '.github/workflows/docker-publish-rootless.yaml'
ignore:
- 'docs/**'
- '.github/workflows'
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
pull_request:
branches: [ "main" ]
@@ -20,170 +20,57 @@ on:
- 'backend/**'
- 'frontend/**'
- 'Dockerfile'
- 'Dockerfile.rootless'
- '.dockerignore'
- '.github/workflows/docker-publish-rootless.yaml'
ignore:
- 'docs/**'
- '.github/workflows'
permissions:
contents: read # Access to repository contents
packages: write # Write access for pushing to GHCR
id-token: write # Required for OIDC authentication (if used)
attestations: write # Required for signing and attestation (if needed)
env:
DOCKERHUB_REPO: sysadminsmedia/homebox
GHCR_REPO: ghcr.io/sysadminsmedia/homebox
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
build-rootless:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
attestations: write
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
- linux/arm/v7
id-token: write
steps:
- name: Enable Debug Logs
run: echo "##[debug]Enabling debug logging"
env:
ACTIONS_RUNNER_DEBUG: true
ACTIONS_STEP_DEBUG: true
- name: Checkout repository
uses: actions/checkout@v4
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
branch=${{ github.event.pull_request.number || github.ref_name }}
echo "BRANCH=${branch//\//-}" >> $GITHUB_ENV
echo "DOCKERNAMES=${{ env.DOCKERHUB_REPO }},${{ env.GHCR_REPO }}" >> $GITHUB_ENV
if [[ "${{ github.event_name }}" != "schedule" ]] || [[ "${{ github.ref }}" != refs/tags/* ]]; then
echo "DOCKERNAMES=${{ env.GHCR_REPO }}" >> $GITHUB_ENV
fi
# Set up BuildKit Docker container builder to be able to build
# multi-platform images and export cache
# https://github.com/docker/setup-buildx-action
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0 # v3.0.0
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v3.0.0 # v3.0.0
with:
images: |
name=${{ env.DOCKERHUB_REPO }},enable=${{ github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') }}
name=${{ env.GHCR_REPO }}
- name: Login to Docker Hub
uses: docker/login-action@v3
if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: metadata
uses: docker/metadata-action@v5.0.0 # v5.0.0
with:
image: ghcr.io/amitie10g/binfmt:latest
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: |
image=ghcr.io/amitie10g/buildkit:master
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
with:
context: . # Explicitly specify the build context
file: ./Dockerfile.rootless # Explicitly specify the Dockerfile
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,"name=${{ env.DOCKERNAMES }}",push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
cache-from: type=registry,ref=ghcr.io/sysadminsmedia/devcache:${{ env.PLATFORM_PAIR }}-${{ env.BRANCH }}-rootless
cache-to: type=registry,ref=ghcr.io/sysadminsmedia/devcache:${{ env.PLATFORM_PAIR }}-${{ env.BRANCH }}-rootless,mode=max,ignore-error=true
build-args: |
VERSION=${{ github.ref_name }}
COMMIT=${{ github.sha }}
provenance: true
sbom: true
annotations: ${{ steps.meta.outputs.annotations }}
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
attestations: write
needs:
- build
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Login to Docker Hub
uses: docker/login-action@v3
if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: |
image=ghcr.io/amitie10g/buildkit:master
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
name=${{ env.DOCKERHUB_REPO }},enable=${{ github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') }}
name=${{ env.GHCR_REPO }}
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
@@ -193,18 +80,29 @@ jobs:
type=schedule,pattern=nightly
flavor: |
suffix=-rootless,onlatest=true
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v5.0.0 # v5.0.0
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
platforms: linux/amd64
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
VERSION=${{ github.ref_name }}
COMMIT=${{ github.sha }}
- name: Create manifest list and push GHCR
id: push-ghcr
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *)
- name: Create manifest list and push Dockerhub
id: push-dockerhub
working-directory: /tmp/digests
if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.DOCKERHUB_REPO }}@sha256:%s ' *)
- name: Attest
uses: actions/attest-build-provenance@v1
id: attest
if: ${{ github.event_name != 'pull_request' }}
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.build-and-push.outputs.digest }}
push-to-registry: true

View File

@@ -9,10 +9,10 @@ on:
- 'backend/**'
- 'frontend/**'
- 'Dockerfile'
- 'Dockerfile.rootless'
- '.dockerignore'
- '.github/workflows/docker-publish.yaml'
ignore:
- 'docs/**'
- '.github/workflows'
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
pull_request:
branches: [ "main" ]
@@ -20,162 +20,56 @@ on:
- 'backend/**'
- 'frontend/**'
- 'Dockerfile'
- 'Dockerfile.rootless'
- '.dockerignore'
- '.github/workflows/docker-publish.yaml'
ignore:
- 'docs/**'
- '.github/workflows'
env:
DOCKERHUB_REPO: sysadminsmedia/homebox
GHCR_REPO: ghcr.io/sysadminsmedia/homebox
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
permissions:
contents: read # Access to repository contents
packages: write # Write access for pushing to GHCR
id-token: write # Required for OIDC authentication (if used)
attestations: write # Required for signing and attestation (if needed)
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read # Allows access to repository contents (read-only)
packages: write # Allows pushing to GHCR
id-token: write # Allows identity token write access for authentication
attestations: write # Needed for signing and attestation (if required)
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
- linux/arm/v7
contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
attestations: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
branch=${{ github.event.pull_request.number || github.ref_name }}
echo "BRANCH=${branch//\//-}" >> $GITHUB_ENV
echo "DOCKERNAMES=${{ env.DOCKERHUB_REPO }},${{ env.GHCR_REPO }}" >> $GITHUB_ENV
if [[ "${{ github.event_name }}" != "schedule" ]] || [[ "${{ github.ref }}" != refs/tags/* ]]; then
echo "DOCKERNAMES=${{ env.GHCR_REPO }}" >> $GITHUB_ENV
fi
# Set up BuildKit Docker container builder to be able to build
# multi-platform images and export cache
# https://github.com/docker/setup-buildx-action
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0 # v3.0.0
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v3.0.0 # v3.0.0
with:
images: |
name=${{ env.DOCKERHUB_REPO }},enable=${{ github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') }}
name=${{ env.GHCR_REPO }}
- name: Login to Docker Hub
uses: docker/login-action@v3
if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
image: ghcr.io/amitie10g/binfmt:latest
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: |
image=ghcr.io/amitie10g/buildkit:master
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
with:
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,"name=${{ env.DOCKERNAMES }}",push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
cache-from: type=registry,ref=ghcr.io/sysadminsmedia/devcache:${{ env.PLATFORM_PAIR }}-${{ env.BRANCH }}
cache-to: type=registry,ref=ghcr.io/sysadminsmedia/devcache:${{ env.PLATFORM_PAIR}}-${{ env.BRANCH }},mode=max,ignore-error=true
build-args: |
VERSION=${{ github.ref_name }}
COMMIT=${{ github.sha }}
provenance: true
sbom: true
annotations: ${{ steps.meta.outputs.annotations }}
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read # Allows access to repository contents (read-only)
packages: write # Allows pushing to GHCR
id-token: write # Allows identity token write access for authentication
attestations: write # Needed for signing and attestation (if required)
needs:
- build
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: |
image=ghcr.io/amitie10g/buildkit:master
- name: Docker meta
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v5.0.0 # v5.0.0
with:
images: |
name=${{ env.DOCKERHUB_REPO }},enable=${{ github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') }}
name=${{ env.GHCR_REPO }}
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
@@ -184,18 +78,28 @@ jobs:
type=semver,pattern={{major}}
type=schedule,pattern=nightly
- name: Create manifest list and push GHCR
id: push-ghcr
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *)
- name: Create manifest list and push Dockerhub
id: push-dockerhub
working-directory: /tmp/digests
if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.DOCKERHUB_REPO }}@sha256:%s ' *)
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v5.0.0 # v5.0.0
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
VERSION=${{ github.ref_name }}
COMMIT=${{ github.sha }}
- name: Attest
uses: actions/attest-build-provenance@v1
id: attest
if: ${{ github.event_name != 'pull_request' }}
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.build-and-push.outputs.digest }}
push-to-registry: true

View File

@@ -34,8 +34,3 @@ jobs:
- name: Test
run: task go:coverage
- name: Validate OpenAPI definition
uses: swaggerexpert/swagger-editor-validate@v1
with:
definition-file: backend/app/api/static/docs/swagger.json

View File

@@ -32,20 +32,6 @@ jobs:
integration-tests:
name: Integration Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17
env:
POSTGRES_USER: homebox
POSTGRES_PASSWORD: homebox
POSTGRES_DB: homebox
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -76,53 +62,3 @@ jobs:
- name: Run Integration Tests
run: task test:ci
integration-tests-pgsql:
strategy:
matrix:
database_version: [17,16,15]
name: Integration Tests PGSQL ${{ matrix.database_version }}
runs-on: ubuntu-latest
services:
postgres:
image: postgres:${{ matrix.database_version }}
env:
POSTGRES_USER: homebox
POSTGRES_PASSWORD: homebox
POSTGRES_DB: homebox
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Task
uses: arduino/setup-task@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.21"
- uses: actions/setup-node@v4
with:
node-version: 18
- uses: pnpm/action-setup@v3.0.0
with:
version: 9.12.2
- name: Install dependencies
run: pnpm install
working-directory: frontend
- name: Run Integration Tests
run: task test:ci:postgresql

View File

@@ -4,12 +4,10 @@ on:
pull_request:
branches:
- main
- vnext
paths:
- 'backend/**'
- 'frontend/**'
- '.github/workflows/**'
jobs:
backend-tests:

8
.gitignore vendored
View File

@@ -2,9 +2,6 @@
backend/.data/*
config.yml
homebox.db
homebox.db-journal
homebox.db-shm
homebox.db-wal
.idea
.DS_Store
test-mailer.json
@@ -36,7 +33,7 @@ go.work
backend/.env
build/*
# Output Directory for Nuxt/Frontend during build steps
# Output Directory for Nuxt/Frontend during build step
backend/app/api/public/*
!backend/app/api/public/.gitkeep
@@ -59,5 +56,4 @@ backend/app/api/static/public/*
!backend/app/api/static/public/.gitkeep
backend/api
docs/.vitepress/cache/
/.data/
docs/.vitepress/cache/

2
.vscode/launch.json vendored
View File

@@ -23,7 +23,7 @@
"HBOX_LOG_LEVEL": "debug",
"HBOX_DEBUG_ENABLED": "true",
"HBOX_STORAGE_DATA": "${workspaceRoot}/backend/.data",
"HBOX_STORAGE_SQLITE_URL": "${workspaceRoot}/backend/.data/homebox.db?_fk=1&_time_format=sqlite"
"HBOX_STORAGE_SQLITE_URL": "${workspaceRoot}/backend/.data/homebox.db?_fk=1"
},
"console": "integratedTerminal",
},

View File

@@ -30,8 +30,5 @@
"editor.quickSuggestions": {
"strings": true
},
"tailwindCSS.experimental.configFile": "./frontend/tailwind.config.js",
"[go]": {
"editor.defaultFormatter": "golang.go"
},
"tailwindCSS.experimental.configFile": "./frontend/tailwind.config.js"
}

View File

@@ -44,7 +44,7 @@ start command `task go:run`
### Frontend Development Notes
start command `task ui:dev`
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`.
@@ -54,4 +54,4 @@ start command `task ui:dev`
Create a new tag in GitHub with the version number vX.X.X. This will trigger a new release to be created.
Test -> Goreleaser -> Publish Release -> Trigger Docker Builds -> Deploy Docs + Fly.io Demo
Test -> Goreleaser -> Publish Release -> Trigger Docker Builds -> Deploy Docs + Fly.io Demo

View File

@@ -1,5 +1,5 @@
# Node dependencies stage
FROM public.ecr.aws/docker/library/node:18-alpine AS frontend-dependencies
FROM --platform=$TARGETPLATFORM node:18-alpine AS frontend-dependencies
WORKDIR /app
# Install pnpm globally (caching layer)
@@ -10,7 +10,7 @@ COPY frontend/package.json frontend/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --shamefully-hoist
# Build Nuxt (frontend) stage
FROM public.ecr.aws/docker/library/node:18-alpine AS frontend-builder
FROM --platform=$TARGETPLATFORM node:18-alpine AS frontend-builder
WORKDIR /app
# Install pnpm globally again (it can reuse the cache if not changed)
@@ -22,7 +22,7 @@ COPY --from=frontend-dependencies /app/node_modules ./node_modules
RUN pnpm build
# Go dependencies stage
FROM public.ecr.aws/docker/library/golang:alpine AS builder-dependencies
FROM --platform=$TARGETPLATFORM golang:alpine AS builder-dependencies
WORKDIR /go/src/app
# Copy go.mod and go.sum for better caching
@@ -30,7 +30,7 @@ COPY ./backend/go.mod ./backend/go.sum ./
RUN go mod download
# Build API stage
FROM public.ecr.aws/docker/library/golang:alpine AS builder
FROM --platform=$TARGETPLATFORM golang:alpine AS builder
ARG BUILD_TIME
ARG COMMIT
ARG VERSION
@@ -58,10 +58,10 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
-v ./app/api/*.go
# Production stage
FROM public.ecr.aws/docker/library/alpine:latest
FROM --platform=$TARGETPLATFORM alpine:latest
ENV HBOX_MODE=production
ENV HBOX_STORAGE_DATA=/data/
ENV HBOX_DATABASE_SQLITE_PATH=/data/homebox.db?_pragma=busy_timeout=2000&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite
ENV HBOX_STORAGE_SQLITE_URL=/data/homebox.db?_pragma=busy_timeout=2000&_pragma=journal_mode=WAL&_fk=1
# Install necessary runtime dependencies
RUN apk --no-cache add ca-certificates wget
@@ -75,7 +75,7 @@ RUN chmod +x /app/api
LABEL Name=homebox Version=0.0.1
LABEL org.opencontainers.image.source="https://github.com/sysadminsmedia/homebox"
# Expose necessary ports for Homebox
# Expose necessary ports
EXPOSE 7745
WORKDIR /app

View File

@@ -1,97 +1,73 @@
# Node dependencies stage
FROM public.ecr.aws/docker/library/node:18-alpine AS frontend-dependencies
# Node dependencies
FROM node:18-alpine AS frontend-dependencies
WORKDIR /app
# Install pnpm globally (caching layer)
RUN npm install -g pnpm
# Copy package.json and lockfile to leverage caching
COPY frontend/package.json frontend/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --shamefully-hoist
# Build Nuxt (frontend) stage
FROM public.ecr.aws/docker/library/node:18-alpine AS frontend-builder
# Build Nuxt
FROM node:18-alpine AS frontend-builder
WORKDIR /app
# Install pnpm globally again (it can reuse the cache if not changed)
RUN npm install -g pnpm
# Copy over source files and node_modules from dependencies stage
COPY frontend .
COPY frontend ./
COPY --from=frontend-dependencies /app/node_modules ./node_modules
RUN pnpm build
# Go dependencies stage
FROM public.ecr.aws/docker/library/golang:alpine AS builder-dependencies
# Build Go dependencies
FROM golang:alpine AS builder-dependencies
WORKDIR /go/src/app
# Copy go.mod and go.sum for better caching
COPY ./backend/go.mod ./backend/go.sum ./
RUN go mod download
# Build API stage
FROM public.ecr.aws/docker/library/golang:alpine AS builder
# Build API
FROM golang:alpine AS builder
ARG BUILD_TIME
ARG COMMIT
ARG VERSION
# Install necessary build tools
RUN apk update && \
apk upgrade && \
apk add --no-cache git build-base gcc g++
RUN apk update && apk upgrade && apk add --no-cache git build-base gcc g++
WORKDIR /go/src/app
# Copy Go modules (from dependencies stage) and source code
COPY --from=builder-dependencies /go/pkg/mod /go/pkg/mod
COPY ./backend .
# Clear old public files and copy new ones from frontend build
RUN rm -rf ./app/api/public
COPY --from=frontend-builder /app/.output/public ./app/api/static/public
COPY --from=builder-dependencies /go/pkg/mod /go/pkg/mod
# Use cache for Go build artifacts
# Use cache for Go build
RUN --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=linux go build \
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
-o /go/bin/api \
-v ./app/api/*.go
-o /go/bin/api ./app/api/*.go
RUN mkdir /data
# Production stage with distroless
FROM gcr.io/distroless/static:latest
# Production stage
FROM public.ecr.aws/docker/library/alpine:latest
ENV HBOX_MODE=production
ENV HBOX_STORAGE_DATA=/data/
ENV HBOX_DATABASE_SQLITE_PATH=/data/homebox.db?_pragma=busy_timeout=2000&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite
ENV HBOX_STORAGE_SQLITE_URL=/data/homebox.db?_fk=1
# Install necessary runtime dependencies
RUN apk --no-cache add ca-certificates wget
# Create a nonroot user with UID/GID 65532
RUN addgroup -g 65532 nonroot && adduser -u 65532 -G nonroot -S nonroot
# Create application directory and copy over built Go binary
RUN mkdir /app
# Copy the binary and data directory, change ownership
COPY --from=builder --chown=nonroot /go/bin/api /app
COPY --from=builder --chown=nonroot /data /data
RUN chmod +x /app/api
# Labels and configuration for the final image
# Add wget to the image
# Note: If using distroless, this may not be applicable
# as distroless images do not include package managers.
# This line may be omitted if you're relying on another way to handle healthchecks.
COPY --from=alpine:latest /bin/wget /usr/bin/wget
LABEL Name=homebox Version=0.0.1
LABEL org.opencontainers.image.source="https://github.com/sysadminsmedia/homebox"
# Expose necessary ports for Homebox
EXPOSE 7745
WORKDIR /app
# Healthcheck configuration
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD [ "wget", "--no-verbose", "--tries=1", "-O", "-", "http://localhost:7745/api/v1/status" ]
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"]
# Persist volume for data
VOLUME [ "/data" ]
VOLUME ["/data"]
# Entrypoint and CMD
# Drop root and run as a low-privileged user
USER nonroot
ENTRYPOINT [ "/app/api" ]
CMD [ "/data/config.yml" ]
ENTRYPOINT ["/app"]
CMD ["/data/config.yml"]

View File

@@ -13,7 +13,7 @@
## What is HomeBox
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:
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.
@@ -21,10 +21,6 @@ HomeBox is the inventory and organization system built for the Home User! With a
# Screenshots
Check out screenshots of the project [here](https://imgur.com/a/5gLWt2j).
You can also try the demo instances of Homebox:
- [Demo](https://demo.homebox.software)
- [Nightly](https://nightly.homebox.software)
- [VNext](https://vnext.homebox.software/)
## Quick Start

View File

@@ -2,8 +2,7 @@ version: "3"
env:
HBOX_LOG_LEVEL: debug
HBOX_DATABASE_DRIVER: sqlite3
HBOX_DATABASE_SQLITE_PATH: .data/homebox.db?_pragma=busy_timeout=1000&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite
HBOX_STORAGE_SQLITE_URL: .data/homebox.db?_pragma=busy_timeout=1000&_pragma=journal_mode=WAL&_fk=1
HBOX_OPTIONS_ALLOW_REGISTRATION: true
UNSAFE_DISABLE_PASSWORD_PROJECTION: "yes_i_am_sure"
tasks:
@@ -29,6 +28,7 @@ tasks:
- "./backend/internal/data/**"
- "./backend/internal/core/services/**/*"
- "./backend/app/tools/typegen/main.go"
typescript-types:
desc: Generates typescript types from swagger definition
cmds:
@@ -52,8 +52,6 @@ tasks:
- cp ./backend/app/api/static/docs/swagger.json docs/docs/api/openapi-2.0.json
go:run:
env:
HBOX_DEMO: true
desc: Starts the backend api server (depends on generate task)
dir: backend
deps:
@@ -62,24 +60,6 @@ tasks:
- go run ./app/api/ {{ .CLI_ARGS }}
silent: false
go:run:postgresql:
env:
HBOX_DEMO: true
HBOX_DATABASE_DRIVER: postgres
HBOX_DATABASE_USERNAME: homebox
HBOX_DATABASE_PASSWORD: homebox
HBOX_DATABASE_DATABASE: homebox
HBOX_DATABASE_HOST: localhost
HBOX_DATABASE_PORT: 5432
HBOX_DATABASE_SSL_MODE: disable
desc: Starts the backend api server with postgresql (depends on generate task)
dir: backend
deps:
- generate
cmds:
- go run ./app/api/ {{ .CLI_ARGS }}
silent: false
go:test:
desc: Runs all go tests using gotestsum - supports passing gotestsum args
dir: backend
@@ -134,21 +114,6 @@ tasks:
cmds:
- cd backend && go run app/tools/migrations/main.go {{ .CLI_ARGS }}
db:migration:postgresql:
env:
HBOX_DATABASE_DRIVER: postgres
HBOX_DATABASE_USERNAME: homebox
HBOX_DATABASE_PASSWORD: homebox
HBOX_DATABASE_DATABASE: homebox
HBOX_DATABASE_HOST: localhost
HBOX_DATABASE_PORT: 5432
HBOX_DATABASE_SSL_MODE: disable
desc: Runs the database diff engine to generate a SQL migration files for postgresql
deps:
- db:generate
cmds:
- cd backend && go run app/tools/migrations/main.go {{ .CLI_ARGS }}
ui:watch:
desc: Starts the vitest test runner in watch mode
dir: frontend
@@ -178,24 +143,7 @@ tasks:
cmds:
- cd backend && go build ./app/api
- backend/api &
- sleep 10
- cd frontend && pnpm run test:ci
silent: true
test:ci:postgresql:
env:
HBOX_DATABASE_DRIVER: postgres
HBOX_DATABASE_USERNAME: homebox
HBOX_DATABASE_PASSWORD: homebox
HBOX_DATABASE_DATABASE: homebox
HBOX_DATABASE_HOST: 127.0.0.1
HBOX_DATABASE_PORT: 5432
HBOX_DATABASE_SSL_MODE: disable
desc: Runs end-to-end test on a live server with postgresql (only for use in CI)
cmds:
- cd backend && go build ./app/api
- backend/api &
- sleep 10
- sleep 5
- cd frontend && pnpm run test:ci
silent: true

View File

@@ -23,9 +23,8 @@ func NewTask(name string, interval time.Duration, fn func(context.Context)) *Bac
}
func (tsk *BackgroundTask) Start(ctx context.Context) error {
tsk.Fn(ctx)
timer := time.NewTimer(tsk.Interval)
for {
select {
case <-ctx.Done():

View File

@@ -13,7 +13,6 @@ import (
"github.com/sysadminsmedia/homebox/backend/internal/core/services"
"github.com/sysadminsmedia/homebox/backend/internal/core/services/reporting/eventbus"
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
"github.com/olahol/melody"
)
@@ -73,7 +72,6 @@ type V1Controller struct {
allowRegistration bool
bus *eventbus.EventBus
url string
config *config.Config
}
type (
@@ -86,25 +84,22 @@ type (
}
APISummary struct {
Healthy bool `json:"health"`
Versions []string `json:"versions"`
Title string `json:"title"`
Message string `json:"message"`
Build Build `json:"build"`
Latest services.Latest `json:"latest"`
Demo bool `json:"demo"`
AllowRegistration bool `json:"allowRegistration"`
LabelPrinting bool `json:"labelPrinting"`
Healthy bool `json:"health"`
Versions []string `json:"versions"`
Title string `json:"title"`
Message string `json:"message"`
Build Build `json:"build"`
Demo bool `json:"demo"`
AllowRegistration bool `json:"allowRegistration"`
}
)
func NewControllerV1(svc *services.AllServices, repos *repo.AllRepos, bus *eventbus.EventBus, config *config.Config, options ...func(*V1Controller)) *V1Controller {
func NewControllerV1(svc *services.AllServices, repos *repo.AllRepos, bus *eventbus.EventBus, options ...func(*V1Controller)) *V1Controller {
ctrl := &V1Controller{
repo: repos,
svc: svc,
allowRegistration: true,
bus: bus,
config: config,
}
for _, opt := range options {
@@ -116,11 +111,11 @@ func NewControllerV1(svc *services.AllServices, repos *repo.AllRepos, bus *event
// HandleBase godoc
//
// @Summary Application Info
// @Tags Base
// @Produce json
// @Success 200 {object} APISummary
// @Router /v1/status [GET]
// @Summary Application Info
// @Tags Base
// @Produce json
// @Success 200 {object} APISummary
// @Router /v1/status [GET]
func (ctrl *V1Controller) HandleBase(ready ReadyFunc, build Build) errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
return server.JSON(w, http.StatusOK, APISummary{
@@ -128,21 +123,19 @@ func (ctrl *V1Controller) HandleBase(ready ReadyFunc, build Build) errchain.Hand
Title: "Homebox",
Message: "Track, Manage, and Organize your Things",
Build: build,
Latest: ctrl.svc.BackgroundService.GetLatestVersion(),
Demo: ctrl.isDemo,
AllowRegistration: ctrl.allowRegistration,
LabelPrinting: ctrl.config.LabelMaker.PrintCommand != nil,
})
}
}
// HandleCurrency godoc
//
// @Summary Currency
// @Tags Base
// @Produce json
// @Success 200 {object} currencies.Currency
// @Router /v1/currency [GET]
// @Summary Currency
// @Tags Base
// @Produce json
// @Success 200 {object} currencies.Currency
// @Router /v1/currency [GET]
func (ctrl *V1Controller) HandleCurrency() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
// Set Cache for 10 Minutes

View File

@@ -1,30 +0,0 @@
package v1
import (
"net/url"
"github.com/rs/zerolog/log"
)
func GetHBURL(refererHeader, fallback string) (hbURL string) {
hbURL = refererHeader
if hbURL == "" {
hbURL = fallback
}
return stripPathFromURL(hbURL)
}
// stripPathFromURL removes the path from a URL.
// ex. https://example.com/tools -> https://example.com
func stripPathFromURL(rawURL string) string {
parsedURL, err := url.Parse(rawURL)
if err != nil {
log.Err(err).Msg("failed to parse URL")
return ""
}
strippedURL := url.URL{Scheme: parsedURL.Scheme, Host: parsedURL.Host}
return strippedURL.String()
}

View File

@@ -32,52 +32,52 @@ func actionHandlerFactory(ref string, fn func(context.Context, uuid.UUID) (int,
// HandleEnsureAssetID godoc
//
// @Summary Ensure Asset IDs
// @Description Ensures all items in the database have an asset ID
// @Tags Actions
// @Produce json
// @Success 200 {object} ActionAmountResult
// @Router /v1/actions/ensure-asset-ids [Post]
// @Security Bearer
// @Summary Ensure Asset IDs
// @Description Ensures all items in the database have an asset ID
// @Tags Actions
// @Produce json
// @Success 200 {object} ActionAmountResult
// @Router /v1/actions/ensure-asset-ids [Post]
// @Security Bearer
func (ctrl *V1Controller) HandleEnsureAssetID() errchain.HandlerFunc {
return actionHandlerFactory("ensure asset IDs", ctrl.svc.Items.EnsureAssetID)
}
// HandleEnsureImportRefs godoc
//
// @Summary Ensures Import Refs
// @Description Ensures all items in the database have an import ref
// @Tags Actions
// @Produce json
// @Success 200 {object} ActionAmountResult
// @Router /v1/actions/ensure-import-refs [Post]
// @Security Bearer
// @Summary Ensures Import Refs
// @Description Ensures all items in the database have an import ref
// @Tags Actions
// @Produce json
// @Success 200 {object} ActionAmountResult
// @Router /v1/actions/ensure-import-refs [Post]
// @Security Bearer
func (ctrl *V1Controller) HandleEnsureImportRefs() errchain.HandlerFunc {
return actionHandlerFactory("ensure import refs", ctrl.svc.Items.EnsureImportRef)
}
// HandleItemDateZeroOut godoc
//
// @Summary Zero Out Time Fields
// @Description Resets all item date fields to the beginning of the day
// @Tags Actions
// @Produce json
// @Success 200 {object} ActionAmountResult
// @Router /v1/actions/zero-item-time-fields [Post]
// @Security Bearer
// @Summary Zero Out Time Fields
// @Description Resets all item date fields to the beginning of the day
// @Tags Actions
// @Produce json
// @Success 200 {object} ActionAmountResult
// @Router /v1/actions/zero-item-time-fields [Post]
// @Security Bearer
func (ctrl *V1Controller) HandleItemDateZeroOut() errchain.HandlerFunc {
return actionHandlerFactory("zero out date time", ctrl.repo.Items.ZeroOutTimeFields)
}
// HandleSetPrimaryPhotos godoc
//
// @Summary Set Primary Photos
// @Description Sets the first photo of each item as the primary photo
// @Tags Actions
// @Produce json
// @Success 200 {object} ActionAmountResult
// @Router /v1/actions/set-primary-photos [Post]
// @Security Bearer
// @Summary Set Primary Photos
// @Description Sets the first photo of each item as the primary photo
// @Tags Actions
// @Produce json
// @Success 200 {object} ActionAmountResult
// @Router /v1/actions/set-primary-photos [Post]
// @Security Bearer
func (ctrl *V1Controller) HandleSetPrimaryPhotos() errchain.HandlerFunc {
return actionHandlerFactory("ensure asset IDs", ctrl.repo.Items.SetPrimaryPhotos)
}

View File

@@ -17,13 +17,13 @@ import (
// HandleAssetGet godocs
//
// @Summary Get Item by Asset ID
// @Tags Items
// @Produce json
// @Param id path string true "Asset ID"
// @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{}
// @Router /v1/assets/{id} [GET]
// @Security Bearer
// @Summary Get Item by Asset ID
// @Tags Items
// @Produce json
// @Param id path string true "Asset ID"
// @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{}
// @Router /v1/assets/{id} [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleAssetGet() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
ctx := services.NewContext(r.Context())

View File

@@ -28,8 +28,8 @@ type (
}
LoginForm struct {
Username string `json:"username" example:"admin@admin.com"`
Password string `json:"password" example:"admin"`
Username string `json:"username"`
Password string `json:"password"`
StayLoggedIn bool `json:"stayLoggedIn"`
}
)
@@ -83,6 +83,8 @@ type AuthProvider interface {
// @Tags Authentication
// @Accept x-www-form-urlencoded
// @Accept application/json
// @Param username formData string false "string" example(admin@admin.com)
// @Param password formData string false "string" example(admin)
// @Param payload body LoginForm true "Login Data"
// @Param provider query string false "auth provider"
// @Produce json
@@ -129,11 +131,11 @@ func (ctrl *V1Controller) HandleAuthLogin(ps ...AuthProvider) errchain.HandlerFu
// HandleAuthLogout godoc
//
// @Summary User Logout
// @Tags Authentication
// @Success 204
// @Router /v1/users/logout [POST]
// @Security Bearer
// @Summary User Logout
// @Tags Authentication
// @Success 204
// @Router /v1/users/logout [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleAuthLogout() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
token := services.UseTokenCtx(r.Context())
@@ -153,13 +155,13 @@ func (ctrl *V1Controller) HandleAuthLogout() errchain.HandlerFunc {
// HandleAuthRefresh godoc
//
// @Summary User Token Refresh
// @Description handleAuthRefresh returns a handler that will issue a new token from an existing token.
// @Description This does not validate that the user still exists within the database.
// @Tags Authentication
// @Success 200
// @Router /v1/users/refresh [GET]
// @Security Bearer
// @Summary User Token Refresh
// @Description handleAuthRefresh returns a handler that will issue a new token from an existing token.
// @Description This does not validate that the user still exists within the database.
// @Tags Authentication
// @Success 200
// @Router /v1/users/refresh [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleAuthRefresh() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
requestToken := services.UseTokenCtx(r.Context())

View File

@@ -26,12 +26,12 @@ type (
// HandleGroupGet godoc
//
// @Summary Get Group
// @Tags Group
// @Produce json
// @Success 200 {object} repo.Group
// @Router /v1/groups [Get]
// @Security Bearer
// @Summary Get Group
// @Tags Group
// @Produce json
// @Success 200 {object} repo.Group
// @Router /v1/groups [Get]
// @Security Bearer
func (ctrl *V1Controller) HandleGroupGet() errchain.HandlerFunc {
fn := func(r *http.Request) (repo.Group, error) {
auth := services.NewContext(r.Context())
@@ -43,13 +43,13 @@ func (ctrl *V1Controller) HandleGroupGet() errchain.HandlerFunc {
// HandleGroupUpdate godoc
//
// @Summary Update Group
// @Tags Group
// @Produce json
// @Param payload body repo.GroupUpdate true "User Data"
// @Success 200 {object} repo.Group
// @Router /v1/groups [Put]
// @Security Bearer
// @Summary Update Group
// @Tags Group
// @Produce json
// @Param payload body repo.GroupUpdate true "User Data"
// @Success 200 {object} repo.Group
// @Router /v1/groups [Put]
// @Security Bearer
func (ctrl *V1Controller) HandleGroupUpdate() errchain.HandlerFunc {
fn := func(r *http.Request, body repo.GroupUpdate) (repo.Group, error) {
auth := services.NewContext(r.Context())
@@ -69,13 +69,13 @@ func (ctrl *V1Controller) HandleGroupUpdate() errchain.HandlerFunc {
// HandleGroupInvitationsCreate godoc
//
// @Summary Create Group Invitation
// @Tags Group
// @Produce json
// @Param payload body GroupInvitationCreate true "User Data"
// @Success 200 {object} GroupInvitation
// @Router /v1/groups/invitations [Post]
// @Security Bearer
// @Summary Create Group Invitation
// @Tags Group
// @Produce json
// @Param payload body GroupInvitationCreate true "User Data"
// @Success 200 {object} GroupInvitation
// @Router /v1/groups/invitations [Post]
// @Security Bearer
func (ctrl *V1Controller) HandleGroupInvitationsCreate() errchain.HandlerFunc {
fn := func(r *http.Request, body GroupInvitationCreate) (GroupInvitation, error) {
if body.ExpiresAt.IsZero() {

View File

@@ -4,11 +4,10 @@ import (
"database/sql"
"encoding/csv"
"errors"
"fmt"
"math/big"
"net/http"
"net/url"
"strings"
"time"
"github.com/google/uuid"
"github.com/hay-kot/httpkit/errchain"
@@ -22,18 +21,18 @@ import (
// HandleItemsGetAll godoc
//
// @Summary Query All Items
// @Tags Items
// @Produce json
// @Param q query string false "search string"
// @Param page query int false "page number"
// @Param pageSize query int false "items per page"
// @Param labels query []string false "label Ids" collectionFormat(multi)
// @Param locations query []string false "location Ids" collectionFormat(multi)
// @Param parentIds query []string false "parent Ids" collectionFormat(multi)
// @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{}
// @Router /v1/items [GET]
// @Security Bearer
// @Summary Query All Items
// @Tags Items
// @Produce json
// @Param q query string false "search string"
// @Param page query int false "page number"
// @Param pageSize query int false "items per page"
// @Param labels query []string false "label Ids" collectionFormat(multi)
// @Param locations query []string false "location Ids" collectionFormat(multi)
// @Param parentIds query []string false "parent Ids" collectionFormat(multi)
// @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{}
// @Router /v1/items [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleItemsGetAll() errchain.HandlerFunc {
extractQuery := func(r *http.Request) repo.ItemQuery {
params := r.URL.Query()
@@ -55,18 +54,16 @@ func (ctrl *V1Controller) HandleItemsGetAll() errchain.HandlerFunc {
}
v := repo.ItemQuery{
Page: queryIntOrNegativeOne(params.Get("page")),
PageSize: queryIntOrNegativeOne(params.Get("pageSize")),
Search: params.Get("q"),
LocationIDs: queryUUIDList(params, "locations"),
LabelIDs: queryUUIDList(params, "labels"),
NegateLabels: queryBool(params.Get("negateLabels")),
OnlyWithoutPhoto: queryBool(params.Get("onlyWithoutPhoto")),
OnlyWithPhoto: queryBool(params.Get("onlyWithPhoto")),
ParentItemIDs: queryUUIDList(params, "parentIds"),
IncludeArchived: queryBool(params.Get("includeArchived")),
Fields: filterFieldItems(params["fields"]),
OrderBy: params.Get("orderBy"),
Page: queryIntOrNegativeOne(params.Get("page")),
PageSize: queryIntOrNegativeOne(params.Get("pageSize")),
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"]),
OrderBy: params.Get("orderBy"),
}
if strings.HasPrefix(v.Search, "#") {
@@ -109,13 +106,13 @@ func (ctrl *V1Controller) HandleItemsGetAll() errchain.HandlerFunc {
// HandleItemFullPath godoc
//
// @Summary Get the full path of an item
// @Tags Items
// @Produce json
// @Param id path string true "Item ID"
// @Success 200 {object} []repo.ItemPath
// @Router /v1/items/{id}/path [GET]
// @Security Bearer
// @Summary Get the full path of an item
// @Tags Items
// @Produce json
// @Param id path string true "Item ID"
// @Success 200 {object} []repo.ItemPath
// @Router /v1/items/{id}/path [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleItemFullPath() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID) ([]repo.ItemPath, error) {
auth := services.NewContext(r.Context())
@@ -151,13 +148,13 @@ func (ctrl *V1Controller) HandleItemFullPath() errchain.HandlerFunc {
// HandleItemsCreate godoc
//
// @Summary Create Item
// @Tags Items
// @Produce json
// @Param payload body repo.ItemCreate true "Item Data"
// @Success 201 {object} repo.ItemSummary
// @Router /v1/items [POST]
// @Security Bearer
// @Summary Create Item
// @Tags Items
// @Produce json
// @Param payload body repo.ItemCreate true "Item Data"
// @Success 201 {object} repo.ItemSummary
// @Router /v1/items [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleItemsCreate() errchain.HandlerFunc {
fn := func(r *http.Request, body repo.ItemCreate) (repo.ItemOut, error) {
return ctrl.svc.Items.Create(services.NewContext(r.Context()), body)
@@ -168,13 +165,13 @@ func (ctrl *V1Controller) HandleItemsCreate() errchain.HandlerFunc {
// HandleItemGet godocs
//
// @Summary Get Item
// @Tags Items
// @Produce json
// @Param id path string true "Item ID"
// @Success 200 {object} repo.ItemOut
// @Router /v1/items/{id} [GET]
// @Security Bearer
// @Summary Get Item
// @Tags Items
// @Produce json
// @Param id path string true "Item ID"
// @Success 200 {object} repo.ItemOut
// @Router /v1/items/{id} [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleItemGet() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID) (repo.ItemOut, error) {
auth := services.NewContext(r.Context())
@@ -187,13 +184,13 @@ func (ctrl *V1Controller) HandleItemGet() errchain.HandlerFunc {
// HandleItemDelete godocs
//
// @Summary Delete Item
// @Tags Items
// @Produce json
// @Param id path string true "Item ID"
// @Success 204
// @Router /v1/items/{id} [DELETE]
// @Security Bearer
// @Summary Delete Item
// @Tags Items
// @Produce json
// @Param id path string true "Item ID"
// @Success 204
// @Router /v1/items/{id} [DELETE]
// @Security Bearer
func (ctrl *V1Controller) HandleItemDelete() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID) (any, error) {
auth := services.NewContext(r.Context())
@@ -206,14 +203,14 @@ func (ctrl *V1Controller) HandleItemDelete() errchain.HandlerFunc {
// HandleItemUpdate godocs
//
// @Summary Update Item
// @Tags Items
// @Produce json
// @Param id path string true "Item ID"
// @Param payload body repo.ItemUpdate true "Item Data"
// @Success 200 {object} repo.ItemOut
// @Router /v1/items/{id} [PUT]
// @Security Bearer
// @Summary Update Item
// @Tags Items
// @Produce json
// @Param id path string true "Item ID"
// @Param payload body repo.ItemUpdate true "Item Data"
// @Success 200 {object} repo.ItemOut
// @Router /v1/items/{id} [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleItemUpdate() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID, body repo.ItemUpdate) (repo.ItemOut, error) {
auth := services.NewContext(r.Context())
@@ -227,14 +224,14 @@ func (ctrl *V1Controller) HandleItemUpdate() errchain.HandlerFunc {
// HandleItemPatch godocs
//
// @Summary Update Item
// @Tags Items
// @Produce json
// @Param id path string true "Item ID"
// @Param payload body repo.ItemPatch true "Item Data"
// @Success 200 {object} repo.ItemOut
// @Router /v1/items/{id} [Patch]
// @Security Bearer
// @Summary Update Item
// @Tags Items
// @Produce json
// @Param id path string true "Item ID"
// @Param payload body repo.ItemPatch true "Item Data"
// @Success 200 {object} repo.ItemOut
// @Router /v1/items/{id} [Patch]
// @Security Bearer
func (ctrl *V1Controller) HandleItemPatch() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID, body repo.ItemPatch) (repo.ItemOut, error) {
auth := services.NewContext(r.Context())
@@ -253,13 +250,13 @@ func (ctrl *V1Controller) HandleItemPatch() errchain.HandlerFunc {
// HandleGetAllCustomFieldNames godocs
//
// @Summary Get All Custom Field Names
// @Tags Items
// @Produce json
// @Success 200
// @Router /v1/items/fields [GET]
// @Success 200 {object} []string
// @Security Bearer
// @Summary Get All Custom Field Names
// @Tags Items
// @Produce json
// @Success 200
// @Router /v1/items/fields [GET]
// @Success 200 {object} []string
// @Security Bearer
func (ctrl *V1Controller) HandleGetAllCustomFieldNames() errchain.HandlerFunc {
fn := func(r *http.Request) ([]string, error) {
auth := services.NewContext(r.Context())
@@ -271,13 +268,13 @@ func (ctrl *V1Controller) HandleGetAllCustomFieldNames() errchain.HandlerFunc {
// HandleGetAllCustomFieldValues godocs
//
// @Summary Get All Custom Field Values
// @Tags Items
// @Produce json
// @Success 200
// @Router /v1/items/fields/values [GET]
// @Success 200 {object} []string
// @Security Bearer
// @Summary Get All Custom Field Values
// @Tags Items
// @Produce json
// @Success 200
// @Router /v1/items/fields/values [GET]
// @Success 200 {object} []string
// @Security Bearer
func (ctrl *V1Controller) HandleGetAllCustomFieldValues() errchain.HandlerFunc {
type query struct {
Field string `schema:"field" validate:"required"`
@@ -295,7 +292,6 @@ func (ctrl *V1Controller) HandleGetAllCustomFieldValues() errchain.HandlerFunc {
//
// @Summary Import Items
// @Tags Items
// @Accept multipart/form-data
// @Produce json
// @Success 204
// @Param csv formData file true "Image to upload"
@@ -329,29 +325,49 @@ func (ctrl *V1Controller) HandleItemsImport() errchain.HandlerFunc {
// HandleItemsExport godocs
//
// @Summary Export Items
// @Tags Items
// @Success 200 {string} string "text/csv"
// @Router /v1/items/export [GET]
// @Security Bearer
// @Summary Export Items
// @Tags Items
// @Success 200 {string} string "text/csv"
// @Router /v1/items/export [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleItemsExport() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
ctx := services.NewContext(r.Context())
csvData, err := ctrl.svc.Items.ExportCSV(r.Context(), ctx.GID, GetHBURL(r.Header.Get("Referer"), ctrl.url))
csvData, err := ctrl.svc.Items.ExportCSV(r.Context(), ctx.GID, getHBURL(r.Header.Get("Referer"), ctrl.url))
if err != nil {
log.Err(err).Msg("failed to export items")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
timestamp := time.Now().Format("2006-01-02_15-04-05") // YYYY-MM-DD_HH-MM-SS format
filename := fmt.Sprintf("homebox-items_%s.csv", timestamp) // add timestamp to filename
w.Header().Set("Content-Type", "text/csv")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment;filename=%s", filename))
w.Header().Set("Content-Disposition", "attachment;filename=homebox-items.csv")
writer := csv.NewWriter(w)
writer.Comma = ','
return writer.WriteAll(csvData)
}
}
func getHBURL(refererHeader, fallback string) (hbURL string) {
hbURL = refererHeader
if hbURL == "" {
hbURL = fallback
}
return stripPathFromURL(hbURL)
}
// stripPathFromURL removes the path from a URL.
// ex. https://example.com/tools -> https://example.com
func stripPathFromURL(rawURL string) string {
parsedURL, err := url.Parse(rawURL)
if err != nil {
log.Err(err).Msg("failed to parse URL")
return ""
}
strippedURL := url.URL{Scheme: parsedURL.Scheme, Host: parsedURL.Host}
return strippedURL.String()
}

View File

@@ -25,7 +25,6 @@ type (
//
// @Summary Create Item Attachment
// @Tags Items Attachments
// @Accept multipart/form-data
// @Produce json
// @Param id path string true "Item ID"
// @Param file formData file true "File attachment"
@@ -105,41 +104,41 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() errchain.HandlerFunc {
// HandleItemAttachmentGet godocs
//
// @Summary Get Item Attachment
// @Tags Items Attachments
// @Produce application/octet-stream
// @Param id path string true "Item ID"
// @Param attachment_id path string true "Attachment ID"
// @Success 200 {object} ItemAttachmentToken
// @Router /v1/items/{id}/attachments/{attachment_id} [GET]
// @Security Bearer
// @Summary Get Item Attachment
// @Tags Items Attachments
// @Produce application/octet-stream
// @Param id path string true "Item ID"
// @Param attachment_id path string true "Attachment ID"
// @Success 200 {object} ItemAttachmentToken
// @Router /v1/items/{id}/attachments/{attachment_id} [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleItemAttachmentGet() errchain.HandlerFunc {
return ctrl.handleItemAttachmentsHandler
}
// HandleItemAttachmentDelete godocs
//
// @Summary Delete Item Attachment
// @Tags Items Attachments
// @Param id path string true "Item ID"
// @Param attachment_id path string true "Attachment ID"
// @Success 204
// @Router /v1/items/{id}/attachments/{attachment_id} [DELETE]
// @Security Bearer
// @Summary Delete Item Attachment
// @Tags Items Attachments
// @Param id path string true "Item ID"
// @Param attachment_id path string true "Attachment ID"
// @Success 204
// @Router /v1/items/{id}/attachments/{attachment_id} [DELETE]
// @Security Bearer
func (ctrl *V1Controller) HandleItemAttachmentDelete() errchain.HandlerFunc {
return ctrl.handleItemAttachmentsHandler
}
// HandleItemAttachmentUpdate godocs
//
// @Summary Update Item Attachment
// @Tags Items Attachments
// @Param id path string true "Item ID"
// @Param attachment_id path string true "Attachment ID"
// @Param payload body repo.ItemAttachmentUpdate true "Attachment Update"
// @Success 200 {object} repo.ItemOut
// @Router /v1/items/{id}/attachments/{attachment_id} [PUT]
// @Security Bearer
// @Summary Update Item Attachment
// @Tags Items Attachments
// @Param id path string true "Item ID"
// @Param attachment_id path string true "Attachment ID"
// @Param payload body repo.ItemAttachmentUpdate true "Attachment Update"
// @Success 200 {object} repo.ItemOut
// @Router /v1/items/{id}/attachments/{attachment_id} [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleItemAttachmentUpdate() errchain.HandlerFunc {
return ctrl.handleItemAttachmentsHandler
}

View File

@@ -1,136 +0,0 @@
package v1
import (
"fmt"
"net/http"
"strconv"
"strings"
"github.com/go-chi/chi/v5"
"github.com/hay-kot/httpkit/errchain"
"github.com/sysadminsmedia/homebox/backend/internal/core/services"
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
"github.com/sysadminsmedia/homebox/backend/internal/sys/validate"
"github.com/sysadminsmedia/homebox/backend/internal/web/adapters"
"github.com/sysadminsmedia/homebox/backend/pkgs/labelmaker"
)
func generateOrPrint(ctrl *V1Controller, w http.ResponseWriter, r *http.Request, title string, description string, url string) error {
params := labelmaker.NewGenerateParams(int(ctrl.config.LabelMaker.Width), int(ctrl.config.LabelMaker.Height), int(ctrl.config.LabelMaker.Margin), int(ctrl.config.LabelMaker.Padding), ctrl.config.LabelMaker.FontSize, title, description, url, ctrl.config.LabelMaker.DynamicLength, ctrl.config.LabelMaker.AdditionalInformation)
print := queryBool(r.URL.Query().Get("print"))
if print {
err := labelmaker.PrintLabel(ctrl.config, &params)
if err != nil {
return err
}
_, err = w.Write([]byte("Printed!"))
return err
} else {
return labelmaker.GenerateLabel(w, &params)
}
}
// HandleGetLocationLabel godoc
//
// @Summary Get Location label
// @Tags Locations
// @Produce json
// @Param id path string true "Location ID"
// @Param print query bool false "Print this label, defaults to false"
// @Success 200 {string} string "image/png"
// @Router /v1/labelmaker/location/{id} [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleGetLocationLabel() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
ID, err := adapters.RouteUUID(r, "id")
if err != nil {
return err
}
auth := services.NewContext(r.Context())
location, err := ctrl.repo.Locations.GetOneByGroup(auth, auth.GID, ID)
if err != nil {
return err
}
hbURL := GetHBURL(r.Header.Get("Referer"), ctrl.url)
return generateOrPrint(ctrl, w, r, location.Name, "Homebox Location", fmt.Sprintf("%s/location/%s", hbURL, location.ID))
}
}
// HandleGetItemLabel godoc
//
// @Summary Get Item label
// @Tags Items
// @Produce json
// @Param id path string true "Item ID"
// @Param print query bool false "Print this label, defaults to false"
// @Success 200 {string} string "image/png"
// @Router /v1/labelmaker/item/{id} [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleGetItemLabel() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
ID, err := adapters.RouteUUID(r, "id")
if err != nil {
return err
}
auth := services.NewContext(r.Context())
item, err := ctrl.repo.Items.GetOneByGroup(auth, auth.GID, ID)
if err != nil {
return err
}
description := ""
if item.Location != nil {
description += fmt.Sprintf("\nLocation: %s", item.Location.Name)
}
hbURL := GetHBURL(r.Header.Get("Referer"), ctrl.url)
return generateOrPrint(ctrl, w, r, item.Name, description, fmt.Sprintf("%s/item/%s", hbURL, item.ID))
}
}
// HandleGetAssetLabel godoc
//
// @Summary Get Asset label
// @Tags Items
// @Produce json
// @Param id path string true "Asset ID"
// @Param print query bool false "Print this label, defaults to false"
// @Success 200 {string} string "image/png"
// @Router /v1/labelmaker/assets/{id} [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleGetAssetLabel() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
assetIDParam := chi.URLParam(r, "id")
assetIDParam = strings.ReplaceAll(assetIDParam, "-", "")
assetID, err := strconv.ParseInt(assetIDParam, 10, 64)
if err != nil {
return err
}
auth := services.NewContext(r.Context())
item, err := ctrl.repo.Items.QueryByAssetID(auth, auth.GID, repo.AssetID(assetID), 0, 1)
if err != nil {
return err
}
if len(item.Items) == 0 {
return validate.NewRequestError(fmt.Errorf("failed to find asset id"), http.StatusNotFound)
}
description := item.Items[0].Name
if item.Items[0].Location != nil {
description += fmt.Sprintf("\nLocation: %s", item.Items[0].Location.Name)
}
hbURL := GetHBURL(r.Header.Get("Referer"), ctrl.url)
return generateOrPrint(ctrl, w, r, item.Items[0].AssetID.String(), description, fmt.Sprintf("%s/a/%s", hbURL, item.Items[0].AssetID.String()))
}
}

View File

@@ -12,12 +12,12 @@ import (
// HandleLabelsGetAll godoc
//
// @Summary Get All Labels
// @Tags Labels
// @Produce json
// @Success 200 {object} []repo.LabelOut
// @Router /v1/labels [GET]
// @Security Bearer
// @Summary Get All Labels
// @Tags Labels
// @Produce json
// @Success 200 {object} []repo.LabelOut
// @Router /v1/labels [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleLabelsGetAll() errchain.HandlerFunc {
fn := func(r *http.Request) ([]repo.LabelSummary, error) {
auth := services.NewContext(r.Context())
@@ -29,13 +29,13 @@ func (ctrl *V1Controller) HandleLabelsGetAll() errchain.HandlerFunc {
// HandleLabelsCreate godoc
//
// @Summary Create Label
// @Tags Labels
// @Produce json
// @Param payload body repo.LabelCreate true "Label Data"
// @Success 200 {object} repo.LabelSummary
// @Router /v1/labels [POST]
// @Security Bearer
// @Summary Create Label
// @Tags Labels
// @Produce json
// @Param payload body repo.LabelCreate true "Label Data"
// @Success 200 {object} repo.LabelSummary
// @Router /v1/labels [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleLabelsCreate() errchain.HandlerFunc {
fn := func(r *http.Request, data repo.LabelCreate) (repo.LabelOut, error) {
auth := services.NewContext(r.Context())
@@ -47,13 +47,13 @@ func (ctrl *V1Controller) HandleLabelsCreate() errchain.HandlerFunc {
// HandleLabelDelete godocs
//
// @Summary Delete Label
// @Tags Labels
// @Produce json
// @Param id path string true "Label ID"
// @Success 204
// @Router /v1/labels/{id} [DELETE]
// @Security Bearer
// @Summary Delete Label
// @Tags Labels
// @Produce json
// @Param id path string true "Label ID"
// @Success 204
// @Router /v1/labels/{id} [DELETE]
// @Security Bearer
func (ctrl *V1Controller) HandleLabelDelete() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID) (any, error) {
auth := services.NewContext(r.Context())
@@ -66,13 +66,13 @@ func (ctrl *V1Controller) HandleLabelDelete() errchain.HandlerFunc {
// HandleLabelGet godocs
//
// @Summary Get Label
// @Tags Labels
// @Produce json
// @Param id path string true "Label ID"
// @Success 200 {object} repo.LabelOut
// @Router /v1/labels/{id} [GET]
// @Security Bearer
// @Summary Get Label
// @Tags Labels
// @Produce json
// @Param id path string true "Label ID"
// @Success 200 {object} repo.LabelOut
// @Router /v1/labels/{id} [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleLabelGet() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID) (repo.LabelOut, error) {
auth := services.NewContext(r.Context())
@@ -84,13 +84,13 @@ func (ctrl *V1Controller) HandleLabelGet() errchain.HandlerFunc {
// HandleLabelUpdate godocs
//
// @Summary Update Label
// @Tags Labels
// @Produce json
// @Param id path string true "Label ID"
// @Success 200 {object} repo.LabelOut
// @Router /v1/labels/{id} [PUT]
// @Security Bearer
// @Summary Update Label
// @Tags Labels
// @Produce json
// @Param id path string true "Label ID"
// @Success 200 {object} repo.LabelOut
// @Router /v1/labels/{id} [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleLabelUpdate() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID, data repo.LabelUpdate) (repo.LabelOut, error) {
auth := services.NewContext(r.Context())

View File

@@ -0,0 +1,51 @@
package v1
import (
"encoding/json"
"net/http"
"github.com/google/uuid"
"github.com/hay-kot/httpkit/errchain"
"github.com/sysadminsmedia/homebox/backend/internal/core/services"
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
"github.com/sysadminsmedia/homebox/backend/internal/web/adapters"
)
type Locales struct {
Locales []string `json:"locales"`
}
// HandleLocalesGetAll godoc
//
// @Summary Get All Locales
// @Tags Locales
// @Produce json
// @Success 200 {object} []Locales
// @Router /v1/locales [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleLocalesGetAll() errchain.HandlerFunc {
fn := func(r *http.Request) ([]Locales, error) {
// TODO: get a list of locales from files
return []Locales{}, nil
}
return adapters.Command(fn, http.StatusOK)
}
// HandleLocalesGet godoc
//
// @Summary Get Locale
// @Tags Locales
// @Produce json
// @Param id path string true "Locale ID"
// @Success 200 {object} interface{}
// @Router /v1/locales/{id} [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleLocalesGet() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID) (interface{}, error) {
// TODO: get the current locale
return interface{}, nil
}
return adapters.CommandID("id", fn, http.StatusOK)
}

View File

@@ -14,13 +14,13 @@ import (
// HandleLocationTreeQuery godoc
//
// @Summary Get Locations Tree
// @Tags Locations
// @Produce json
// @Param withItems query bool false "include items in response tree"
// @Success 200 {object} []repo.TreeItem
// @Router /v1/locations/tree [GET]
// @Security Bearer
// @Summary Get Locations Tree
// @Tags Locations
// @Produce json
// @Param withItems query bool false "include items in response tree"
// @Success 200 {object} []repo.TreeItem
// @Router /v1/locations/tree [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleLocationTreeQuery() errchain.HandlerFunc {
fn := func(r *http.Request, query repo.TreeQuery) ([]repo.TreeItem, error) {
auth := services.NewContext(r.Context())
@@ -32,13 +32,13 @@ func (ctrl *V1Controller) HandleLocationTreeQuery() errchain.HandlerFunc {
// HandleLocationGetAll godoc
//
// @Summary Get All Locations
// @Tags Locations
// @Produce json
// @Param filterChildren query bool false "Filter locations with parents"
// @Success 200 {object} []repo.LocationOutCount
// @Router /v1/locations [GET]
// @Security Bearer
// @Summary Get All Locations
// @Tags Locations
// @Produce json
// @Param filterChildren query bool false "Filter locations with parents"
// @Success 200 {object} []repo.LocationOutCount
// @Router /v1/locations [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleLocationGetAll() errchain.HandlerFunc {
fn := func(r *http.Request, q repo.LocationQuery) ([]repo.LocationOutCount, error) {
auth := services.NewContext(r.Context())
@@ -50,13 +50,13 @@ func (ctrl *V1Controller) HandleLocationGetAll() errchain.HandlerFunc {
// HandleLocationCreate godoc
//
// @Summary Create Location
// @Tags Locations
// @Produce json
// @Param payload body repo.LocationCreate true "Location Data"
// @Success 200 {object} repo.LocationSummary
// @Router /v1/locations [POST]
// @Security Bearer
// @Summary Create Location
// @Tags Locations
// @Produce json
// @Param payload body repo.LocationCreate true "Location Data"
// @Success 200 {object} repo.LocationSummary
// @Router /v1/locations [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleLocationCreate() errchain.HandlerFunc {
fn := func(r *http.Request, createData repo.LocationCreate) (repo.LocationOut, error) {
auth := services.NewContext(r.Context())
@@ -68,13 +68,13 @@ func (ctrl *V1Controller) HandleLocationCreate() errchain.HandlerFunc {
// HandleLocationDelete godoc
//
// @Summary Delete Location
// @Tags Locations
// @Produce json
// @Param id path string true "Location ID"
// @Success 204
// @Router /v1/locations/{id} [DELETE]
// @Security Bearer
// @Summary Delete Location
// @Tags Locations
// @Produce json
// @Param id path string true "Location ID"
// @Success 204
// @Router /v1/locations/{id} [DELETE]
// @Security Bearer
func (ctrl *V1Controller) HandleLocationDelete() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID) (any, error) {
auth := services.NewContext(r.Context())
@@ -124,13 +124,13 @@ func (ctrl *V1Controller) GetLocationWithPrice(auth context.Context, gid uuid.UU
// HandleLocationGet godoc
//
// @Summary Get Location
// @Tags Locations
// @Produce json
// @Param id path string true "Location ID"
// @Success 200 {object} repo.LocationOut
// @Router /v1/locations/{id} [GET]
// @Security Bearer
// @Summary Get Location
// @Tags Locations
// @Produce json
// @Param id path string true "Location ID"
// @Success 200 {object} repo.LocationOut
// @Router /v1/locations/{id} [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleLocationGet() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID) (repo.LocationOut, error) {
auth := services.NewContext(r.Context())
@@ -144,14 +144,14 @@ func (ctrl *V1Controller) HandleLocationGet() errchain.HandlerFunc {
// HandleLocationUpdate godoc
//
// @Summary Update Location
// @Tags Locations
// @Produce json
// @Param id path string true "Location ID"
// @Param payload body repo.LocationUpdate true "Location Data"
// @Success 200 {object} repo.LocationOut
// @Router /v1/locations/{id} [PUT]
// @Security Bearer
// @Summary Update Location
// @Tags Locations
// @Produce json
// @Param id path string true "Location ID"
// @Param payload body repo.LocationUpdate true "Location Data"
// @Success 200 {object} repo.LocationOut
// @Router /v1/locations/{id} [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleLocationUpdate() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID, body repo.LocationUpdate) (repo.LocationOut, error) {
auth := services.NewContext(r.Context())

View File

@@ -15,7 +15,6 @@ import (
// @Summary Get Maintenance Log
// @Tags Item Maintenance
// @Produce json
// @Param id path string true "Item ID"
// @Param filters query repo.MaintenanceFilters false "which maintenance to retrieve"
// @Success 200 {array} repo.MaintenanceEntryWithDetails[]
// @Router /v1/items/{id}/maintenance [GET]
@@ -34,7 +33,6 @@ func (ctrl *V1Controller) HandleMaintenanceLogGet() errchain.HandlerFunc {
// @Summary Create Maintenance Entry
// @Tags Item Maintenance
// @Produce json
// @Param id path string true "Item ID"
// @Param payload body repo.MaintenanceEntryCreate true "Entry Data"
// @Success 201 {object} repo.MaintenanceEntry
// @Router /v1/items/{id}/maintenance [POST]

View File

@@ -12,13 +12,13 @@ import (
// HandleMaintenanceGetAll godoc
//
// @Summary Query All Maintenance
// @Tags Maintenance
// @Produce json
// @Param filters query repo.MaintenanceFilters false "which maintenance to retrieve"
// @Success 200 {array} repo.MaintenanceEntryWithDetails[]
// @Router /v1/maintenance [GET]
// @Security Bearer
// @Summary Query All Maintenance
// @Tags Maintenance
// @Produce json
// @Param filters query repo.MaintenanceFilters false "which maintenance to retrieve"
// @Success 200 {array} repo.MaintenanceEntryWithDetails[]
// @Router /v1/maintenance [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleMaintenanceGetAll() errchain.HandlerFunc {
fn := func(r *http.Request, filters repo.MaintenanceFilters) ([]repo.MaintenanceEntryWithDetails, error) {
auth := services.NewContext(r.Context())
@@ -33,7 +33,6 @@ func (ctrl *V1Controller) HandleMaintenanceGetAll() errchain.HandlerFunc {
// @Summary Update Maintenance Entry
// @Tags Maintenance
// @Produce json
// @Param id path string true "Maintenance ID"
// @Param payload body repo.MaintenanceEntryUpdate true "Entry Data"
// @Success 200 {object} repo.MaintenanceEntry
// @Router /v1/maintenance/{id} [PUT]
@@ -52,7 +51,6 @@ func (ctrl *V1Controller) HandleMaintenanceEntryUpdate() errchain.HandlerFunc {
// @Summary Delete Maintenance Entry
// @Tags Maintenance
// @Produce json
// @Param id path string true "Maintenance ID"
// @Success 204
// @Router /v1/maintenance/{id} [DELETE]
// @Security Bearer

View File

@@ -13,12 +13,12 @@ import (
// HandleGetUserNotifiers godoc
//
// @Summary Get Notifiers
// @Tags Notifiers
// @Produce json
// @Success 200 {object} []repo.NotifierOut
// @Router /v1/notifiers [GET]
// @Security Bearer
// @Summary Get Notifiers
// @Tags Notifiers
// @Produce json
// @Success 200 {object} []repo.NotifierOut
// @Router /v1/notifiers [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleGetUserNotifiers() errchain.HandlerFunc {
fn := func(r *http.Request, _ struct{}) ([]repo.NotifierOut, error) {
user := services.UseUserCtx(r.Context())
@@ -30,13 +30,13 @@ func (ctrl *V1Controller) HandleGetUserNotifiers() errchain.HandlerFunc {
// HandleCreateNotifier godoc
//
// @Summary Create Notifier
// @Tags Notifiers
// @Produce json
// @Param payload body repo.NotifierCreate true "Notifier Data"
// @Success 200 {object} repo.NotifierOut
// @Router /v1/notifiers [POST]
// @Security Bearer
// @Summary Create Notifier
// @Tags Notifiers
// @Produce json
// @Param payload body repo.NotifierCreate true "Notifier Data"
// @Success 200 {object} repo.NotifierOut
// @Router /v1/notifiers [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleCreateNotifier() errchain.HandlerFunc {
fn := func(r *http.Request, in repo.NotifierCreate) (repo.NotifierOut, error) {
auth := services.NewContext(r.Context())
@@ -48,12 +48,12 @@ func (ctrl *V1Controller) HandleCreateNotifier() errchain.HandlerFunc {
// HandleDeleteNotifier godocs
//
// @Summary Delete a Notifier
// @Tags Notifiers
// @Param id path string true "Notifier ID"
// @Success 204
// @Router /v1/notifiers/{id} [DELETE]
// @Security Bearer
// @Summary Delete a Notifier
// @Tags Notifiers
// @Param id path string true "Notifier ID"
// @Success 204
// @Router /v1/notifiers/{id} [DELETE]
// @Security Bearer
func (ctrl *V1Controller) HandleDeleteNotifier() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID) (any, error) {
auth := services.NewContext(r.Context())
@@ -65,13 +65,13 @@ func (ctrl *V1Controller) HandleDeleteNotifier() errchain.HandlerFunc {
// HandleUpdateNotifier godocs
//
// @Summary Update Notifier
// @Tags Notifiers
// @Param id path string true "Notifier ID"
// @Param payload body repo.NotifierUpdate true "Notifier Data"
// @Success 200 {object} repo.NotifierOut
// @Router /v1/notifiers/{id} [PUT]
// @Security Bearer
// @Summary Update Notifier
// @Tags Notifiers
// @Param id path string true "Notifier ID"
// @Param payload body repo.NotifierUpdate true "Notifier Data"
// @Success 200 {object} repo.NotifierOut
// @Router /v1/notifiers/{id} [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleUpdateNotifier() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID, in repo.NotifierUpdate) (repo.NotifierOut, error) {
auth := services.NewContext(r.Context())
@@ -86,6 +86,7 @@ func (ctrl *V1Controller) HandleUpdateNotifier() errchain.HandlerFunc {
// @Summary Test Notifier
// @Tags Notifiers
// @Produce json
// @Param id path string true "Notifier ID"
// @Param url query string true "URL"
// @Success 204
// @Router /v1/notifiers/test [POST]

View File

@@ -20,13 +20,13 @@ var qrcodeLogo []byte
// HandleGenerateQRCode godoc
//
// @Summary Create QR Code
// @Tags Items
// @Produce json
// @Param data query string false "data to be encoded into qrcode"
// @Success 200 {string} string "image/jpeg"
// @Router /v1/qrcode [GET]
// @Security Bearer
// @Summary Create QR Code
// @Tags Items
// @Produce json
// @Param data query string false "data to be encoded into qrcode"
// @Success 200 {string} string "image/jpeg"
// @Router /v1/qrcode [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleGenerateQRCode() errchain.HandlerFunc {
type query struct {
// 4,296 characters is the maximum length of a QR code

View File

@@ -8,12 +8,12 @@ import (
// HandleBillOfMaterialsExport godoc
//
// @Summary Export Bill of Materials
// @Tags Reporting
// @Produce json
// @Success 200 {string} string "text/csv"
// @Router /v1/reporting/bill-of-materials [GET]
// @Security Bearer
// @Summary Export Bill of Materials
// @Tags Reporting
// @Produce json
// @Success 200 {string} string "text/csv"
// @Router /v1/reporting/bill-of-materials [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleBillOfMaterialsExport() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
actor := services.UseUserCtx(r.Context())

View File

@@ -14,12 +14,12 @@ import (
// HandleGroupStatisticsLocations godoc
//
// @Summary Get Location Statistics
// @Tags Statistics
// @Produce json
// @Success 200 {object} []repo.TotalsByOrganizer
// @Router /v1/groups/statistics/locations [GET]
// @Security Bearer
// @Summary Get Location Statistics
// @Tags Statistics
// @Produce json
// @Success 200 {object} []repo.TotalsByOrganizer
// @Router /v1/groups/statistics/locations [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleGroupStatisticsLocations() errchain.HandlerFunc {
fn := func(r *http.Request) ([]repo.TotalsByOrganizer, error) {
auth := services.NewContext(r.Context())
@@ -31,12 +31,12 @@ func (ctrl *V1Controller) HandleGroupStatisticsLocations() errchain.HandlerFunc
// HandleGroupStatisticsLabels godoc
//
// @Summary Get Label Statistics
// @Tags Statistics
// @Produce json
// @Success 200 {object} []repo.TotalsByOrganizer
// @Router /v1/groups/statistics/labels [GET]
// @Security Bearer
// @Summary Get Label Statistics
// @Tags Statistics
// @Produce json
// @Success 200 {object} []repo.TotalsByOrganizer
// @Router /v1/groups/statistics/labels [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleGroupStatisticsLabels() errchain.HandlerFunc {
fn := func(r *http.Request) ([]repo.TotalsByOrganizer, error) {
auth := services.NewContext(r.Context())
@@ -48,12 +48,12 @@ func (ctrl *V1Controller) HandleGroupStatisticsLabels() errchain.HandlerFunc {
// HandleGroupStatistics godoc
//
// @Summary Get Group Statistics
// @Tags Statistics
// @Produce json
// @Success 200 {object} repo.GroupStatistics
// @Router /v1/groups/statistics [GET]
// @Security Bearer
// @Summary Get Group Statistics
// @Tags Statistics
// @Produce json
// @Success 200 {object} repo.GroupStatistics
// @Router /v1/groups/statistics [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleGroupStatistics() errchain.HandlerFunc {
fn := func(r *http.Request) (repo.GroupStatistics, error) {
auth := services.NewContext(r.Context())
@@ -65,14 +65,14 @@ func (ctrl *V1Controller) HandleGroupStatistics() errchain.HandlerFunc {
// HandleGroupStatisticsPriceOverTime godoc
//
// @Summary Get Purchase Price Statistics
// @Tags Statistics
// @Produce json
// @Success 200 {object} repo.ValueOverTime
// @Param start query string false "start date"
// @Param end query string false "end date"
// @Router /v1/groups/statistics/purchase-price [GET]
// @Security Bearer
// @Summary Get Purchase Price Statistics
// @Tags Statistics
// @Produce json
// @Success 200 {object} repo.ValueOverTime
// @Param start query string false "start date"
// @Param end query string false "end date"
// @Router /v1/groups/statistics/purchase-price [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleGroupStatisticsPriceOverTime() errchain.HandlerFunc {
parseDate := func(datestr string, defaultDate time.Time) (time.Time, error) {
if datestr == "" {

View File

@@ -15,12 +15,12 @@ import (
// HandleUserRegistration godoc
//
// @Summary Register New User
// @Tags User
// @Produce json
// @Param payload body services.UserRegistration true "User Data"
// @Success 204
// @Router /v1/users/register [Post]
// @Summary Register New User
// @Tags User
// @Produce json
// @Param payload body services.UserRegistration true "User Data"
// @Success 204
// @Router /v1/users/register [Post]
func (ctrl *V1Controller) HandleUserRegistration() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
regData := services.UserRegistration{}
@@ -46,12 +46,12 @@ func (ctrl *V1Controller) HandleUserRegistration() errchain.HandlerFunc {
// HandleUserSelf godoc
//
// @Summary Get User Self
// @Tags User
// @Produce json
// @Success 200 {object} Wrapped{item=repo.UserOut}
// @Router /v1/users/self [GET]
// @Security Bearer
// @Summary Get User Self
// @Tags User
// @Produce json
// @Success 200 {object} Wrapped{item=repo.UserOut}
// @Router /v1/users/self [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleUserSelf() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
token := services.UseTokenCtx(r.Context())
@@ -67,13 +67,13 @@ func (ctrl *V1Controller) HandleUserSelf() errchain.HandlerFunc {
// HandleUserSelfUpdate godoc
//
// @Summary Update Account
// @Tags User
// @Produce json
// @Param payload body repo.UserUpdate true "User Data"
// @Success 200 {object} Wrapped{item=repo.UserUpdate}
// @Router /v1/users/self [PUT]
// @Security Bearer
// @Summary Update Account
// @Tags User
// @Produce json
// @Param payload body repo.UserUpdate true "User Data"
// @Success 200 {object} Wrapped{item=repo.UserUpdate}
// @Router /v1/users/self [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleUserSelfUpdate() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
updateData := repo.UserUpdate{}
@@ -94,12 +94,12 @@ func (ctrl *V1Controller) HandleUserSelfUpdate() errchain.HandlerFunc {
// HandleUserSelfDelete godoc
//
// @Summary Delete Account
// @Tags User
// @Produce json
// @Success 204
// @Router /v1/users/self [DELETE]
// @Security Bearer
// @Summary Delete Account
// @Tags User
// @Produce json
// @Success 204
// @Router /v1/users/self [DELETE]
// @Security Bearer
func (ctrl *V1Controller) HandleUserSelfDelete() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
if ctrl.isDemo {
@@ -124,12 +124,12 @@ type (
// HandleUserSelfChangePassword godoc
//
// @Summary Change Password
// @Tags User
// @Success 204
// @Param payload body ChangePassword true "Password Payload"
// @Router /v1/users/change-password [PUT]
// @Security Bearer
// @Summary Change Password
// @Tags User
// @Success 204
// @Param payload body ChangePassword true "Password Payload"
// @Router /v1/users/change-password [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleUserSelfChangePassword() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
if ctrl.isDemo {

View File

@@ -4,11 +4,9 @@ import (
"bytes"
"context"
"fmt"
"github.com/google/uuid"
"net/http"
"os"
"path/filepath"
"strings"
"time"
atlas "ariga.io/atlas/sql/migrate"
@@ -30,7 +28,6 @@ import (
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
"github.com/sysadminsmedia/homebox/backend/internal/web/mid"
_ "github.com/lib/pq"
_ "github.com/sysadminsmedia/homebox/backend/pkgs/cgofreesqlite"
)
@@ -49,19 +46,6 @@ func build() string {
return fmt.Sprintf("%s, commit %s, built at %s", version, short, buildTime)
}
func validatePostgresSSLMode(sslMode string) bool {
validModes := map[string]bool{
"": true,
"disable": true,
"allow": true,
"prefer": true,
"require": true,
"verify-ca": true,
"verify-full": true,
}
return validModes[strings.ToLower(strings.TrimSpace(sslMode))]
}
// @title Homebox API
// @version 1.0
// @description Track, Manage, and Organize your Things.
@@ -71,7 +55,6 @@ func validatePostgresSSLMode(sslMode string) bool {
// @in header
// @name Authorization
// @description "Type 'Bearer TOKEN' to correctly set the API Key"
func main() {
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
@@ -97,32 +80,13 @@ func run(cfg *config.Config) error {
log.Fatal().Err(err).Msg("failed to create data directory")
}
if strings.ToLower(cfg.Database.Driver) == "postgres" {
if !validatePostgresSSLMode(cfg.Database.SslMode) {
log.Fatal().Str("sslmode", cfg.Database.SslMode).Msg("invalid sslmode")
}
}
// Set up the database URL based on the driver because for some reason a common URL format is not used
databaseURL := ""
switch strings.ToLower(cfg.Database.Driver) {
case "sqlite3":
databaseURL = cfg.Database.SqlitePath
case "postgres":
databaseURL = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", cfg.Database.Host, cfg.Database.Port, cfg.Database.Username, cfg.Database.Password, cfg.Database.Database, cfg.Database.SslMode)
default:
log.Fatal().Str("driver", cfg.Database.Driver).Msg("unsupported database driver")
}
c, err := ent.Open(strings.ToLower(cfg.Database.Driver), databaseURL)
c, err := ent.Open("sqlite3", cfg.Storage.SqliteURL)
if err != nil {
log.Fatal().
Err(err).
Str("driver", strings.ToLower(cfg.Database.Driver)).
Str("host", cfg.Database.Host).
Str("port", cfg.Database.Port).
Str("database", cfg.Database.Database).
Msg("failed opening connection to {driver} database at {host}:{port}/{database}")
Str("driver", "sqlite").
Str("url", cfg.Storage.SqliteURL).
Msg("failed opening connection to sqlite")
}
defer func(c *ent.Client) {
err := c.Close()
@@ -131,14 +95,9 @@ func run(cfg *config.Config) error {
}
}(c)
// Always create a random temporary directory for migrations
tempUUID, err := uuid.NewUUID()
if err != nil {
return err
}
temp := filepath.Join(os.TempDir(), fmt.Sprintf("homebox-%s", tempUUID.String()))
temp := filepath.Join(os.TempDir(), "migrations")
err = migrations.Write(temp, cfg.Database.Driver)
err = migrations.Write(temp)
if err != nil {
return err
}
@@ -158,18 +117,17 @@ func run(cfg *config.Config) error {
if err != nil {
log.Error().
Err(err).
Str("driver", cfg.Database.Driver).
Str("url", databaseURL).
Str("driver", "sqlite").
Str("url", cfg.Storage.SqliteURL).
Msg("failed creating schema resources")
return err
}
defer func() {
err := os.RemoveAll(temp)
if err != nil {
log.Error().Err(err).Msg("failed to remove temporary directory for database migrations")
}
}()
err = os.RemoveAll(temp)
if err != nil {
log.Error().Err(err).Msg("failed to remove temporary directory for database migrations")
return err
}
collectFuncs := []currencies.CollectorFunc{
currencies.CollectDefaults(),
@@ -296,14 +254,15 @@ func run(cfg *config.Config) error {
}
}))
if cfg.Options.GithubReleaseCheck {
runner.AddPlugin(NewTask("get-latest-github-release", time.Hour, func(ctx context.Context) {
log.Debug().Msg("running get latest github release")
err := app.services.BackgroundService.GetLatestGithubRelease(context.Background())
// TODO: read from env var0 GOOS=linux go build
if true {
runner.AddPlugin(NewTask("locale-update", time.Duration(24)*time.Hour, func(ctx context.Context) {
log.Debug().Msg("running locale update")
err := app.services.BackgroundService.UpdateLocales(ctx)
if err != nil {
log.Error().
Err(err).
Msg("failed to get latest github release")
Msg("failed to update locales")
}
}))
}

View File

@@ -52,7 +52,6 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
a.services,
a.repos,
a.bus,
a.conf,
v1.WithMaxUploadSize(a.conf.Web.MaxUploadSize),
v1.WithRegistration(a.conf.Options.AllowRegistration),
v1.WithDemoStatus(a.conf.Demo), // Disable Password Change in Demo Mode
@@ -162,11 +161,6 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentGet(), assetMW...),
)
// Labelmaker
r.Get("/labelmaker/location/{id}", chain.ToHandlerFunc(v1Ctrl.HandleGetLocationLabel(), userMW...))
r.Get("/labelmaker/item/{id}", chain.ToHandlerFunc(v1Ctrl.HandleGetItemLabel(), userMW...))
r.Get("/labelmaker/asset/{id}", chain.ToHandlerFunc(v1Ctrl.HandleGetAssetLabel(), userMW...))
// Reporting Services
r.Get("/reporting/bill-of-materials", chain.ToHandlerFunc(v1Ctrl.HandleBillOfMaterialsExport(), userMW...))

View File

@@ -566,9 +566,6 @@ const docTemplate = `{
"Bearer": []
}
],
"consumes": [
"multipart/form-data"
],
"produces": [
"application/json"
],
@@ -740,9 +737,6 @@ const docTemplate = `{
"Bearer": []
}
],
"consumes": [
"multipart/form-data"
],
"produces": [
"application/json"
],
@@ -927,13 +921,6 @@ const docTemplate = `{
],
"summary": "Get Maintenance Log",
"parameters": [
{
"type": "string",
"description": "Item ID",
"name": "id",
"in": "path",
"required": true
},
{
"enum": [
"scheduled",
@@ -976,13 +963,6 @@ const docTemplate = `{
],
"summary": "Create Maintenance Entry",
"parameters": [
{
"type": "string",
"description": "Item ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Entry Data",
"name": "payload",
@@ -1039,123 +1019,6 @@ const docTemplate = `{
}
}
},
"/v1/labelmaker/assets/{id}": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Items"
],
"summary": "Get Asset label",
"parameters": [
{
"type": "string",
"description": "Asset ID",
"name": "id",
"in": "path",
"required": true
},
{
"type": "boolean",
"description": "Print this label, defaults to false",
"name": "print",
"in": "query"
}
],
"responses": {
"200": {
"description": "image/png",
"schema": {
"type": "string"
}
}
}
}
},
"/v1/labelmaker/item/{id}": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Items"
],
"summary": "Get Item label",
"parameters": [
{
"type": "string",
"description": "Item ID",
"name": "id",
"in": "path",
"required": true
},
{
"type": "boolean",
"description": "Print this label, defaults to false",
"name": "print",
"in": "query"
}
],
"responses": {
"200": {
"description": "image/png",
"schema": {
"type": "string"
}
}
}
}
},
"/v1/labelmaker/location/{id}": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Locations"
],
"summary": "Get Location label",
"parameters": [
{
"type": "string",
"description": "Location ID",
"name": "id",
"in": "path",
"required": true
},
{
"type": "boolean",
"description": "Print this label, defaults to false",
"name": "print",
"in": "query"
}
],
"responses": {
"200": {
"description": "image/png",
"schema": {
"type": "string"
}
}
}
}
},
"/v1/labels": {
"get": {
"security": [
@@ -1571,13 +1434,6 @@ const docTemplate = `{
],
"summary": "Update Maintenance Entry",
"parameters": [
{
"type": "string",
"description": "Maintenance ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Entry Data",
"name": "payload",
@@ -1610,15 +1466,6 @@ const docTemplate = `{
"Maintenance"
],
"summary": "Delete Maintenance Entry",
"parameters": [
{
"type": "string",
"description": "Maintenance ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
@@ -1701,6 +1548,13 @@ const docTemplate = `{
],
"summary": "Test Notifier",
"parameters": [
{
"type": "string",
"description": "Notifier ID",
"name": "id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "URL",
@@ -1898,6 +1752,20 @@ const docTemplate = `{
],
"summary": "User Login",
"parameters": [
{
"type": "string",
"example": "admin@admin.com",
"description": "string",
"name": "username",
"in": "formData"
},
{
"type": "string",
"example": "admin",
"description": "string",
"name": "password",
"in": "formData"
},
{
"description": "Login Data",
"name": "payload",
@@ -2374,9 +2242,6 @@ const docTemplate = `{
"soldTo": {
"type": "string"
},
"syncChildItemsLocations": {
"type": "boolean"
},
"updatedAt": {
"type": "string"
},
@@ -2421,10 +2286,6 @@ const docTemplate = `{
"archived": {
"type": "boolean"
},
"assetId": {
"type": "string",
"example": "0"
},
"createdAt": {
"type": "string"
},
@@ -2579,9 +2440,6 @@ const docTemplate = `{
"type": "string",
"maxLength": 255
},
"syncChildItemsLocations": {
"type": "boolean"
},
"warrantyDetails": {
"type": "string"
},
@@ -3067,17 +2925,6 @@ const docTemplate = `{
}
}
},
"services.Latest": {
"type": "object",
"properties": {
"date": {
"type": "string"
},
"version": {
"type": "string"
}
}
},
"services.UserRegistration": {
"type": "object",
"properties": {
@@ -3110,12 +2957,6 @@ const docTemplate = `{
"health": {
"type": "boolean"
},
"labelPrinting": {
"type": "boolean"
},
"latest": {
"$ref": "#/definitions/services.Latest"
},
"message": {
"type": "string"
},
@@ -3205,15 +3046,13 @@ const docTemplate = `{
"type": "object",
"properties": {
"password": {
"type": "string",
"example": "admin"
"type": "string"
},
"stayLoggedIn": {
"type": "boolean"
},
"username": {
"type": "string",
"example": "admin@admin.com"
"type": "string"
}
}
},

View File

@@ -559,9 +559,6 @@
"Bearer": []
}
],
"consumes": [
"multipart/form-data"
],
"produces": [
"application/json"
],
@@ -733,9 +730,6 @@
"Bearer": []
}
],
"consumes": [
"multipart/form-data"
],
"produces": [
"application/json"
],
@@ -920,13 +914,6 @@
],
"summary": "Get Maintenance Log",
"parameters": [
{
"type": "string",
"description": "Item ID",
"name": "id",
"in": "path",
"required": true
},
{
"enum": [
"scheduled",
@@ -969,13 +956,6 @@
],
"summary": "Create Maintenance Entry",
"parameters": [
{
"type": "string",
"description": "Item ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Entry Data",
"name": "payload",
@@ -1032,123 +1012,6 @@
}
}
},
"/v1/labelmaker/assets/{id}": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Items"
],
"summary": "Get Asset label",
"parameters": [
{
"type": "string",
"description": "Asset ID",
"name": "id",
"in": "path",
"required": true
},
{
"type": "boolean",
"description": "Print this label, defaults to false",
"name": "print",
"in": "query"
}
],
"responses": {
"200": {
"description": "image/png",
"schema": {
"type": "string"
}
}
}
}
},
"/v1/labelmaker/item/{id}": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Items"
],
"summary": "Get Item label",
"parameters": [
{
"type": "string",
"description": "Item ID",
"name": "id",
"in": "path",
"required": true
},
{
"type": "boolean",
"description": "Print this label, defaults to false",
"name": "print",
"in": "query"
}
],
"responses": {
"200": {
"description": "image/png",
"schema": {
"type": "string"
}
}
}
}
},
"/v1/labelmaker/location/{id}": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Locations"
],
"summary": "Get Location label",
"parameters": [
{
"type": "string",
"description": "Location ID",
"name": "id",
"in": "path",
"required": true
},
{
"type": "boolean",
"description": "Print this label, defaults to false",
"name": "print",
"in": "query"
}
],
"responses": {
"200": {
"description": "image/png",
"schema": {
"type": "string"
}
}
}
}
},
"/v1/labels": {
"get": {
"security": [
@@ -1564,13 +1427,6 @@
],
"summary": "Update Maintenance Entry",
"parameters": [
{
"type": "string",
"description": "Maintenance ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Entry Data",
"name": "payload",
@@ -1603,15 +1459,6 @@
"Maintenance"
],
"summary": "Delete Maintenance Entry",
"parameters": [
{
"type": "string",
"description": "Maintenance ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
@@ -1694,6 +1541,13 @@
],
"summary": "Test Notifier",
"parameters": [
{
"type": "string",
"description": "Notifier ID",
"name": "id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "URL",
@@ -1891,6 +1745,20 @@
],
"summary": "User Login",
"parameters": [
{
"type": "string",
"example": "admin@admin.com",
"description": "string",
"name": "username",
"in": "formData"
},
{
"type": "string",
"example": "admin",
"description": "string",
"name": "password",
"in": "formData"
},
{
"description": "Login Data",
"name": "payload",
@@ -2367,9 +2235,6 @@
"soldTo": {
"type": "string"
},
"syncChildItemsLocations": {
"type": "boolean"
},
"updatedAt": {
"type": "string"
},
@@ -2414,10 +2279,6 @@
"archived": {
"type": "boolean"
},
"assetId": {
"type": "string",
"example": "0"
},
"createdAt": {
"type": "string"
},
@@ -2572,9 +2433,6 @@
"type": "string",
"maxLength": 255
},
"syncChildItemsLocations": {
"type": "boolean"
},
"warrantyDetails": {
"type": "string"
},
@@ -3060,17 +2918,6 @@
}
}
},
"services.Latest": {
"type": "object",
"properties": {
"date": {
"type": "string"
},
"version": {
"type": "string"
}
}
},
"services.UserRegistration": {
"type": "object",
"properties": {
@@ -3103,12 +2950,6 @@
"health": {
"type": "boolean"
},
"labelPrinting": {
"type": "boolean"
},
"latest": {
"$ref": "#/definitions/services.Latest"
},
"message": {
"type": "string"
},
@@ -3198,15 +3039,13 @@
"type": "object",
"properties": {
"password": {
"type": "string",
"example": "admin"
"type": "string"
},
"stayLoggedIn": {
"type": "boolean"
},
"username": {
"type": "string",
"example": "admin@admin.com"
"type": "string"
}
}
},

View File

@@ -188,8 +188,6 @@ definitions:
type: string
soldTo:
type: string
syncChildItemsLocations:
type: boolean
updatedAt:
type: string
warrantyDetails:
@@ -219,9 +217,6 @@ definitions:
properties:
archived:
type: boolean
assetId:
example: "0"
type: string
createdAt:
type: string
description:
@@ -328,8 +323,6 @@ definitions:
soldTo:
maxLength: 255
type: string
syncChildItemsLocations:
type: boolean
warrantyDetails:
type: string
warrantyExpires:
@@ -654,13 +647,6 @@ definitions:
value:
type: number
type: object
services.Latest:
properties:
date:
type: string
version:
type: string
type: object
services.UserRegistration:
properties:
email:
@@ -682,10 +668,6 @@ definitions:
type: boolean
health:
type: boolean
labelPrinting:
type: boolean
latest:
$ref: '#/definitions/services.Latest'
message:
type: string
title:
@@ -744,12 +726,10 @@ definitions:
v1.LoginForm:
properties:
password:
example: admin
type: string
stayLoggedIn:
type: boolean
username:
example: admin@admin.com
type: string
type: object
v1.TokenResponse:
@@ -1152,8 +1132,6 @@ paths:
- Items
/v1/items/{id}/attachments:
post:
consumes:
- multipart/form-data
parameters:
- description: Item ID
in: path
@@ -1267,11 +1245,6 @@ paths:
/v1/items/{id}/maintenance:
get:
parameters:
- description: Item ID
in: path
name: id
required: true
type: string
- enum:
- scheduled
- completed
@@ -1299,11 +1272,6 @@ paths:
- Item Maintenance
post:
parameters:
- description: Item ID
in: path
name: id
required: true
type: string
- description: Entry Data
in: body
name: payload
@@ -1390,8 +1358,6 @@ paths:
- Items
/v1/items/import:
post:
consumes:
- multipart/form-data
parameters:
- description: Image to upload
in: formData
@@ -1408,78 +1374,6 @@ paths:
summary: Import Items
tags:
- Items
/v1/labelmaker/assets/{id}:
get:
parameters:
- description: Asset ID
in: path
name: id
required: true
type: string
- description: Print this label, defaults to false
in: query
name: print
type: boolean
produces:
- application/json
responses:
"200":
description: image/png
schema:
type: string
security:
- Bearer: []
summary: Get Asset label
tags:
- Items
/v1/labelmaker/item/{id}:
get:
parameters:
- description: Item ID
in: path
name: id
required: true
type: string
- description: Print this label, defaults to false
in: query
name: print
type: boolean
produces:
- application/json
responses:
"200":
description: image/png
schema:
type: string
security:
- Bearer: []
summary: Get Item label
tags:
- Items
/v1/labelmaker/location/{id}:
get:
parameters:
- description: Location ID
in: path
name: id
required: true
type: string
- description: Print this label, defaults to false
in: query
name: print
type: boolean
produces:
- application/json
responses:
"200":
description: image/png
schema:
type: string
security:
- Bearer: []
summary: Get Location label
tags:
- Locations
/v1/labels:
get:
produces:
@@ -1726,12 +1620,6 @@ paths:
- Maintenance
/v1/maintenance/{id}:
delete:
parameters:
- description: Maintenance ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
@@ -1744,11 +1632,6 @@ paths:
- Maintenance
put:
parameters:
- description: Maintenance ID
in: path
name: id
required: true
type: string
- description: Entry Data
in: body
name: payload
@@ -1845,6 +1728,11 @@ paths:
/v1/notifiers/test:
post:
parameters:
- description: Notifier ID
in: path
name: id
required: true
type: string
- description: URL
in: query
name: url
@@ -1928,6 +1816,16 @@ paths:
- application/x-www-form-urlencoded
- application/json
parameters:
- description: string
example: admin@admin.com
in: formData
name: username
type: string
- description: string
example: admin
in: formData
name: password
type: string
- description: Login Data
in: body
name: payload

View File

@@ -3,11 +3,8 @@ package main
import (
"context"
"fmt"
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
"log"
"os"
"path/filepath"
"strings"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/migrate"
@@ -15,29 +12,13 @@ import (
_ "ariga.io/atlas/sql/sqlite"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql/schema"
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
)
func main() {
cfg, err := config.New(build(), "Homebox inventory management system")
if err != nil {
panic(err)
}
sqlDialect := ""
switch strings.ToLower(cfg.Database.Driver) {
case "sqlite3":
sqlDialect = dialect.SQLite
case "postgres":
sqlDialect = dialect.Postgres
default:
log.Fatalf("unsupported database driver: %s", cfg.Database.Driver)
}
ctx := context.Background()
// Create a local migration directory able to understand Atlas migration file format for replay.
safePath := filepath.Clean(fmt.Sprintf("internal/data/migrations/%s", sqlDialect))
dir, err := atlas.NewLocalDir(safePath)
dir, err := atlas.NewLocalDir("internal/data/migrations/migrations")
if err != nil {
log.Fatalf("failed creating atlas migration directory: %v", err)
}
@@ -45,7 +26,7 @@ func main() {
opts := []schema.MigrateOption{
schema.WithDir(dir), // provide migration directory
schema.WithMigrationMode(schema.ModeReplay), // provide migration mode
schema.WithDialect(sqlDialect), // Ent dialect to use
schema.WithDialect(dialect.SQLite), // Ent dialect to use
schema.WithFormatter(atlas.DefaultFormatter),
schema.WithDropIndex(true),
schema.WithDropColumn(true),
@@ -54,55 +35,11 @@ func main() {
log.Fatalln("migration name is required. Use: 'go run -mod=mod ent/migrate/main.go <name>'")
}
if sqlDialect == dialect.Postgres {
if !validatePostgresSSLMode(cfg.Database.SslMode) {
log.Fatalf("invalid sslmode: %s", cfg.Database.SslMode)
}
}
databaseURL := ""
switch {
case cfg.Database.Driver == "sqlite3":
databaseURL = fmt.Sprintf("sqlite://%s", cfg.Database.SqlitePath)
case cfg.Database.Driver == "postgres":
databaseURL = fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s", cfg.Database.Username, cfg.Database.Password, cfg.Database.Host, cfg.Database.Port, cfg.Database.Database, cfg.Database.SslMode)
default:
log.Fatalf("unsupported database driver: %s", cfg.Database.Driver)
}
// Generate migrations using Atlas support for MySQL (note the Ent dialect option passed above).
err = migrate.NamedDiff(ctx, databaseURL, os.Args[1], opts...)
err = migrate.NamedDiff(ctx, "sqlite://.data/homebox.migration.db?_fk=1", os.Args[1], opts...)
if err != nil {
log.Fatalf("failed generating migration file: %v", err)
}
fmt.Println("Migration file generated successfully.")
}
var (
version = "nightly"
commit = "HEAD"
buildTime = "now"
)
func build() string {
short := commit
if len(short) > 7 {
short = short[:7]
}
return fmt.Sprintf("%s, commit %s, built at %s", version, short, buildTime)
}
func validatePostgresSSLMode(sslMode string) bool {
validModes := map[string]bool{
"": true,
"disable": true,
"allow": true,
"prefer": true,
"require": true,
"verify-ca": true,
"verify-full": true,
}
return validModes[strings.ToLower(strings.TrimSpace(sslMode))]
}

View File

@@ -3,76 +3,74 @@ module github.com/sysadminsmedia/homebox/backend
go 1.23.0
require (
ariga.io/atlas v0.31.1-0.20250212144724-069be8033e83
entgo.io/ent v0.14.3
github.com/ardanlabs/conf/v3 v3.4.0
ariga.io/atlas v0.19.1
entgo.io/ent v0.14.1
github.com/ardanlabs/conf/v3 v3.1.8
github.com/containrrr/shoutrrr v0.8.0
github.com/go-chi/chi/v5 v5.2.1
github.com/go-playground/validator/v10 v10.25.0
github.com/go-chi/chi/v5 v5.1.0
github.com/go-playground/validator/v10 v10.22.1
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1
github.com/google/uuid v1.6.0
github.com/gorilla/schema v1.4.1
github.com/hay-kot/httpkit v0.0.11
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.24
github.com/olahol/melody v1.2.1
github.com/pkg/errors v0.9.1
github.com/rs/zerolog v1.33.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/stretchr/testify v1.10.0
github.com/stretchr/testify v1.9.0
github.com/swaggo/http-swagger/v2 v2.0.2
github.com/swaggo/swag v1.16.4
github.com/yeqown/go-qrcode/v2 v2.2.5
github.com/yeqown/go-qrcode/writer/standard v1.2.5
golang.org/x/crypto v0.35.0
modernc.org/sqlite v1.36.0
github.com/swaggo/swag v1.16.3
github.com/yeqown/go-qrcode/v2 v2.2.4
github.com/yeqown/go-qrcode/writer/standard v1.2.4
golang.org/x/crypto v0.28.0
modernc.org/sqlite v1.33.1
)
require github.com/zclconf/go-cty-yaml v1.1.0 // indirect
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/bmatcuk/doublestar v1.3.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/fogleman/gg v1.3.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-openapi/inflect v0.21.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-openapi/inflect v0.19.0 // indirect
github.com/go-openapi/jsonpointer v0.20.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/spec v0.20.9 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/hcl/v2 v2.23.0 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcl/v2 v2.19.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/swaggo/files/v2 v2.0.2 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/swaggo/files/v2 v2.0.0 // indirect
github.com/yeqown/reedsolomon v1.0.0 // indirect
github.com/zclconf/go-cty v1.16.0 // indirect
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
golang.org/x/image v0.23.0
golang.org/x/mod v0.23.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/tools v0.30.0 // indirect
github.com/zclconf/go-cty v1.14.1 // indirect
golang.org/x/image v0.18.0 // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect
golang.org/x/tools v0.24.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.61.13 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.8.2 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.55.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
)

View File

@@ -1,7 +1,11 @@
ariga.io/atlas v0.31.1-0.20250212144724-069be8033e83 h1:nX4HXncwIdvQ8/8sIUIf1nyCkK8qdBaHQ7EtzPpuiGE=
ariga.io/atlas v0.31.1-0.20250212144724-069be8033e83/go.mod h1:Oe1xWPuu5q9LzyrWfbZmEZxFYeu4BHTyzfjeW2aZp/w=
entgo.io/ent v0.14.3 h1:wokAV/kIlH9TeklJWGGS7AYJdVckr0DloWjIcO9iIIQ=
entgo.io/ent v0.14.3/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM=
ariga.io/atlas v0.19.1 h1:QzBHkakwzEhmPWOzNhw8Yr/Bbicj6Iq5hwEoNI/Jr9A=
ariga.io/atlas v0.19.1/go.mod h1:VPlcXdd4w2KqKnH54yEZcry79UAhpaWaxEsmn5JRNoE=
ariga.io/atlas v0.28.0 h1:qmn9tUyJypJkIw+X3ECUwDtkMTiFupgstHbjRN4xGH0=
ariga.io/atlas v0.28.0/go.mod h1:LOOp18LCL9r+VifvVlJqgYJwYl271rrXD9/wIyzJ8sw=
entgo.io/ent v0.12.5 h1:KREM5E4CSoej4zeGa88Ou/gfturAnpUv0mzAjch1sj4=
entgo.io/ent v0.12.5/go.mod h1:Y3JVAjtlIk8xVZYSn3t3mf8xlZIn5SAOXZQxD6kKI+Q=
entgo.io/ent v0.14.1 h1:fUERL506Pqr92EPHJqr8EYxbPioflJo6PudkrEA8a/s=
entgo.io/ent v0.14.1/go.mod h1:MH6XLG0KXpkcDQhKiHfANZSzR55TJyPL5IGNpI8wpco=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
@@ -10,45 +14,54 @@ github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7l
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/ardanlabs/conf/v3 v3.4.0 h1:Qy7/doJjhsv7Lvzqd9tbvH8fAZ9jzqKtwnwcmZ+sxGs=
github.com/ardanlabs/conf/v3 v3.4.0/go.mod h1:OIi6NK95fj8jKFPdZ/UmcPlY37JBg99hdP9o5XmNK9c=
github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
github.com/ardanlabs/conf/v3 v3.1.8 h1:r0KUV9/Hni5XdeWR2+A1BiedIDnry5CjezoqgJ0rnFQ=
github.com/ardanlabs/conf/v3 v3.1.8/go.mod h1:OIi6NK95fj8jKFPdZ/UmcPlY37JBg99hdP9o5XmNK9c=
github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec=
github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-openapi/inflect v0.21.0 h1:FoBjBTQEcbg2cJUWX6uwL9OyIW8eqc9k4KhN4lfbeYk=
github.com/go-openapi/inflect v0.21.0/go.mod h1:INezMuUu7SJQc2AyR3WO0DqqYUJSj8Kb4hBd7WtjlAw=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ=
github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA=
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8=
github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
@@ -68,41 +81,56 @@ 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.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos=
github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA=
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=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5RPI=
github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
github.com/hay-kot/httpkit v0.0.11 h1:ZdB2uqsFBSDpfUoClGK5c5orjBjQkEVSXh7fZX5FKEk=
github.com/hay-kot/httpkit v0.0.11/go.mod h1:0kZdk5/swzdfqfg2c6pBWimcgeJ9PTyO97EbHnYl2Sw=
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/olahol/melody v1.2.1 h1:xdwRkzHxf+B0w4TKbGpUSSkV516ZucQZJIWLztOWICQ=
github.com/olahol/melody v1.2.1/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
@@ -118,77 +146,98 @@ github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUz
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU=
github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg=
github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ=
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
github.com/yeqown/go-qrcode/v2 v2.2.5 h1:HCOe2bSjkhZyYoyyNaXNzh4DJZll6inVJQQw+8228Zk=
github.com/yeqown/go-qrcode/v2 v2.2.5/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw=
github.com/yeqown/go-qrcode/writer/standard v1.2.5 h1:m+5BUIcbsaG2md76FIqI/oZULrAju8tsk47eOohovQ0=
github.com/yeqown/go-qrcode/writer/standard v1.2.5/go.mod h1:O4MbzsotGCvy8upYPCR91j81dr5XLT7heuljcNXW+oQ=
github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk=
github.com/yeqown/go-qrcode/v2 v2.2.4 h1:cXdYlrhzHzVAnJHiwr/T6lAUmS9MtEStjEZBjArrvnc=
github.com/yeqown/go-qrcode/v2 v2.2.4/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw=
github.com/yeqown/go-qrcode/writer/standard v1.2.4 h1:41e/aLr1AMVWlug6oUMkDg2r0+dv5ofB7UaTkekKZBc=
github.com/yeqown/go-qrcode/writer/standard v1.2.4/go.mod h1:H8nLSGYUWBpNyBPjDcJzAanMzYBBYMFtrU2lwoSRn+k=
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
github.com/zclconf/go-cty v1.16.0 h1:xPKEhst+BW5D0wxebMZkxgapvOE/dw7bFTlgSc9nD6w=
github.com/zclconf/go-cty v1.16.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0=
github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
github.com/zclconf/go-cty v1.14.1 h1:t9fyA35fwjjUMcmL5hLER+e/rEPqrbCK1/OSE4SI9KA=
github.com/zclconf/go-cty v1.14.1/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8=
github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.36.0 h1:EQXNRn4nIS+gfsKeUTymHIz1waxuv5BzU7558dHSfH8=
modernc.org/sqlite v1.36.0/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/sqlite v1.33.0 h1:WWkA/T2G17okiLGgKAj4/RMIvgyMT19yQ038160IeYk=
modernc.org/sqlite v1.33.0/go.mod h1:9uQ9hF/pCZoYZK73D/ud5Z7cIRIILSZI8NdIemVMTX8=
modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM=
modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

File diff suppressed because it is too large Load Diff

View File

@@ -61,7 +61,7 @@ func New(repos *repo.AllRepos, opts ...OptionsFunc) *AllServices {
repo: repos,
autoIncrementAssetID: options.autoIncrementAssetID,
},
BackgroundService: &BackgroundService{repos, Latest{}},
BackgroundService: &BackgroundService{repos},
Currencies: currencies.NewCurrencyService(options.currencies),
}
}

View File

@@ -50,7 +50,7 @@ func bootstrap() {
}
func MainNoExit(m *testing.M) int {
client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1&_time_format=sqlite")
client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil {
log.Fatalf("failed opening connection to sqlite: %v", err)
}

View File

@@ -2,9 +2,6 @@ package services
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
@@ -14,13 +11,8 @@ import (
"github.com/sysadminsmedia/homebox/backend/internal/data/types"
)
type Latest struct {
Version string `json:"version"`
Date string `json:"date"`
}
type BackgroundService struct {
repos *repo.AllRepos
latest Latest
repos *repo.AllRepos
}
func (svc *BackgroundService) SendNotifiersToday(ctx context.Context) error {
@@ -88,51 +80,19 @@ func (svc *BackgroundService) SendNotifiersToday(ctx context.Context) error {
return nil
}
func (svc *BackgroundService) GetLatestGithubRelease(ctx context.Context) error {
url := "https://api.github.com/repos/sysadminsmedia/homebox/releases/latest"
func (svc *BackgroundService) UpdateLocales(ctx context.Context) error {
log.Debug().Msg("updating locales")
// fetch list of locales from github
// is it worth checking if any changes have been made?
// download locales overwriting files in static/public/locales
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return fmt.Errorf("failed to create latest version request: %w", err)
}
// curl -H "Accept: application/vnd.github.v3+json" \
// -H "If-Modified-Since: Thu, 31 Oct 2024 09:59:02 GMT" \
// -o /dev/null -s -w "%{http_code}\n" \
// https://api.github.com/repos/sysadminsmedia/homebox/contents/frontend/locales
// keep track of last modified date
req.Header.Set("User-Agent", "Homebox-Version-Checker")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to make latest version request: %w", err)
}
defer func() {
err := resp.Body.Close()
if err != nil {
log.Printf("error closing latest version response body: %v", err)
}
}()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("latest version unexpected status code: %d", resp.StatusCode)
}
// ignoring fields that are not relevant
type Release struct {
ReleaseVersion string `json:"tag_name"`
PublishedAt time.Time `json:"published_at"`
}
var release Release
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
return fmt.Errorf("failed to decode latest version response: %w", err)
}
svc.latest = Latest{
Version: release.ReleaseVersion,
Date: release.PublishedAt.String(),
}
return nil
}
func (svc *BackgroundService) GetLatestVersion() Latest {
return svc.latest
}
}

View File

@@ -40,8 +40,6 @@ type Item struct {
Archived bool `json:"archived,omitempty"`
// AssetID holds the value of the "asset_id" field.
AssetID int `json:"asset_id,omitempty"`
// SyncChildItemsLocations holds the value of the "sync_child_items_locations" field.
SyncChildItemsLocations bool `json:"sync_child_items_locations,omitempty"`
// SerialNumber holds the value of the "serial_number" field.
SerialNumber string `json:"serial_number,omitempty"`
// ModelNumber holds the value of the "model_number" field.
@@ -183,7 +181,7 @@ func (*Item) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
case item.FieldInsured, item.FieldArchived, item.FieldSyncChildItemsLocations, item.FieldLifetimeWarranty:
case item.FieldInsured, item.FieldArchived, item.FieldLifetimeWarranty:
values[i] = new(sql.NullBool)
case item.FieldPurchasePrice, item.FieldSoldPrice:
values[i] = new(sql.NullFloat64)
@@ -282,12 +280,6 @@ func (i *Item) assignValues(columns []string, values []any) error {
} else if value.Valid {
i.AssetID = int(value.Int64)
}
case item.FieldSyncChildItemsLocations:
if value, ok := values[j].(*sql.NullBool); !ok {
return fmt.Errorf("unexpected type %T for field sync_child_items_locations", values[j])
} else if value.Valid {
i.SyncChildItemsLocations = value.Bool
}
case item.FieldSerialNumber:
if value, ok := values[j].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field serial_number", values[j])
@@ -493,9 +485,6 @@ func (i *Item) String() string {
builder.WriteString("asset_id=")
builder.WriteString(fmt.Sprintf("%v", i.AssetID))
builder.WriteString(", ")
builder.WriteString("sync_child_items_locations=")
builder.WriteString(fmt.Sprintf("%v", i.SyncChildItemsLocations))
builder.WriteString(", ")
builder.WriteString("serial_number=")
builder.WriteString(i.SerialNumber)
builder.WriteString(", ")

View File

@@ -35,8 +35,6 @@ const (
FieldArchived = "archived"
// FieldAssetID holds the string denoting the asset_id field in the database.
FieldAssetID = "asset_id"
// FieldSyncChildItemsLocations holds the string denoting the sync_child_items_locations field in the database.
FieldSyncChildItemsLocations = "sync_child_items_locations"
// FieldSerialNumber holds the string denoting the serial_number field in the database.
FieldSerialNumber = "serial_number"
// FieldModelNumber holds the string denoting the model_number field in the database.
@@ -144,7 +142,6 @@ var Columns = []string{
FieldInsured,
FieldArchived,
FieldAssetID,
FieldSyncChildItemsLocations,
FieldSerialNumber,
FieldModelNumber,
FieldManufacturer,
@@ -212,8 +209,6 @@ var (
DefaultArchived bool
// DefaultAssetID holds the default value on creation for the "asset_id" field.
DefaultAssetID int
// DefaultSyncChildItemsLocations holds the default value on creation for the "sync_child_items_locations" field.
DefaultSyncChildItemsLocations bool
// SerialNumberValidator is a validator for the "serial_number" field. It is called by the builders before save.
SerialNumberValidator func(string) error
// ModelNumberValidator is a validator for the "model_number" field. It is called by the builders before save.
@@ -292,11 +287,6 @@ func ByAssetID(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldAssetID, opts...).ToFunc()
}
// BySyncChildItemsLocations orders the results by the sync_child_items_locations field.
func BySyncChildItemsLocations(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldSyncChildItemsLocations, opts...).ToFunc()
}
// BySerialNumber orders the results by the serial_number field.
func BySerialNumber(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldSerialNumber, opts...).ToFunc()

View File

@@ -106,11 +106,6 @@ func AssetID(v int) predicate.Item {
return predicate.Item(sql.FieldEQ(FieldAssetID, v))
}
// SyncChildItemsLocations applies equality check predicate on the "sync_child_items_locations" field. It's identical to SyncChildItemsLocationsEQ.
func SyncChildItemsLocations(v bool) predicate.Item {
return predicate.Item(sql.FieldEQ(FieldSyncChildItemsLocations, v))
}
// SerialNumber applies equality check predicate on the "serial_number" field. It's identical to SerialNumberEQ.
func SerialNumber(v string) predicate.Item {
return predicate.Item(sql.FieldEQ(FieldSerialNumber, v))
@@ -646,16 +641,6 @@ func AssetIDLTE(v int) predicate.Item {
return predicate.Item(sql.FieldLTE(FieldAssetID, v))
}
// SyncChildItemsLocationsEQ applies the EQ predicate on the "sync_child_items_locations" field.
func SyncChildItemsLocationsEQ(v bool) predicate.Item {
return predicate.Item(sql.FieldEQ(FieldSyncChildItemsLocations, v))
}
// SyncChildItemsLocationsNEQ applies the NEQ predicate on the "sync_child_items_locations" field.
func SyncChildItemsLocationsNEQ(v bool) predicate.Item {
return predicate.Item(sql.FieldNEQ(FieldSyncChildItemsLocations, v))
}
// SerialNumberEQ applies the EQ predicate on the "serial_number" field.
func SerialNumberEQ(v string) predicate.Item {
return predicate.Item(sql.FieldEQ(FieldSerialNumber, v))

View File

@@ -159,20 +159,6 @@ func (ic *ItemCreate) SetNillableAssetID(i *int) *ItemCreate {
return ic
}
// SetSyncChildItemsLocations sets the "sync_child_items_locations" field.
func (ic *ItemCreate) SetSyncChildItemsLocations(b bool) *ItemCreate {
ic.mutation.SetSyncChildItemsLocations(b)
return ic
}
// SetNillableSyncChildItemsLocations sets the "sync_child_items_locations" field if the given value is not nil.
func (ic *ItemCreate) SetNillableSyncChildItemsLocations(b *bool) *ItemCreate {
if b != nil {
ic.SetSyncChildItemsLocations(*b)
}
return ic
}
// SetSerialNumber sets the "serial_number" field.
func (ic *ItemCreate) SetSerialNumber(s string) *ItemCreate {
ic.mutation.SetSerialNumber(s)
@@ -552,10 +538,6 @@ func (ic *ItemCreate) defaults() {
v := item.DefaultAssetID
ic.mutation.SetAssetID(v)
}
if _, ok := ic.mutation.SyncChildItemsLocations(); !ok {
v := item.DefaultSyncChildItemsLocations
ic.mutation.SetSyncChildItemsLocations(v)
}
if _, ok := ic.mutation.LifetimeWarranty(); !ok {
v := item.DefaultLifetimeWarranty
ic.mutation.SetLifetimeWarranty(v)
@@ -617,9 +599,6 @@ func (ic *ItemCreate) check() error {
if _, ok := ic.mutation.AssetID(); !ok {
return &ValidationError{Name: "asset_id", err: errors.New(`ent: missing required field "Item.asset_id"`)}
}
if _, ok := ic.mutation.SyncChildItemsLocations(); !ok {
return &ValidationError{Name: "sync_child_items_locations", err: errors.New(`ent: missing required field "Item.sync_child_items_locations"`)}
}
if v, ok := ic.mutation.SerialNumber(); ok {
if err := item.SerialNumberValidator(v); err != nil {
return &ValidationError{Name: "serial_number", err: fmt.Errorf(`ent: validator failed for field "Item.serial_number": %w`, err)}
@@ -732,10 +711,6 @@ func (ic *ItemCreate) createSpec() (*Item, *sqlgraph.CreateSpec) {
_spec.SetField(item.FieldAssetID, field.TypeInt, value)
_node.AssetID = value
}
if value, ok := ic.mutation.SyncChildItemsLocations(); ok {
_spec.SetField(item.FieldSyncChildItemsLocations, field.TypeBool, value)
_node.SyncChildItemsLocations = value
}
if value, ok := ic.mutation.SerialNumber(); ok {
_spec.SetField(item.FieldSerialNumber, field.TypeString, value)
_node.SerialNumber = value

View File

@@ -185,20 +185,6 @@ func (iu *ItemUpdate) AddAssetID(i int) *ItemUpdate {
return iu
}
// SetSyncChildItemsLocations sets the "sync_child_items_locations" field.
func (iu *ItemUpdate) SetSyncChildItemsLocations(b bool) *ItemUpdate {
iu.mutation.SetSyncChildItemsLocations(b)
return iu
}
// SetNillableSyncChildItemsLocations sets the "sync_child_items_locations" field if the given value is not nil.
func (iu *ItemUpdate) SetNillableSyncChildItemsLocations(b *bool) *ItemUpdate {
if b != nil {
iu.SetSyncChildItemsLocations(*b)
}
return iu
}
// SetSerialNumber sets the "serial_number" field.
func (iu *ItemUpdate) SetSerialNumber(s string) *ItemUpdate {
iu.mutation.SetSerialNumber(s)
@@ -850,9 +836,6 @@ func (iu *ItemUpdate) sqlSave(ctx context.Context) (n int, err error) {
if value, ok := iu.mutation.AddedAssetID(); ok {
_spec.AddField(item.FieldAssetID, field.TypeInt, value)
}
if value, ok := iu.mutation.SyncChildItemsLocations(); ok {
_spec.SetField(item.FieldSyncChildItemsLocations, field.TypeBool, value)
}
if value, ok := iu.mutation.SerialNumber(); ok {
_spec.SetField(item.FieldSerialNumber, field.TypeString, value)
}
@@ -1410,20 +1393,6 @@ func (iuo *ItemUpdateOne) AddAssetID(i int) *ItemUpdateOne {
return iuo
}
// SetSyncChildItemsLocations sets the "sync_child_items_locations" field.
func (iuo *ItemUpdateOne) SetSyncChildItemsLocations(b bool) *ItemUpdateOne {
iuo.mutation.SetSyncChildItemsLocations(b)
return iuo
}
// SetNillableSyncChildItemsLocations sets the "sync_child_items_locations" field if the given value is not nil.
func (iuo *ItemUpdateOne) SetNillableSyncChildItemsLocations(b *bool) *ItemUpdateOne {
if b != nil {
iuo.SetSyncChildItemsLocations(*b)
}
return iuo
}
// SetSerialNumber sets the "serial_number" field.
func (iuo *ItemUpdateOne) SetSerialNumber(s string) *ItemUpdateOne {
iuo.mutation.SetSerialNumber(s)
@@ -2105,9 +2074,6 @@ func (iuo *ItemUpdateOne) sqlSave(ctx context.Context) (_node *Item, err error)
if value, ok := iuo.mutation.AddedAssetID(); ok {
_spec.AddField(item.FieldAssetID, field.TypeInt, value)
}
if value, ok := iuo.mutation.SyncChildItemsLocations(); ok {
_spec.SetField(item.FieldSyncChildItemsLocations, field.TypeBool, value)
}
if value, ok := iuo.mutation.SerialNumber(); ok {
_spec.SetField(item.FieldSerialNumber, field.TypeString, value)
}

View File

@@ -162,7 +162,6 @@ var (
{Name: "insured", Type: field.TypeBool, Default: false},
{Name: "archived", Type: field.TypeBool, Default: false},
{Name: "asset_id", Type: field.TypeInt, Default: 0},
{Name: "sync_child_items_locations", Type: field.TypeBool, Default: false},
{Name: "serial_number", Type: field.TypeString, Nullable: true, Size: 255},
{Name: "model_number", Type: field.TypeString, Nullable: true, Size: 255},
{Name: "manufacturer", Type: field.TypeString, Nullable: true, Size: 255},
@@ -188,19 +187,19 @@ var (
ForeignKeys: []*schema.ForeignKey{
{
Symbol: "items_groups_items",
Columns: []*schema.Column{ItemsColumns[25]},
Columns: []*schema.Column{ItemsColumns[24]},
RefColumns: []*schema.Column{GroupsColumns[0]},
OnDelete: schema.Cascade,
},
{
Symbol: "items_items_children",
Columns: []*schema.Column{ItemsColumns[26]},
Columns: []*schema.Column{ItemsColumns[25]},
RefColumns: []*schema.Column{ItemsColumns[0]},
OnDelete: schema.SetNull,
},
{
Symbol: "items_locations_items",
Columns: []*schema.Column{ItemsColumns[27]},
Columns: []*schema.Column{ItemsColumns[26]},
RefColumns: []*schema.Column{LocationsColumns[0]},
OnDelete: schema.Cascade,
},
@@ -214,17 +213,17 @@ var (
{
Name: "item_manufacturer",
Unique: false,
Columns: []*schema.Column{ItemsColumns[14]},
Columns: []*schema.Column{ItemsColumns[13]},
},
{
Name: "item_model_number",
Unique: false,
Columns: []*schema.Column{ItemsColumns[13]},
Columns: []*schema.Column{ItemsColumns[12]},
},
{
Name: "item_serial_number",
Unique: false,
Columns: []*schema.Column{ItemsColumns[12]},
Columns: []*schema.Column{ItemsColumns[11]},
},
{
Name: "item_archived",

View File

@@ -4085,7 +4085,6 @@ type ItemMutation struct {
archived *bool
asset_id *int
addasset_id *int
sync_child_items_locations *bool
serial_number *string
model_number *string
manufacturer *string
@@ -4671,42 +4670,6 @@ func (m *ItemMutation) ResetAssetID() {
m.addasset_id = nil
}
// SetSyncChildItemsLocations sets the "sync_child_items_locations" field.
func (m *ItemMutation) SetSyncChildItemsLocations(b bool) {
m.sync_child_items_locations = &b
}
// SyncChildItemsLocations returns the value of the "sync_child_items_locations" field in the mutation.
func (m *ItemMutation) SyncChildItemsLocations() (r bool, exists bool) {
v := m.sync_child_items_locations
if v == nil {
return
}
return *v, true
}
// OldSyncChildItemsLocations returns the old "sync_child_items_locations" field's value of the Item entity.
// If the Item object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *ItemMutation) OldSyncChildItemsLocations(ctx context.Context) (v bool, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldSyncChildItemsLocations is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldSyncChildItemsLocations requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldSyncChildItemsLocations: %w", err)
}
return oldValue.SyncChildItemsLocations, nil
}
// ResetSyncChildItemsLocations resets all changes to the "sync_child_items_locations" field.
func (m *ItemMutation) ResetSyncChildItemsLocations() {
m.sync_child_items_locations = nil
}
// SetSerialNumber sets the "serial_number" field.
func (m *ItemMutation) SetSerialNumber(s string) {
m.serial_number = &s
@@ -5766,7 +5729,7 @@ func (m *ItemMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *ItemMutation) Fields() []string {
fields := make([]string, 0, 24)
fields := make([]string, 0, 23)
if m.created_at != nil {
fields = append(fields, item.FieldCreatedAt)
}
@@ -5797,9 +5760,6 @@ func (m *ItemMutation) Fields() []string {
if m.asset_id != nil {
fields = append(fields, item.FieldAssetID)
}
if m.sync_child_items_locations != nil {
fields = append(fields, item.FieldSyncChildItemsLocations)
}
if m.serial_number != nil {
fields = append(fields, item.FieldSerialNumber)
}
@@ -5867,8 +5827,6 @@ func (m *ItemMutation) Field(name string) (ent.Value, bool) {
return m.Archived()
case item.FieldAssetID:
return m.AssetID()
case item.FieldSyncChildItemsLocations:
return m.SyncChildItemsLocations()
case item.FieldSerialNumber:
return m.SerialNumber()
case item.FieldModelNumber:
@@ -5924,8 +5882,6 @@ func (m *ItemMutation) OldField(ctx context.Context, name string) (ent.Value, er
return m.OldArchived(ctx)
case item.FieldAssetID:
return m.OldAssetID(ctx)
case item.FieldSyncChildItemsLocations:
return m.OldSyncChildItemsLocations(ctx)
case item.FieldSerialNumber:
return m.OldSerialNumber(ctx)
case item.FieldModelNumber:
@@ -6031,13 +5987,6 @@ func (m *ItemMutation) SetField(name string, value ent.Value) error {
}
m.SetAssetID(v)
return nil
case item.FieldSyncChildItemsLocations:
v, ok := value.(bool)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetSyncChildItemsLocations(v)
return nil
case item.FieldSerialNumber:
v, ok := value.(string)
if !ok {
@@ -6340,9 +6289,6 @@ func (m *ItemMutation) ResetField(name string) error {
case item.FieldAssetID:
m.ResetAssetID()
return nil
case item.FieldSyncChildItemsLocations:
m.ResetSyncChildItemsLocations()
return nil
case item.FieldSerialNumber:
m.ResetSerialNumber()
return nil

View File

@@ -259,40 +259,36 @@ func init() {
itemDescAssetID := itemFields[5].Descriptor()
// item.DefaultAssetID holds the default value on creation for the asset_id field.
item.DefaultAssetID = itemDescAssetID.Default.(int)
// itemDescSyncChildItemsLocations is the schema descriptor for sync_child_items_locations field.
itemDescSyncChildItemsLocations := itemFields[6].Descriptor()
// item.DefaultSyncChildItemsLocations holds the default value on creation for the sync_child_items_locations field.
item.DefaultSyncChildItemsLocations = itemDescSyncChildItemsLocations.Default.(bool)
// itemDescSerialNumber is the schema descriptor for serial_number field.
itemDescSerialNumber := itemFields[7].Descriptor()
itemDescSerialNumber := itemFields[6].Descriptor()
// item.SerialNumberValidator is a validator for the "serial_number" field. It is called by the builders before save.
item.SerialNumberValidator = itemDescSerialNumber.Validators[0].(func(string) error)
// itemDescModelNumber is the schema descriptor for model_number field.
itemDescModelNumber := itemFields[8].Descriptor()
itemDescModelNumber := itemFields[7].Descriptor()
// item.ModelNumberValidator is a validator for the "model_number" field. It is called by the builders before save.
item.ModelNumberValidator = itemDescModelNumber.Validators[0].(func(string) error)
// itemDescManufacturer is the schema descriptor for manufacturer field.
itemDescManufacturer := itemFields[9].Descriptor()
itemDescManufacturer := itemFields[8].Descriptor()
// item.ManufacturerValidator is a validator for the "manufacturer" field. It is called by the builders before save.
item.ManufacturerValidator = itemDescManufacturer.Validators[0].(func(string) error)
// itemDescLifetimeWarranty is the schema descriptor for lifetime_warranty field.
itemDescLifetimeWarranty := itemFields[10].Descriptor()
itemDescLifetimeWarranty := itemFields[9].Descriptor()
// item.DefaultLifetimeWarranty holds the default value on creation for the lifetime_warranty field.
item.DefaultLifetimeWarranty = itemDescLifetimeWarranty.Default.(bool)
// itemDescWarrantyDetails is the schema descriptor for warranty_details field.
itemDescWarrantyDetails := itemFields[12].Descriptor()
itemDescWarrantyDetails := itemFields[11].Descriptor()
// item.WarrantyDetailsValidator is a validator for the "warranty_details" field. It is called by the builders before save.
item.WarrantyDetailsValidator = itemDescWarrantyDetails.Validators[0].(func(string) error)
// itemDescPurchasePrice is the schema descriptor for purchase_price field.
itemDescPurchasePrice := itemFields[15].Descriptor()
itemDescPurchasePrice := itemFields[14].Descriptor()
// item.DefaultPurchasePrice holds the default value on creation for the purchase_price field.
item.DefaultPurchasePrice = itemDescPurchasePrice.Default.(float64)
// itemDescSoldPrice is the schema descriptor for sold_price field.
itemDescSoldPrice := itemFields[18].Descriptor()
itemDescSoldPrice := itemFields[17].Descriptor()
// item.DefaultSoldPrice holds the default value on creation for the sold_price field.
item.DefaultSoldPrice = itemDescSoldPrice.Default.(float64)
// itemDescSoldNotes is the schema descriptor for sold_notes field.
itemDescSoldNotes := itemFields[19].Descriptor()
itemDescSoldNotes := itemFields[18].Descriptor()
// item.SoldNotesValidator is a validator for the "sold_notes" field. It is called by the builders before save.
item.SoldNotesValidator = itemDescSoldNotes.Validators[0].(func(string) error)
// itemDescID is the schema descriptor for id field.

View File

@@ -51,8 +51,6 @@ func (Item) Fields() []ent.Field {
Default(false),
field.Int("asset_id").
Default(0),
field.Bool("sync_child_items_locations").
Default(false),
// ------------------------------------
// item identification

View File

@@ -3,29 +3,23 @@ package migrations
import (
"embed"
"fmt"
"os"
"path"
)
//go:embed all:sqlite3 all:postgres
//go:embed all:migrations
var Files embed.FS
// Write writes the embedded migrations to a temporary directory.
// It returns an error and a cleanup function. The cleanup function
// should be called when the migrations are no longer needed.
func Write(temp string, dialect string) error {
allowedDialects := map[string]bool{"sqlite3": true, "postgres": true}
if !allowedDialects[dialect] {
return fmt.Errorf("unsupported dialect: %s", dialect)
}
func Write(temp string) error {
err := os.MkdirAll(temp, 0o755)
if err != nil {
return err
}
fsDir, err := Files.ReadDir(dialect)
fsDir, err := Files.ReadDir("migrations")
if err != nil {
return err
}
@@ -35,7 +29,7 @@ func Write(temp string, dialect string) error {
continue
}
b, err := Files.ReadFile(path.Join(dialect, f.Name()))
b, err := Files.ReadFile(path.Join("migrations", f.Name()))
if err != nil {
return err
}

View File

@@ -1,4 +1,4 @@
h1:lHvusH+dq770FHk3fVAVqZqcW2Q0c9wR+uhouqrzXuw=
h1:sjJCTAqc9FG8BKBIzh5ZynYD/Ilz6vnLqM4XX83WQ4M=
20220929052825_init.sql h1:ZlCqm1wzjDmofeAcSX3jE4h4VcdTNGpRg2eabztDy9Q=
20221001210956_group_invitations.sql h1:YQKJFtE39wFOcRNbZQ/d+ZlHwrcfcsZlcv/pLEYdpjw=
20221009173029_add_user_roles.sql h1:vWmzAfgEWQeGk0Vn70zfVPCcfEZth3E0JcvyKTjpYyU=
@@ -13,4 +13,3 @@ h1:lHvusH+dq770FHk3fVAVqZqcW2Q0c9wR+uhouqrzXuw=
20230305065819_add_notifier_types.sql h1:r5xrgCKYQ2o9byBqYeAX1zdp94BLdaxf4vq9OmGHNl0=
20230305071524_add_group_id_to_notifiers.sql h1:xDShqbyClcFhvJbwclOHdczgXbdffkxXNWjV61hL/t4=
20231006213457_add_primary_attachment_flag.sql h1:J4tMSJQFa7vaj0jpnh8YKTssdyIjRyq6RXDXZIzDDu4=
20241226183416_sync_childs.sql h1:L9EWCzgz68OEw0r6Ryv0BdC6ViJbd/C/pt9o/FkSsbk=

View File

@@ -1,58 +0,0 @@
-- Create "groups" table
CREATE TABLE "groups" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "currency" character varying NOT NULL DEFAULT 'usd', PRIMARY KEY ("id"));
-- Create "documents" table
CREATE TABLE "documents" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "title" character varying NOT NULL, "path" character varying NOT NULL, "group_documents" uuid NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "documents_groups_documents" FOREIGN KEY ("group_documents") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE);
-- Create "locations" table
CREATE TABLE "locations" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "description" character varying NULL, "group_locations" uuid NOT NULL, "location_children" uuid NULL, PRIMARY KEY ("id"), CONSTRAINT "locations_groups_locations" FOREIGN KEY ("group_locations") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "locations_locations_children" FOREIGN KEY ("location_children") REFERENCES "locations" ("id") ON UPDATE NO ACTION ON DELETE SET NULL);
-- Create "items" table
CREATE TABLE "items" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "description" character varying NULL, "import_ref" character varying NULL, "notes" character varying NULL, "quantity" bigint NOT NULL DEFAULT 1, "insured" boolean NOT NULL DEFAULT false, "archived" boolean NOT NULL DEFAULT false, "asset_id" bigint NOT NULL DEFAULT 0, "serial_number" character varying NULL, "model_number" character varying NULL, "manufacturer" character varying NULL, "lifetime_warranty" boolean NOT NULL DEFAULT false, "warranty_expires" timestamptz NULL, "warranty_details" character varying NULL, "purchase_time" timestamptz NULL, "purchase_from" character varying NULL, "purchase_price" double precision NOT NULL DEFAULT 0, "sold_time" timestamptz NULL, "sold_to" character varying NULL, "sold_price" double precision NOT NULL DEFAULT 0, "sold_notes" character varying NULL, "group_items" uuid NOT NULL, "item_children" uuid NULL, "location_items" uuid NULL, PRIMARY KEY ("id"), CONSTRAINT "items_groups_items" FOREIGN KEY ("group_items") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "items_items_children" FOREIGN KEY ("item_children") REFERENCES "items" ("id") ON UPDATE NO ACTION ON DELETE SET NULL, CONSTRAINT "items_locations_items" FOREIGN KEY ("location_items") REFERENCES "locations" ("id") ON UPDATE NO ACTION ON DELETE CASCADE);
-- Create index "item_archived" to table: "items"
CREATE INDEX "item_archived" ON "items" ("archived");
-- Create index "item_asset_id" to table: "items"
CREATE INDEX "item_asset_id" ON "items" ("asset_id");
-- Create index "item_manufacturer" to table: "items"
CREATE INDEX "item_manufacturer" ON "items" ("manufacturer");
-- Create index "item_model_number" to table: "items"
CREATE INDEX "item_model_number" ON "items" ("model_number");
-- Create index "item_name" to table: "items"
CREATE INDEX "item_name" ON "items" ("name");
-- Create index "item_serial_number" to table: "items"
CREATE INDEX "item_serial_number" ON "items" ("serial_number");
-- Create "attachments" table
CREATE TABLE "attachments" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "type" character varying NOT NULL DEFAULT 'attachment', "primary" boolean NOT NULL DEFAULT false, "document_attachments" uuid NOT NULL, "item_attachments" uuid NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "attachments_documents_attachments" FOREIGN KEY ("document_attachments") REFERENCES "documents" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "attachments_items_attachments" FOREIGN KEY ("item_attachments") REFERENCES "items" ("id") ON UPDATE NO ACTION ON DELETE CASCADE);
-- Create "users" table
CREATE TABLE "users" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "email" character varying NOT NULL, "password" character varying NOT NULL, "is_superuser" boolean NOT NULL DEFAULT false, "superuser" boolean NOT NULL DEFAULT false, "role" character varying NOT NULL DEFAULT 'user', "activated_on" timestamptz NULL, "group_users" uuid NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "users_groups_users" FOREIGN KEY ("group_users") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE);
-- Create index "users_email_key" to table: "users"
CREATE UNIQUE INDEX "users_email_key" ON "users" ("email");
-- Create "auth_tokens" table
CREATE TABLE "auth_tokens" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "token" bytea NOT NULL, "expires_at" timestamptz NOT NULL, "user_auth_tokens" uuid NULL, PRIMARY KEY ("id"), CONSTRAINT "auth_tokens_users_auth_tokens" FOREIGN KEY ("user_auth_tokens") REFERENCES "users" ("id") ON UPDATE NO ACTION ON DELETE CASCADE);
-- Create index "auth_tokens_token_key" to table: "auth_tokens"
CREATE UNIQUE INDEX "auth_tokens_token_key" ON "auth_tokens" ("token");
-- Create index "authtokens_token" to table: "auth_tokens"
CREATE INDEX "authtokens_token" ON "auth_tokens" ("token");
-- Create "auth_roles" table
CREATE TABLE "auth_roles" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "role" character varying NOT NULL DEFAULT 'user', "auth_tokens_roles" uuid NULL, PRIMARY KEY ("id"), CONSTRAINT "auth_roles_auth_tokens_roles" FOREIGN KEY ("auth_tokens_roles") REFERENCES "auth_tokens" ("id") ON UPDATE NO ACTION ON DELETE CASCADE);
-- Create index "auth_roles_auth_tokens_roles_key" to table: "auth_roles"
CREATE UNIQUE INDEX "auth_roles_auth_tokens_roles_key" ON "auth_roles" ("auth_tokens_roles");
-- Create "group_invitation_tokens" table
CREATE TABLE "group_invitation_tokens" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "token" bytea NOT NULL, "expires_at" timestamptz NOT NULL, "uses" bigint NOT NULL DEFAULT 0, "group_invitation_tokens" uuid NULL, PRIMARY KEY ("id"), CONSTRAINT "group_invitation_tokens_groups_invitation_tokens" FOREIGN KEY ("group_invitation_tokens") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE);
-- Create index "group_invitation_tokens_token_key" to table: "group_invitation_tokens"
CREATE UNIQUE INDEX "group_invitation_tokens_token_key" ON "group_invitation_tokens" ("token");
-- Create "item_fields" table
CREATE TABLE "item_fields" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "description" character varying NULL, "type" character varying NOT NULL, "text_value" character varying NULL, "number_value" bigint NULL, "boolean_value" boolean NOT NULL DEFAULT false, "time_value" timestamptz NOT NULL, "item_fields" uuid NULL, PRIMARY KEY ("id"), CONSTRAINT "item_fields_items_fields" FOREIGN KEY ("item_fields") REFERENCES "items" ("id") ON UPDATE NO ACTION ON DELETE CASCADE);
-- Create "labels" table
CREATE TABLE "labels" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "description" character varying NULL, "color" character varying NULL, "group_labels" uuid NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "labels_groups_labels" FOREIGN KEY ("group_labels") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE);
-- Create "label_items" table
CREATE TABLE "label_items" ("label_id" uuid NOT NULL, "item_id" uuid NOT NULL, PRIMARY KEY ("label_id", "item_id"), CONSTRAINT "label_items_item_id" FOREIGN KEY ("item_id") REFERENCES "items" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "label_items_label_id" FOREIGN KEY ("label_id") REFERENCES "labels" ("id") ON UPDATE NO ACTION ON DELETE CASCADE);
-- Create "maintenance_entries" table
CREATE TABLE "maintenance_entries" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "date" timestamptz NULL, "scheduled_date" timestamptz NULL, "name" character varying NOT NULL, "description" character varying NULL, "cost" double precision NOT NULL DEFAULT 0, "item_id" uuid NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "maintenance_entries_items_maintenance_entries" FOREIGN KEY ("item_id") REFERENCES "items" ("id") ON UPDATE NO ACTION ON DELETE CASCADE);
-- Create "notifiers" table
CREATE TABLE "notifiers" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "url" character varying NOT NULL, "is_active" boolean NOT NULL DEFAULT true, "group_id" uuid NOT NULL, "user_id" uuid NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "notifiers_groups_notifiers" FOREIGN KEY ("group_id") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "notifiers_users_notifiers" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON UPDATE NO ACTION ON DELETE CASCADE);
-- Create index "notifier_group_id" to table: "notifiers"
CREATE INDEX "notifier_group_id" ON "notifiers" ("group_id");
-- Create index "notifier_group_id_is_active" to table: "notifiers"
CREATE INDEX "notifier_group_id_is_active" ON "notifiers" ("group_id", "is_active");
-- Create index "notifier_user_id" to table: "notifiers"
CREATE INDEX "notifier_user_id" ON "notifiers" ("user_id");
-- Create index "notifier_user_id_is_active" to table: "notifiers"
CREATE INDEX "notifier_user_id_is_active" ON "notifiers" ("user_id", "is_active");

View File

@@ -1,2 +0,0 @@
-- Modify "items" table
ALTER TABLE "items" ADD COLUMN "sync_child_items_locations" boolean NOT NULL DEFAULT false;

View File

@@ -1,3 +0,0 @@
h1:3uDJVgJuOnlMCx2Ma6EC8WhM6Kiv/1ioXEyEQkeotnU=
20241027025146_init.sql h1:PJhm+pjGRtFfgmGu7MwJo8+bVelVfU5LB+LZ/c8nnGE=
20250112202302_catchup.sql h1:DCzm15PdJewaPY7hzhFWiBJqYxEDd0ZKGOUhK0/1hgc=

View File

@@ -1,24 +0,0 @@
-- Disable the enforcement of foreign-keys constraints
PRAGMA foreign_keys = off;
-- Create "new_items" table
CREATE TABLE `new_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), `archived` bool NOT NULL DEFAULT (false), `asset_id` integer NOT NULL DEFAULT (0), `sync_child_items_locations` 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, `item_children` uuid NULL, `location_items` uuid NULL, PRIMARY KEY (`id`), CONSTRAINT `items_groups_items` FOREIGN KEY (`group_items`) REFERENCES `groups` (`id`) ON DELETE CASCADE, CONSTRAINT `items_items_children` FOREIGN KEY (`item_children`) REFERENCES `items` (`id`) ON DELETE SET NULL, CONSTRAINT `items_locations_items` FOREIGN KEY (`location_items`) REFERENCES `locations` (`id`) ON DELETE CASCADE);
-- Copy rows from old table "items" to new temporary table "new_items"
INSERT INTO `new_items` (`id`, `created_at`, `updated_at`, `name`, `description`, `import_ref`, `notes`, `quantity`, `insured`, `archived`, `asset_id`, `serial_number`, `model_number`, `manufacturer`, `lifetime_warranty`, `warranty_expires`, `warranty_details`, `purchase_time`, `purchase_from`, `purchase_price`, `sold_time`, `sold_to`, `sold_price`, `sold_notes`, `group_items`, `item_children`, `location_items`) SELECT `id`, `created_at`, `updated_at`, `name`, `description`, `import_ref`, `notes`, `quantity`, `insured`, `archived`, `asset_id`, `serial_number`, `model_number`, `manufacturer`, `lifetime_warranty`, `warranty_expires`, `warranty_details`, `purchase_time`, `purchase_from`, `purchase_price`, `sold_time`, `sold_to`, `sold_price`, `sold_notes`, `group_items`, `item_children`, `location_items` FROM `items`;
-- Drop "items" table after copying rows
DROP TABLE `items`;
-- Rename temporary table "new_items" to "items"
ALTER TABLE `new_items` RENAME TO `items`;
-- Create index "item_name" to table: "items"
CREATE INDEX `item_name` ON `items` (`name`);
-- Create index "item_manufacturer" to table: "items"
CREATE INDEX `item_manufacturer` ON `items` (`manufacturer`);
-- Create index "item_model_number" to table: "items"
CREATE INDEX `item_model_number` ON `items` (`model_number`);
-- Create index "item_serial_number" to table: "items"
CREATE INDEX `item_serial_number` ON `items` (`serial_number`);
-- Create index "item_archived" to table: "items"
CREATE INDEX `item_archived` ON `items` (`archived`);
-- Create index "item_asset_id" to table: "items"
CREATE INDEX `item_asset_id` ON `items` (`asset_id`);
-- Enable back the enforcement of foreign-keys constraints
PRAGMA foreign_keys = on;

View File

@@ -40,7 +40,7 @@ func bootstrap() {
}
func MainNoExit(m *testing.M) int {
client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1&_time_format=sqlite")
client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil {
log.Fatalf("failed opening connection to sqlite: %v", err)
}

View File

@@ -161,10 +161,16 @@ func (r *GroupRepository) StatsPurchasePrice(ctx context.Context, gid uuid.UUID,
// Get the Totals for the Start and End of the Given Time Period
q := `
SELECT
SUM(CASE WHEN created_at < $1 THEN purchase_price ELSE 0 END) AS price_at_start,
SUM(CASE WHEN created_at < $2 THEN purchase_price ELSE 0 END) AS price_at_end
FROM items
WHERE group_items = $3 AND archived = false
(SELECT Sum(purchase_price)
FROM items
WHERE group_items = ?
AND items.archived = false
AND items.created_at < ?) AS price_at_start,
(SELECT Sum(purchase_price)
FROM items
WHERE group_items = ?
AND items.archived = false
AND items.created_at < ?) AS price_at_end
`
stats := ValueOverTime{
Start: start,
@@ -174,7 +180,7 @@ func (r *GroupRepository) StatsPurchasePrice(ctx context.Context, gid uuid.UUID,
var maybeStart *float64
var maybeEnd *float64
row := r.db.Sql().QueryRowContext(ctx, q, sqliteDateFormat(start), sqliteDateFormat(end), gid)
row := r.db.Sql().QueryRowContext(ctx, q, gid, sqliteDateFormat(start), gid, sqliteDateFormat(end))
err := row.Scan(&maybeStart, &maybeEnd)
if err != nil {
return nil, err
@@ -223,20 +229,20 @@ func (r *GroupRepository) StatsPurchasePrice(ctx context.Context, gid uuid.UUID,
func (r *GroupRepository) StatsGroup(ctx context.Context, gid uuid.UUID) (GroupStatistics, error) {
q := `
SELECT
(SELECT COUNT(*) FROM users WHERE group_users = $2) AS total_users,
(SELECT COUNT(*) FROM items WHERE group_items = $2 AND items.archived = false) AS total_items,
(SELECT COUNT(*) FROM locations WHERE group_locations = $2) AS total_locations,
(SELECT COUNT(*) FROM labels WHERE group_labels = $2) AS total_labels,
(SELECT SUM(purchase_price*quantity) FROM items WHERE group_items = $2 AND items.archived = false) AS total_item_price,
(SELECT COUNT(*)
FROM items
WHERE group_items = $2
AND items.archived = false
AND (items.lifetime_warranty = true OR items.warranty_expires > $1)
) AS total_with_warranty;
(SELECT COUNT(*) FROM users WHERE group_users = ?) AS total_users,
(SELECT COUNT(*) FROM items WHERE group_items = ? AND items.archived = false) AS total_items,
(SELECT COUNT(*) FROM locations WHERE group_locations = ?) AS total_locations,
(SELECT COUNT(*) FROM labels WHERE group_labels = ?) AS total_labels,
(SELECT SUM(purchase_price*quantity) FROM items WHERE group_items = ? AND items.archived = false) AS total_item_price,
(SELECT COUNT(*)
FROM items
WHERE group_items = ?
AND items.archived = false
AND (items.lifetime_warranty = true OR items.warranty_expires > date())
) AS total_with_warranty
`
var stats GroupStatistics
row := r.db.Sql().QueryRowContext(ctx, q, sqliteDateFormat(time.Now()), gid)
row := r.db.Sql().QueryRowContext(ctx, q, gid, gid, gid, gid, gid, gid)
var maybeTotalItemPrice *float64
var maybeTotalWithWarranty *int

View File

@@ -33,8 +33,7 @@ func Test_Group_Update(t *testing.T) {
assert.Equal(t, "EUR", g.Currency)
}
// TODO: Fix this test at some point, the data itself in production/development is working fine, it only fails on the test
/*func Test_Group_GroupStatistics(t *testing.T) {
func Test_Group_GroupStatistics(t *testing.T) {
useItems(t, 20)
useLabels(t, 20)
@@ -45,4 +44,4 @@ func Test_Group_Update(t *testing.T) {
assert.Equal(t, 20, stats.TotalLabels)
assert.Equal(t, 1, stats.TotalUsers)
assert.Equal(t, 1, stats.TotalLocations)
}*/
}

View File

@@ -30,20 +30,18 @@ type (
}
ItemQuery struct {
Page int
PageSize int
Search string `json:"search"`
AssetID AssetID `json:"assetId"`
LocationIDs []uuid.UUID `json:"locationIds"`
LabelIDs []uuid.UUID `json:"labelIds"`
NegateLabels bool `json:"negateLabels"`
OnlyWithoutPhoto bool `json:"onlyWithoutPhoto"`
OnlyWithPhoto bool `json:"onlyWithPhoto"`
ParentItemIDs []uuid.UUID `json:"parentIds"`
SortBy string `json:"sortBy"`
IncludeArchived bool `json:"includeArchived"`
Fields []FieldQuery `json:"fields"`
OrderBy string `json:"orderBy"`
Page int
PageSize int
Search string `json:"search"`
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"`
Fields []FieldQuery `json:"fields"`
OrderBy string `json:"orderBy"`
}
ItemField struct {
@@ -69,15 +67,14 @@ type (
}
ItemUpdate struct {
ParentID uuid.UUID `json:"parentId" extensions:"x-nullable,x-omitempty"`
ID uuid.UUID `json:"id"`
AssetID AssetID `json:"assetId" swaggertype:"string"`
Name string `json:"name" validate:"required,min=1,max=255"`
Description string `json:"description" validate:"max=1000"`
Quantity int `json:"quantity"`
Insured bool `json:"insured"`
Archived bool `json:"archived"`
SyncChildItemsLocations bool `json:"syncChildItemsLocations"`
ParentID uuid.UUID `json:"parentId" extensions:"x-nullable,x-omitempty"`
ID uuid.UUID `json:"id"`
AssetID AssetID `json:"assetId" swaggertype:"string"`
Name string `json:"name" validate:"required,min=1,max=255"`
Description string `json:"description" validate:"max=1000"`
Quantity int `json:"quantity"`
Insured bool `json:"insured"`
Archived bool `json:"archived"`
// Edges
LocationID uuid.UUID `json:"locationId"`
@@ -95,12 +92,12 @@ type (
// Purchase
PurchaseTime types.Date `json:"purchaseTime"`
PurchaseFrom string `json:"purchaseFrom" validate:"max=255"`
PurchaseFrom string `json:"purchaseFrom" validate:"max=255"`
PurchasePrice float64 `json:"purchasePrice" extensions:"x-nullable,x-omitempty"`
// Sold
SoldTime types.Date `json:"soldTime"`
SoldTo string `json:"soldTo" validate:"max=255"`
SoldTo string `json:"soldTo" validate:"max=255"`
SoldPrice float64 `json:"soldPrice" extensions:"x-nullable,x-omitempty"`
SoldNotes string `json:"soldNotes"`
@@ -118,7 +115,6 @@ type (
ItemSummary struct {
ImportRef string `json:"-"`
ID uuid.UUID `json:"id"`
AssetID AssetID `json:"assetId,string"`
Name string `json:"name"`
Description string `json:"description"`
Quantity int `json:"quantity"`
@@ -141,8 +137,6 @@ type (
ItemSummary
AssetID AssetID `json:"assetId,string"`
SyncChildItemsLocations bool `json:"syncChildItemsLocations"`
SerialNumber string `json:"serialNumber"`
ModelNumber string `json:"modelNumber"`
Manufacturer string `json:"manufacturer"`
@@ -196,7 +190,6 @@ func mapItemSummary(item *ent.Item) ItemSummary {
return ItemSummary{
ID: item.ID,
AssetID: AssetID(item.AssetID),
Name: item.Name,
Description: item.Description,
ImportRef: item.ImportRef,
@@ -255,13 +248,12 @@ func mapItemOut(item *ent.Item) ItemOut {
}
return ItemOut{
Parent: parent,
AssetID: AssetID(item.AssetID),
ItemSummary: mapItemSummary(item),
LifetimeWarranty: item.LifetimeWarranty,
WarrantyExpires: types.DateFromTime(item.WarrantyExpires),
WarrantyDetails: item.WarrantyDetails,
SyncChildItemsLocations: item.SyncChildItemsLocations,
Parent: parent,
AssetID: AssetID(item.AssetID),
ItemSummary: mapItemSummary(item),
LifetimeWarranty: item.LifetimeWarranty,
WarrantyExpires: types.DateFromTime(item.WarrantyExpires),
WarrantyDetails: item.WarrantyDetails,
// Identification
SerialNumber: item.SerialNumber,
@@ -387,27 +379,6 @@ func (e *ItemsRepository) QueryByGroup(ctx context.Context, gid uuid.UUID, q Ite
}
}
if q.OnlyWithoutPhoto {
andPredicates = append(andPredicates, item.Not(
item.HasAttachmentsWith(
attachment.And(
attachment.Primary(true),
attachment.TypeEQ(attachment.TypePhoto),
),
)),
)
}
if q.OnlyWithPhoto {
andPredicates = append(andPredicates, item.HasAttachmentsWith(
attachment.And(
attachment.Primary(true),
attachment.TypeEQ(attachment.TypePhoto),
),
),
)
}
if len(q.LocationIDs) > 0 {
locationPredicates := make([]predicate.Item, 0, len(q.LocationIDs))
for _, l := range q.LocationIDs {
@@ -451,8 +422,6 @@ func (e *ItemsRepository) QueryByGroup(ctx context.Context, gid uuid.UUID, q Ite
qb = qb.Order(ent.Desc(item.FieldCreatedAt))
case "updatedAt":
qb = qb.Order(ent.Desc(item.FieldUpdatedAt))
case "assetId":
qb = qb.Order(ent.Asc(item.FieldAssetID))
default: // "name"
qb = qb.Order(ent.Asc(item.FieldName))
}
@@ -637,8 +606,7 @@ func (e *ItemsRepository) UpdateByGroup(ctx context.Context, gid uuid.UUID, data
SetWarrantyExpires(data.WarrantyExpires.Time()).
SetWarrantyDetails(data.WarrantyDetails).
SetQuantity(data.Quantity).
SetAssetID(int(data.AssetID)).
SetSyncChildItemsLocations(data.SyncChildItemsLocations)
SetAssetID(int(data.AssetID))
currentLabels, err := e.db.Item.Query().Where(item.ID(data.ID)).QueryLabel().All(ctx)
if err != nil {
@@ -665,28 +633,6 @@ func (e *ItemsRepository) UpdateByGroup(ctx context.Context, gid uuid.UUID, data
q.ClearParent()
}
if data.SyncChildItemsLocations {
children, err := e.db.Item.Query().Where(item.ID(data.ID)).QueryChildren().All(ctx)
if err != nil {
return ItemOut{}, err
}
location := data.LocationID
for _, child := range children {
childLocation, err := child.QueryLocation().First(ctx)
if err != nil {
return ItemOut{}, err
}
if location != childLocation.ID {
err = child.Update().SetLocationID(location).Exec(ctx)
if err != nil {
return ItemOut{}, err
}
}
}
}
err = q.Exec(ctx)
if err != nil {
return ItemOut{}, err

View File

@@ -121,7 +121,7 @@ func (r *LocationRepository) GetAll(ctx context.Context, gid uuid.UUID, filter L
FROM
locations
WHERE
locations.group_locations = $1 {{ FILTER_CHILDREN }}
locations.group_locations = ? {{ FILTER_CHILDREN }}
ORDER BY
locations.name ASC
`
@@ -164,9 +164,7 @@ func (r *LocationRepository) getOne(ctx context.Context, where ...predicate.Loca
Where(where...).
WithGroup().
WithParent().
WithChildren(func(lq *ent.LocationQuery) {
lq.Order(location.ByName())
}).
WithChildren().
Only(ctx))
}
@@ -280,8 +278,8 @@ func (r *LocationRepository) PathForLoc(ctx context.Context, gid, locID uuid.UUI
query := `WITH RECURSIVE location_path AS (
SELECT id, name, location_children
FROM locations
WHERE id = $1 -- Replace ? with the ID of the item's location
AND group_locations = $2 -- Replace ? with the ID of the group
WHERE id = ? -- Replace ? with the ID of the item's location
AND group_locations = ? -- Replace ? with the ID of the group
UNION ALL
@@ -334,7 +332,7 @@ func (r *LocationRepository) Tree(ctx context.Context, gid uuid.UUID, tq TreeQue
'location' AS node_type
FROM locations
WHERE location_children IS NULL
AND group_locations = $1
AND group_locations = ?
UNION ALL
SELECT c.id,
@@ -357,8 +355,10 @@ func (r *LocationRepository) Tree(ctx context.Context, gid uuid.UUID, tq TreeQue
SELECT *
FROM location_tree
{{ WITH_ITEMS_FROM }}
) tree
ORDER BY node_type DESC, -- sort locations before items
level,

View File

@@ -5,7 +5,6 @@ import (
"errors"
"time"
"entgo.io/ent/dialect/sql"
"github.com/google/uuid"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/group"
@@ -131,36 +130,19 @@ func (r *MaintenanceEntryRepository) GetMaintenanceByItemID(ctx context.Context,
item.HasGroupWith(group.IDEQ(groupID)),
),
)
switch filters.Status {
case MaintenanceFilterStatusScheduled:
if filters.Status == MaintenanceFilterStatusScheduled {
query = query.Where(maintenanceentry.Or(
maintenanceentry.DateIsNil(),
maintenanceentry.DateEQ(time.Time{}),
maintenanceentry.DateGT(time.Now()),
))
// Sort scheduled entries by ascending scheduled date
query = query.Order(
maintenanceentry.ByScheduledDate(sql.OrderAsc()),
)
case MaintenanceFilterStatusCompleted:
} else if filters.Status == MaintenanceFilterStatusCompleted {
query = query.Where(
maintenanceentry.Not(maintenanceentry.Or(
maintenanceentry.DateIsNil(),
maintenanceentry.DateEQ(time.Time{}),
maintenanceentry.DateGT(time.Now()),
)))
// Sort completed entries by descending completion date
query = query.Order(
maintenanceentry.ByDate(sql.OrderDesc()),
)
default:
// Sort entries by default by scheduled and maintenance date in descending order
query = query.Order(
maintenanceentry.ByScheduledDate(sql.OrderDesc()),
maintenanceentry.ByDate(sql.OrderDesc()),
)
maintenanceentry.DateEQ(time.Time{})),
))
}
entries, err := query.WithItem().All(ctx)
entries, err := query.WithItem().Order(maintenanceentry.ByScheduledDate()).All(ctx)
if err != nil {
return []MaintenanceEntryWithDetails{}, err

View File

@@ -32,8 +32,8 @@ func getPrevMonth(now time.Time) time.Time {
func TestMaintenanceEntryRepository_GetLog(t *testing.T) {
item := useItems(t, 1)[0]
// Create 11 maintenance entries for the item
created := make([]MaintenanceEntryCreate, 11)
// Create 10 maintenance entries for the item
created := make([]MaintenanceEntryCreate, 10)
thisMonth := time.Now()
lastMonth := getPrevMonth(thisMonth)
@@ -52,14 +52,6 @@ func TestMaintenanceEntryRepository_GetLog(t *testing.T) {
}
}
// Add an entry completed in the future
created[10] = MaintenanceEntryCreate{
CompletedDate: types.DateFromTime(time.Now().AddDate(0, 0, 1)),
Name: "Maintenance",
Description: "Maintenance description",
Cost: 10,
}
for _, entry := range created {
_, err := tRepos.MaintEntry.Create(context.Background(), item.ID, entry)
if err != nil {

View File

@@ -18,23 +18,20 @@ const (
type Config struct {
conf.Version
Mode string `yaml:"mode" conf:"default:development"` // development or production
Web WebConfig `yaml:"web"`
Storage Storage `yaml:"storage"`
Database Database `yaml:"database"`
Log LoggerConf `yaml:"logger"`
Mailer MailerConf `yaml:"mailer"`
Demo bool `yaml:"demo"`
Debug DebugConf `yaml:"debug"`
Options Options `yaml:"options"`
LabelMaker LabelMakerConf `yaml:"labelmaker"`
Mode string `yaml:"mode" conf:"default:development"` // development or production
Web WebConfig `yaml:"web"`
Storage Storage `yaml:"storage"`
Log LoggerConf `yaml:"logger"`
Mailer MailerConf `yaml:"mailer"`
Demo bool `yaml:"demo"`
Debug DebugConf `yaml:"debug"`
Options Options `yaml:"options"`
}
type Options struct {
AllowRegistration bool `yaml:"disable_registration" conf:"default:true"`
AutoIncrementAssetID bool `yaml:"auto_increment_asset_id" conf:"default:true"`
CurrencyConfig string `yaml:"currencies"`
GithubReleaseCheck bool `yaml:"check_github_release" conf:"default:true"`
}
type DebugConf struct {
@@ -51,17 +48,6 @@ type WebConfig struct {
IdleTimeout time.Duration `yaml:"idle_timeout" conf:"default:30s"`
}
type LabelMakerConf struct {
Width int64 `yaml:"width" conf:"default:526"`
Height int64 `yaml:"height" conf:"default:200"`
Padding int64 `yaml:"padding" conf:"default:32"`
Margin int64 `yaml:"margin" conf:"default:32"`
FontSize float64 `yaml:"font_size" conf:"default:32.0"`
PrintCommand *string `yaml:"string"`
AdditionalInformation *string `yaml:"string"`
DynamicLength bool `yaml:"bool" conf:"default:true"`
}
// New parses the CLI/Config file and returns a Config struct. If the file argument is an empty string, the
// file is not read. If the file is not empty, the file is read and the Config struct is returned.
func New(buildstr string, description string) (*Config, error) {

View File

@@ -6,16 +6,6 @@ const (
type Storage struct {
// Data is the path to the root directory
Data string `yaml:"data" conf:"default:./.data"`
}
type Database struct {
Driver string `yaml:"driver" conf:"default:sqlite3"`
Username string `yaml:"username"`
Password string `yaml:"password"`
Host string `yaml:"host"`
Port string `yaml:"port"`
Database string `yaml:"database"`
SslMode string `yaml:"ssl_mode"`
SqlitePath string `yaml:"sqlite_path" conf:"default:./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite"`
Data string `yaml:"data" conf:"default:./.data"`
SqliteURL string `yaml:"sqlite-url" conf:"default:./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1"`
}

View File

@@ -1,325 +0,0 @@
// Package labelmaker provides functionality for generating and printing labels for items, locations and assets stored in Homebox
package labelmaker
import (
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"text/template"
"time"
"github.com/golang/freetype"
"github.com/golang/freetype/truetype"
"github.com/skip2/go-qrcode"
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
"golang.org/x/image/font"
"golang.org/x/image/font/gofont/gobold"
"golang.org/x/image/font/gofont/gomedium"
)
type GenerateParameters struct {
Width int
Height int
QrSize int
Margin int
ComponentPadding int
TitleText string
TitleFontSize float64
DescriptionText string
DescriptionFontSize float64
AdditionalInformation *string
Dpi float64
URL string
DynamicLength bool
}
func (p *GenerateParameters) Validate() error {
if p.Width <= 0 {
return fmt.Errorf("invalid width")
}
if p.Height <= 0 {
return fmt.Errorf("invalid height")
}
if p.Margin < 0 {
return fmt.Errorf("invalid margin")
}
if p.ComponentPadding < 0 {
return fmt.Errorf("invalid component padding")
}
return nil
}
func NewGenerateParams(width int, height int, margin int, padding int, fontSize float64, title string, description string, url string, dynamicLength bool, additionalInformation *string) GenerateParameters {
return GenerateParameters{
Width: width,
Height: height,
QrSize: height - (padding * 2),
Margin: margin,
ComponentPadding: padding,
TitleText: title,
DescriptionText: description,
TitleFontSize: fontSize,
DescriptionFontSize: fontSize * 0.8,
Dpi: 72,
URL: url,
AdditionalInformation: additionalInformation,
DynamicLength: dynamicLength,
}
}
func measureString(text string, face font.Face, ctx *freetype.Context) int {
width := 0
for _, r := range text {
awidth, _ := face.GlyphAdvance(r)
width += awidth.Round()
}
return ctx.PointToFixed(float64(width)).Round()
}
func wrapText(text string, face font.Face, maxWidth int, maxHeight int, lineHeight int, ctx *freetype.Context) ([]string, string) {
lines := strings.Split(text, "\n")
unlimitedHeight := maxHeight == -1
var wrappedLines []string
currentHeight := 0
processedChars := 0
for _, line := range lines {
words := strings.Fields(line)
if len(words) == 0 {
wrappedLines = append(wrappedLines, "")
processedChars += 1
if !unlimitedHeight {
currentHeight += lineHeight
if currentHeight > maxHeight {
return wrappedLines[:len(wrappedLines)-1], text[processedChars:]
}
}
continue
}
currentLine := words[0]
for _, word := range words[1:] {
testLine := currentLine + " " + word
width := measureString(testLine, face, ctx)
if width <= maxWidth {
currentLine = testLine
} else {
wrappedLines = append(wrappedLines, currentLine)
processedChars += len(currentLine) + 1
if !unlimitedHeight {
currentHeight += lineHeight
if currentHeight > maxHeight {
return wrappedLines[:len(wrappedLines)-1], text[processedChars-len(currentLine)-1:]
}
}
currentLine = word
}
}
wrappedLines = append(wrappedLines, currentLine)
processedChars += len(currentLine) + 1
if !unlimitedHeight {
currentHeight += lineHeight
if currentHeight > maxHeight {
return wrappedLines[:len(wrappedLines)-1], text[processedChars-len(currentLine)-1:]
}
}
}
return wrappedLines, ""
}
func GenerateLabel(w io.Writer, params *GenerateParameters) error {
if err := params.Validate(); err != nil {
return err
}
bodyText := params.DescriptionText
if params.AdditionalInformation != nil {
bodyText = bodyText + "\n" + *params.AdditionalInformation
}
// Create QR code
qr, err := qrcode.New(params.URL, qrcode.Medium)
if err != nil {
return err
}
qr.DisableBorder = true
qrImage := qr.Image(params.QrSize)
regularFont, err := truetype.Parse(gomedium.TTF)
if err != nil {
return err
}
boldFont, err := truetype.Parse(gobold.TTF)
if err != nil {
return err
}
regularFace := truetype.NewFace(regularFont, &truetype.Options{
Size: params.DescriptionFontSize,
DPI: params.Dpi,
})
boldFace := truetype.NewFace(boldFont, &truetype.Options{
Size: params.TitleFontSize,
DPI: params.Dpi,
})
// Calculate text area dimensions
maxWidth := params.Width - (params.Margin * 2) - params.ComponentPadding
// Create temporary contexts for text measurement
tmpImg := image.NewRGBA(image.Rect(0, 0, 1, 1))
boldContext := createContext(boldFont, params.TitleFontSize, tmpImg, params.Dpi)
regularContext := createContext(regularFont, params.DescriptionFontSize, tmpImg, params.Dpi)
// Calculate total height needed
totalHeight := params.Margin
titleLineSpacing := boldContext.PointToFixed(params.TitleFontSize).Round()
titleLines, _ := wrapText(params.TitleText, boldFace, maxWidth-params.QrSize, -1, titleLineSpacing, boldContext)
titleHeight := titleLineSpacing * len(titleLines)
totalHeight += titleHeight
totalHeight += params.ComponentPadding / 4
regularLineSpacing := regularContext.PointToFixed(params.DescriptionFontSize).Round()
descriptionLinesRight, descriptionRemaining := wrapText(bodyText, regularFace, maxWidth-params.QrSize, params.QrSize-titleHeight, regularLineSpacing, regularContext)
totalHeight += regularLineSpacing * len(descriptionLinesRight)
var textYBottomText int
var descriptionLinesBottom []string
hasBottomText := descriptionRemaining != ""
if hasBottomText {
totalHeight = max(params.Margin+params.QrSize+params.ComponentPadding/2, totalHeight)
textYBottomText = totalHeight
descriptionLinesBottom, _ = wrapText(descriptionRemaining, regularFace, maxWidth, -1, regularLineSpacing, regularContext)
totalHeight += regularLineSpacing * len(descriptionLinesBottom)
totalHeight += params.Margin
}
var requiredHeight int
if params.DynamicLength {
requiredHeight = max(totalHeight, params.QrSize+(params.Margin*2))
} else {
requiredHeight = params.Height
}
// Create the actual image with calculated height
bounds := image.Rect(0, 0, params.Width, requiredHeight)
img := image.NewRGBA(bounds)
draw.Draw(img, bounds, &image.Uniform{color.White}, image.Point{}, draw.Src)
// Draw QR code onto the image
draw.Draw(img,
image.Rect(params.Margin, params.Margin, params.QrSize+params.Margin, params.QrSize+params.Margin),
qrImage,
image.Point{},
draw.Over)
// Create final drawing contexts
boldContext = createContext(boldFont, params.TitleFontSize, img, params.Dpi)
regularContext = createContext(regularFont, params.DescriptionFontSize, img, params.Dpi)
textXRight := params.Margin + params.ComponentPadding + params.QrSize
textY := params.Margin - 8
// Draw title
for _, line := range titleLines {
pt := freetype.Pt(textXRight, textY+titleLineSpacing)
if _, err = boldContext.DrawString(line, pt); err != nil {
return err
}
textY += titleLineSpacing
}
// Draw description right from QR Code
textY += params.ComponentPadding / 4
for _, line := range descriptionLinesRight {
pt := freetype.Pt(textXRight, textY+regularLineSpacing)
if _, err = regularContext.DrawString(line, pt); err != nil {
return err
}
textY += regularLineSpacing
}
// Draw description below QR Code
if hasBottomText {
for _, line := range descriptionLinesBottom {
pt := freetype.Pt(params.Margin, textYBottomText+regularLineSpacing)
if _, err = regularContext.DrawString(line, pt); err != nil {
return err
}
textYBottomText += regularLineSpacing
}
}
return png.Encode(w, img)
}
// Helper function to create freetype context
func createContext(font *truetype.Font, size float64, img *image.RGBA, dpi float64) *freetype.Context {
c := freetype.NewContext()
c.SetDPI(dpi)
c.SetFont(font)
c.SetFontSize(size)
c.SetClip(img.Bounds())
c.SetDst(img)
c.SetSrc(image.NewUniform(color.Black))
return c
}
func PrintLabel(cfg *config.Config, params *GenerateParameters) error {
tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("label-%d.png", time.Now().UnixNano()))
f, err := os.OpenFile(tmpFile, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
return err
}
defer func() {
_ = f.Close()
if err := os.Remove(f.Name()); err != nil {
log.Printf("failed to remove temporary label file: %v", err)
}
}()
err = GenerateLabel(f, params)
if err != nil {
return err
}
if cfg.LabelMaker.PrintCommand == nil {
return fmt.Errorf("no print command specified")
}
commandTemplate := template.Must(template.New("command").Parse(*cfg.LabelMaker.PrintCommand))
builder := &strings.Builder{}
if err := commandTemplate.Execute(builder, map[string]string{
"FileName": f.Name(),
}); err != nil {
return err
}
commandParts := strings.Fields(builder.String())
if len(commandParts) == 0 {
return nil
}
command := exec.Command(commandParts[0], commandParts[1:]...)
_, err = command.CombinedOutput()
if err != nil {
return err
}
return nil
}

View File

@@ -41,7 +41,7 @@ export default defineConfig({
},
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: 'API Docs', link: '/en/api' },
{ text: 'API', link: 'https://redocly.github.io/redoc/?url=https://raw.githubusercontent.com/sysadminsmedia/homebox/main/docs/docs/api/openapi-2.0.json' },
{ text: 'Demo', link: 'https://demo.homebox.software' },
],
@@ -53,17 +53,6 @@ export default defineConfig({
{ icon: 'discord', link: 'https://discord.homebox.software' },
{ icon: 'github', link: 'https://git.homebox.software' },
{ icon: 'mastodon', link: 'https://noc.social/@sysadminszone' },
],
footer: {
message: 'HomeBox is an open-source project under the <a href="https://github.com/sysadminsmedia/homebox/blob/main/LICENSE">MIT license</a>',
copyright: '&copy; <a href="https://sysadminsmedia.com/">Sysadmins Media</a>, 2025',
}
},
markdown: {
image: {
lazyLoading: true
}
]
}
})

View File

@@ -4,15 +4,9 @@ export default [
items: [
{text: 'Quick Start', link: '/en/quick-start'},
{text: 'Installation', link: '/en/installation'},
{text: 'Configure', link: '/en/configure'},
{text: 'Upgrade Guide', link: '/en/upgrade'},
]
},
{
text: 'Users Guide',
items: [
{text: 'Organizing Items', link: '/en/user-guide/organizing-items'},
{text: 'Tips and Tricks', link: '/en/user-guide/tips-tricks'},
{text: 'Organizing Your Items', link: '/en/organizing-items'},
{text: 'Configure Homebox', link: '/en/configure-homebox'},
{text: 'Tips and Tricks', link: '/en/tips-tricks'}
]
},
{

View File

@@ -559,9 +559,6 @@
"Bearer": []
}
],
"consumes": [
"multipart/form-data"
],
"produces": [
"application/json"
],
@@ -733,9 +730,6 @@
"Bearer": []
}
],
"consumes": [
"multipart/form-data"
],
"produces": [
"application/json"
],
@@ -920,13 +914,6 @@
],
"summary": "Get Maintenance Log",
"parameters": [
{
"type": "string",
"description": "Item ID",
"name": "id",
"in": "path",
"required": true
},
{
"enum": [
"scheduled",
@@ -969,13 +956,6 @@
],
"summary": "Create Maintenance Entry",
"parameters": [
{
"type": "string",
"description": "Item ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Entry Data",
"name": "payload",
@@ -1032,123 +1012,6 @@
}
}
},
"/v1/labelmaker/assets/{id}": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Items"
],
"summary": "Get Asset label",
"parameters": [
{
"type": "string",
"description": "Asset ID",
"name": "id",
"in": "path",
"required": true
},
{
"type": "boolean",
"description": "Print this label, defaults to false",
"name": "print",
"in": "query"
}
],
"responses": {
"200": {
"description": "image/png",
"schema": {
"type": "string"
}
}
}
}
},
"/v1/labelmaker/item/{id}": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Items"
],
"summary": "Get Item label",
"parameters": [
{
"type": "string",
"description": "Item ID",
"name": "id",
"in": "path",
"required": true
},
{
"type": "boolean",
"description": "Print this label, defaults to false",
"name": "print",
"in": "query"
}
],
"responses": {
"200": {
"description": "image/png",
"schema": {
"type": "string"
}
}
}
}
},
"/v1/labelmaker/location/{id}": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Locations"
],
"summary": "Get Location label",
"parameters": [
{
"type": "string",
"description": "Location ID",
"name": "id",
"in": "path",
"required": true
},
{
"type": "boolean",
"description": "Print this label, defaults to false",
"name": "print",
"in": "query"
}
],
"responses": {
"200": {
"description": "image/png",
"schema": {
"type": "string"
}
}
}
}
},
"/v1/labels": {
"get": {
"security": [
@@ -1564,13 +1427,6 @@
],
"summary": "Update Maintenance Entry",
"parameters": [
{
"type": "string",
"description": "Maintenance ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Entry Data",
"name": "payload",
@@ -1603,15 +1459,6 @@
"Maintenance"
],
"summary": "Delete Maintenance Entry",
"parameters": [
{
"type": "string",
"description": "Maintenance ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
@@ -1694,6 +1541,13 @@
],
"summary": "Test Notifier",
"parameters": [
{
"type": "string",
"description": "Notifier ID",
"name": "id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "URL",
@@ -1891,6 +1745,20 @@
],
"summary": "User Login",
"parameters": [
{
"type": "string",
"example": "admin@admin.com",
"description": "string",
"name": "username",
"in": "formData"
},
{
"type": "string",
"example": "admin",
"description": "string",
"name": "password",
"in": "formData"
},
{
"description": "Login Data",
"name": "payload",
@@ -2367,9 +2235,6 @@
"soldTo": {
"type": "string"
},
"syncChildItemsLocations": {
"type": "boolean"
},
"updatedAt": {
"type": "string"
},
@@ -2414,10 +2279,6 @@
"archived": {
"type": "boolean"
},
"assetId": {
"type": "string",
"example": "0"
},
"createdAt": {
"type": "string"
},
@@ -2572,9 +2433,6 @@
"type": "string",
"maxLength": 255
},
"syncChildItemsLocations": {
"type": "boolean"
},
"warrantyDetails": {
"type": "string"
},
@@ -3060,17 +2918,6 @@
}
}
},
"services.Latest": {
"type": "object",
"properties": {
"date": {
"type": "string"
},
"version": {
"type": "string"
}
}
},
"services.UserRegistration": {
"type": "object",
"properties": {
@@ -3103,12 +2950,6 @@
"health": {
"type": "boolean"
},
"labelPrinting": {
"type": "boolean"
},
"latest": {
"$ref": "#/definitions/services.Latest"
},
"message": {
"type": "string"
},
@@ -3198,15 +3039,13 @@
"type": "object",
"properties": {
"password": {
"type": "string",
"example": "admin"
"type": "string"
},
"stayLoggedIn": {
"type": "boolean"
},
"username": {
"type": "string",
"example": "admin@admin.com"
"type": "string"
}
}
},

View File

@@ -1,37 +0,0 @@
---
layout: page
sidebar: false
---
<script setup lang="ts">
import { useData } from 'vitepress';
const elementScript = document.createElement('script');
elementScript.src = 'https://unpkg.com/@stoplight/elements/web-components.min.js';
document.head.appendChild(elementScript);
const elementStyle = document.createElement('link');
elementStyle.rel = 'stylesheet';
elementStyle.href = 'https://unpkg.com/@stoplight/elements/styles.min.css';
document.head.appendChild(elementStyle);
const { isDark } = useData();
let theme = 'light';
if (isDark.value) {
theme = 'dark';
}
</script>
<style>
.TryItPanel {
display: none;
}
</style>
<elements-api
apiDescriptionUrl="https://cdn.jsdelivr.net/gh/sysadminsmedia/homebox@main/docs/docs/api/openapi-2.0.json"
router="hash"
layout="responsive"
hideSchemas="true"
:data-theme="theme"
/>

View File

@@ -0,0 +1,59 @@
# Configure Homebox
## Env Variables & Configuration
| Variable | Default | Description |
| ------------------------------------ | ---------------------- | ---------------------------------------------------------------------------------- |
| HBOX_MODE | `production` | application mode used for runtime behavior can be one of: `development`, `production` |
| 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_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 | 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_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 |
| HBOX_MAILER_USERNAME | | email user to use |
| HBOX_MAILER_PASSWORD | | email password to use |
| HBOX_MAILER_FROM | | email from address to use |
| HBOX_SWAGGER_HOST | 7745 | swagger host to use, if not set swagger will be disabled |
| HBOX_SWAGGER_SCHEMA | `http` | swagger schema to use, can be one of: `http`, `https` |
::: tip "CLI Arguments"
If you're deploying without docker you can use command line arguments to configure the application. Run `homebox --help` for more information.
```sh
Usage: api [options] [arguments]
OPTIONS
--mode/$HBOX_MODE <string> (default: development)
--web-port/$HBOX_WEB_PORT <string> (default: 7745)
--web-host/$HBOX_WEB_HOST <string>
--web-max-upload-size/$HBOX_WEB_MAX_UPLOAD_SIZE <int> (default: 10)
--storage-data/$HBOX_STORAGE_DATA <string> (default: ./.data)
--storage-sqlite-url/$HBOX_STORAGE_SQLITE_URL <string> (default: ./.data/homebox.db?_fk=1)
--log-level/$HBOX_LOG_LEVEL <string> (default: info)
--log-format/$HBOX_LOG_FORMAT <string> (default: text)
--mailer-host/$HBOX_MAILER_HOST <string>
--mailer-port/$HBOX_MAILER_PORT <int>
--mailer-username/$HBOX_MAILER_USERNAME <string>
--mailer-password/$HBOX_MAILER_PASSWORD <string>
--mailer-from/$HBOX_MAILER_FROM <string>
--swagger-host/$HBOX_SWAGGER_HOST <string> (default: localhost:7745)
--swagger-scheme/$HBOX_SWAGGER_SCHEME <string> (default: http)
--demo/$HBOX_DEMO <bool>
--debug-enabled/$HBOX_DEBUG_ENABLED <bool> (default: false)
--debug-port/$HBOX_DEBUG_PORT <string> (default: 4000)
--options-allow-registration/$HBOX_OPTIONS_ALLOW_REGISTRATION <bool> (default: true)
--options-auto-increment-asset-id/$HBOX_OPTIONS_AUTO_INCREMENT_ASSET_ID <bool> (default: true)
--options-currency-config/$HBOX_OPTIONS_CURRENCY_CONFIG <string>
--help/-h display this help message
```
:::

View File

@@ -1,111 +0,0 @@
---
outline: false
aside: false
---
# Configure Homebox
## Env Variables & Configuration
| Variable | Default | Description |
|-----------------------------------------|----------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| HBOX_MODE | `production` | application mode used for runtime behavior can be one of: `development`, `production` |
| 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_CURRENCY_CONFIG | | json configuration file containing additional currencie |
| HBOX_WEB_MAX_UPLOAD | 10 | maximum file upload size supported in MB |
| 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_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 |
| HBOX_MAILER_USERNAME | | email user to use |
| HBOX_MAILER_PASSWORD | | email password to use |
| HBOX_MAILER_FROM | | email from address to use |
| HBOX_SWAGGER_HOST | 7745 | swagger host to use, if not set swagger will be disabled |
| HBOX_SWAGGER_SCHEMA | `http` | swagger schema to use, can be one of: `http`, `https` |
| HBOX_DATABASE_TYPE | sqlite3 | sets the correct database type (`sqlite3` or `postgres`) |
| HBOX_DATABASE_SQLITE_PATH | ./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1 | sets the directory path for Sqlite |
| HBOX_DATABASE_HOST | | sets the hostname for a postgres database |
| HBOX_DATABASE_PORT | | sets the port for a postgres database |
| HBOX_DATABASE_USERNAME | | sets the username for a postgres connection |
| HBOX_DATABASE_PASSWORD | | sets the password for a postgres connection |
| HBOX_DATABASE_DATABASE | | sets the database for a postgres connection |
| HBOX_DATABASE_SSL_MODE | | sets the sslmode for a postgres connection |
| HBOX_OPTIONS_CHECK_GITHUB_RELEASE | true | check for new github releases |
| HBOX_LABEL_MAKER_WIDTH | 526 | width for generated labels in pixels |
| HBOX_LABEL_MAKER_HEIGHT | 200 | height for generated labels in pixels |
| HBOX_LABEL_MAKER_PADDING | 32 | space between elements on label |
| HBOX_LABEL_MAKER_FONT_SIZE | 32.0 | font size for label text |
| HBOX_LABEL_MAKER_PRINT_COMMAND | | the command to use for printing labels. if empty, label printing is disabled. <span v-pre>`{{.FileName}}`</span> in the command will be replaced with the png filename of the label |
| HBOX_LABEL_MAKER_DYNAMIC_LENGTH | true | allow label generation with open length. `HBOX_LABEL_MAKER_HEIGHT` is still used for layout and minimal height. If not used, long text may be cut off, but all labels have the same size. |
| HBOX_LABEL_MAKER_ADDITIONAL_INFORMATION | | Additional information added to the label like name or phone number |
::: warning Security Considerations
For postgreSQL in production:
- Do not use the default `postgres` user
- Do not use the default `postgres` database
- Always use a strong unique password
- Always use SSL (`sslmode=require` or `sslmode=verify-full`)
- Consider using a connection pooler like `pgbouncer`
For SQLite in production:
- Secure file permissions for the database file (e.g. `chmod 600`)
- Use a secure directory for the database file
- Use a secure backup strategy
- Monitor the file size and consider using a different database for large installations
:::
::: tip CLI Arguments
If you're deploying without docker you can use command line arguments to configure the application. Run `homebox --help` for more information.
```sh
Usage: api [options] [arguments]
OPTIONS
--mode/$HBOX_MODE <string> (default: development)
--web-port/$HBOX_WEB_PORT <string> (default: 7745)
--web-host/$HBOX_WEB_HOST <string>
--web-max-file-upload/$HBOX_WEB_MAX_FILE_UPLOAD <int> (default: 10)
--storage-data/$HBOX_STORAGE_DATA <string> (default: ./.data)
--log-level/$HBOX_LOG_LEVEL <string> (default: info)
--log-format/$HBOX_LOG_FORMAT <string> (default: text)
--mailer-host/$HBOX_MAILER_HOST <string>
--mailer-port/$HBOX_MAILER_PORT <int>
--mailer-username/$HBOX_MAILER_USERNAME <string>
--mailer-password/$HBOX_MAILER_PASSWORD <string>
--mailer-from/$HBOX_MAILER_FROM <string>
--swagger-host/$HBOX_SWAGGER_HOST <string> (default: localhost:7745)
--swagger-scheme/$HBOX_SWAGGER_SCHEME <string> (default: http)
--demo/$HBOX_DEMO <bool>
--debug-enabled/$HBOX_DEBUG_ENABLED <bool> (default: false)
--debug-port/$HBOX_DEBUG_PORT <string> (default: 4000)
--database-type/$HBOX_DATABASE_TYPE <string> (default: sqlite3)
--database-sqlite-path/$HBOX_DATABASE_SQLITE_PATH <string> (default: ./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1)
--database-host/$HBOX_DATABASE_HOST <string>
--database-port/$HBOX_DATABASE_PORT <string>
--database-username/$HBOX_DATABASE_USERNAME <string>
--database-password/$HBOX_DATABASE_PASSWORD <string>
--database-database/$HBOX_DATABASE_DATABASE <string>
--database-ssl-mode/$HBOX_DATABASE_SSL_MODE <string>
--options-allow-registration/$HBOX_OPTIONS_ALLOW_REGISTRATION <bool> (default: true)
--options-auto-increment-asset-id/$HBOX_OPTIONS_AUTO_INCREMENT_ASSET_ID <bool> (default: true)
--options-currency-config/$HBOX_OPTIONS_CURRENCY_CONFIG <string>
--options-check-github-release/$HBOX_OPTIONS_CHECK_GITHUB_RELEASE <bool> (default: true)
--label-maker-width/$HBOX_LABEL_MAKER_WIDTH <int> (default: 526)
--label-maker-height/$HBOX_LABEL_MAKER_HEIGHT <int> (default: 200)
--label-maker-padding/$HBOX_LABEL_MAKER_PADDING <int> (default: 32)
--label-maker-margin/$HBOX_LABEL_MAKER_MARGIN <int> (default: 32)
--label-maker-font-size/$HBOX_LABEL_MAKER_FONT_SIZE <float> (default: 32.0)
--label-maker-print-command/$HBOX_LABEL_MAKER_PRINT_COMMAND <string>
--label-maker-additional-information/$HBOX_LABEL_MAKER_DYNAMIC_LENGTH <string> (default: true)
--label-maker-additional-information/$HBOX_LABEL_MAKER_ADDITIONAL_INFORMATION <string>
--help/-h display this help message
```
:::

View File

@@ -1,60 +0,0 @@
# Shadcn-Vue
[Shadcn-Vue](https://www.shadcn-vue.com/) is a collection of Vue components based on [shadcn/ui](https://ui.shadcn.com/). We are currently in the process of migrating from DaisyUI to Shadcn-Vue for our component system.
## What is shadcn-vue?
To quote shadcn-vue:
> This is NOT a component library. It's a collection of re-usable components that you can copy and paste or use the CLI to add to your apps.
> What do you mean not a component library?
> It means you do not install it as a dependency. It is not available or distributed via npm, with no plans to publish it.
> Pick the components you need. Use the CLI to automatically add the components, or copy and paste the code into your project and customize to your needs. The code is yours.
The key advantage of this approach is that we have full control over the components and can modify them to suit our specific needs without being constrained by a third-party dependency.
## Adding Components
1. Add components using the CLI:
```bash
pnpx shadcn-vue@latest add [component-name]
```
For example:
```bash
pnpx shadcn-vue@latest add button
```
2. The components will be added to the route in the `components/ui` it then needs to be moved to `frontend/components/ui` for use.
## Usage
1. Import components from the components directory:
```vue
import { Button } from '@/components/ui/button'
```
2. Components can be used with their respective props and slots as documented in the shadcn-vue documentation.
## Modifying Components
When modifying components, follow these best practices:
1. If you need to modify a component for a specific use case:
- Copy the component and give it a name that reflects its purpose
- Keep the original shadcn component intact for other uses
2. When making global changes:
- Modify the component in the `components/ui` directory
- Document any significant changes in comments
## Testing without DaisyUI
During the migration process, you can test without DaisyUI using these commands:
```bash
DISABLE_DAISYUI=true; task ui:dev
```
or
```bash
DISABLE_DAISYUI=true; task ui:fix
```

View File

@@ -40,7 +40,7 @@ Homebox is the inventory and organization system built for the Home User! With a
- _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.
- _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. However, a Postgres backend is also supported for larger installations.
- _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.
## Project Status
@@ -53,8 +53,8 @@ There are a lot of great inventory management systems out there, but none of the
### Spreadsheet
That's a fair point. If your needs can be fulfilled by a Spreadsheet, We'd suggest using that instead. We've found spreadsheets get pretty unwieldy when you have a lot of data, and it's hard to keep track of what's where. We also wanted to be able to search and filter my data in a more robust way than a spreadsheet can provide. We also wanted to leave the door open for more advanced features in the future like maintenance logs, moving label generators, and more.
That's a fair point. If your needs can be fulfilled by a Spreadsheet, I'd suggest using that instead. I've found spreadsheets get pretty unwieldy when you have a lot of data, and it's hard to keep track of what's where. I also wanted to be able to search and filter my data in a more robust way than a spreadsheet can provide. I also wanted to leave the door open for more advanced features in the future like maintenance logs, moving label generators, and more.
### Snipe-It?
Snipe-It is the gold standard for IT management. If your use-case is to manage consumables and IT physical infrastructure, We highly suggest you look at Snipe-It over Homebox, it's just more purpose built for that use case. Homebox is, in contrast, purpose built for the home user, which means that we try to focus on keeping things simple and easy to use. Lowering the friction for creating items and managing them is a key goal of Homebox which means you lose out on some of the more advanced features. In most cases, this is a good trade-off.
Snipe-It is the gold standard for IT management. If your use-case is to manage consumables and IT physical infrastructure, I highly suggest you look at Snipe-It over Homebox, it's just more purpose built for that use case. Homebox is, in contrast, purpose built for the home user, which means that we try to focus on keeping things simple and easy to use. Lowering the friction for creating items and managing them is a key goal of Homebox which means you lose out on some of the more advanced features. In most cases, this is a good trade-off.

View File

@@ -5,9 +5,6 @@ There are two main ways to run the application.
1. As a [Docker](https://www.docker.com/) container.
2. Using the correct executable for your platform by downloading it from the [Releases](https://github.com/sysadminsmedia/homebox/releases).
::: info Configuration Options
The application can be configured using environment variables. You can find a list of all available options in the [configuration section](./configure).
:::
## Docker
@@ -51,7 +48,7 @@ services:
environment:
- HBOX_LOG_LEVEL=info
- HBOX_LOG_FORMAT=text
- HBOX_WEB_MAX_FILE_UPLOAD=10
- HBOX_WEB_MAX_UPLOAD_SIZE=10
volumes:
- homebox-data:/data/
ports:
@@ -99,4 +96,4 @@ You can learn more about Docker by [reading the official Docker documentation.](
1. Download the appropriate release for your CPU architecture from the [releases page on GitHub](https://github.com/sysadminsmedia/homebox/releases). (Use `homebox_Darwin_x86_64.tar.gz` for Intel-based macs and `homebox_Darwin_arm64.tar.gz` for Apple Silicon)
2. Extract the archive.
3. Run the `homebox` executable.
4. The web interface will be accessible on port 7745 by default. Access the page by navigating to `http://local.ip.address:7745/` (replace with the right ip address)
4. The web interface will be accessible on port 7745 by default. Access the page by navigating to `http://local.ip.address:7745/` (replace with the right ip address)

Some files were not shown because too many files have changed in this diff Show More