Compare commits
4 Commits
v0.22.0-rc
...
katos/purc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
809b0db5e5 | ||
|
|
ae8f568bfa | ||
|
|
87725348be | ||
|
|
da78f13513 |
@@ -1,3 +1,5 @@
|
|||||||
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:22-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
|
||||||
@@ -3,7 +3,13 @@
|
|||||||
{
|
{
|
||||||
"name": "Node.js & TypeScript",
|
"name": "Node.js & TypeScript",
|
||||||
"build": {
|
"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.
|
// Configure tool-specific properties.
|
||||||
@@ -29,6 +35,6 @@
|
|||||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||||
"remoteUser": "node",
|
"remoteUser": "node",
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/go:1": "1.24"
|
"ghcr.io/devcontainers/features/go:1": "1.21"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
.gitattributes
vendored
@@ -1,3 +0,0 @@
|
|||||||
backend/internal/data/ent/** linguist-generated=true
|
|
||||||
backend/internal/data/ent/schema/** linguist-generated=false
|
|
||||||
frontend/lib/api/types/** linguist-generated=true
|
|
||||||
40
.github/AGENTS.md
vendored
@@ -1,40 +0,0 @@
|
|||||||
This is a Go based repository with a VueJS client for the frontend built with Vite and Nuxt, with ShadCN.
|
|
||||||
|
|
||||||
To make life easier, the use of a Taskfile is included for the majority of development commands.
|
|
||||||
|
|
||||||
Please follow these guidelines when contributing:
|
|
||||||
|
|
||||||
## Required Before Each Commit
|
|
||||||
- Generate Swagger Files: `task swag --force`
|
|
||||||
- Generate JS API Client: `task typescript-types --force`
|
|
||||||
- Lint Golang: `task go:lint`
|
|
||||||
- Lint frontend: `task ui:fix`
|
|
||||||
|
|
||||||
## Repository Structure
|
|
||||||
### Backend
|
|
||||||
- `backend/`: Contains the backend folders
|
|
||||||
- `backend/app`: Contains main app code including API endpoints
|
|
||||||
- `backend/internal/core`: Contains basic services such as currencies
|
|
||||||
- `backend/data`: Contains all information related to data, including `ent` schemas, repos, migrations, etc.
|
|
||||||
- `backend/data/migrations`: Contains migration data, the `sqlite3` sub-folder contains sqlite migrations, `postgres` sub-folder the postgres migrations, BOTH are REQUIRED.
|
|
||||||
- `backend/data/ent/schema`: Contains the actual `ent` data models.
|
|
||||||
- `backend/data/repo`: Contains the data repositories
|
|
||||||
- `backend/pkgs`: Contains general helper functions and services
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
- `frontend/`: Contains initial frontend files
|
|
||||||
- `frontend/components`: Contains the ShadCN components
|
|
||||||
- `frontend/locales`: Contains the i18n JSON for languages
|
|
||||||
- `frontend/pages`: Contains VueJS pages
|
|
||||||
- `frontend/test`: Contains Playwright setup
|
|
||||||
- `frontend/test/e2e`: Contains actual Playwright test files
|
|
||||||
|
|
||||||
### Docs
|
|
||||||
- `docs/`: Contains VitePress based documentation
|
|
||||||
|
|
||||||
## Key Guidelines
|
|
||||||
1. Follow best practices for the various programming languages
|
|
||||||
2. Maintain existing code structure and organization when possible
|
|
||||||
3. Use dependency injection when reasonable
|
|
||||||
4. Write tests for new functionality and after fixing bugs to validate they're fixed
|
|
||||||
5. Document changes to the `docs/` folder when appropriate
|
|
||||||
3
.github/FUNDING.yml
vendored
@@ -1,2 +1 @@
|
|||||||
open_collective: homebox
|
github: [tankerkiller125,katosdev]
|
||||||
github: [tankerkiller125,katosdev,tonyaellie]
|
|
||||||
|
|||||||
28
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,9 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: "Bug Report"
|
name: "Bug Report"
|
||||||
description: "Submit a bug report for the current release"
|
description: "Submit a bug report for the current release"
|
||||||
labels: ["🕷️ bug"]
|
labels: ["bug"]
|
||||||
projects: ["sysadminsmedia/2"]
|
projects: ["sysadminsmedia/2"]
|
||||||
type: "Bug"
|
|
||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: checks
|
id: checks
|
||||||
@@ -21,8 +20,6 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I already read the docs and didn't find an answer.
|
- label: I already read the docs and didn't find an answer.
|
||||||
required: true
|
required: true
|
||||||
- label: I can replicate the issue inside the Demo install.
|
|
||||||
required: true
|
|
||||||
- type: input
|
- type: input
|
||||||
id: homebox-version
|
id: homebox-version
|
||||||
attributes:
|
attributes:
|
||||||
@@ -58,29 +55,6 @@ body:
|
|||||||
- Other
|
- Other
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
|
||||||
id: database
|
|
||||||
attributes:
|
|
||||||
label: Database Type
|
|
||||||
description: What database backend are you using?
|
|
||||||
multiple: false
|
|
||||||
options:
|
|
||||||
- SQLite
|
|
||||||
- PostgreSQL
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
id: arch
|
|
||||||
attributes:
|
|
||||||
label: OS Architechture
|
|
||||||
description: What type of processor are you running on.
|
|
||||||
multiple: true
|
|
||||||
options:
|
|
||||||
- x86_64 (AMD, Intel)
|
|
||||||
- ARM64
|
|
||||||
- ARM/v7
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: os-details
|
id: os-details
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
11
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,11 +0,0 @@
|
|||||||
blank_issues_enabled: false
|
|
||||||
contact_links:
|
|
||||||
- name: GitHub Community Support
|
|
||||||
url: https://github.com/sysadminsmedia/homebox/discussions/categories/support
|
|
||||||
about: Get support for issues here
|
|
||||||
- name: Feature Requests
|
|
||||||
url: https://github.com/sysadminsmedia/homebox/discussions/categories/ideas
|
|
||||||
about: Have an idea for Homebox? Share it in our discussions forum. If we decide to take it on we will create an issue for it.
|
|
||||||
- name: Translate
|
|
||||||
url: https://translate.sysadminsmedia.com
|
|
||||||
about: Help us translate Homebox! All contributions and all languages welcome!
|
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
labels: ["⬆️ enhancement"]
|
---
|
||||||
|
name: "Feature Request"
|
||||||
|
description: "Submit a feature request for the current release"
|
||||||
|
labels: ["enhancement"]
|
||||||
|
projects: ["sysadminsmedia/2"]
|
||||||
body:
|
body:
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: problem-statement
|
id: problem-statement
|
||||||
@@ -26,5 +30,9 @@ body:
|
|||||||
label: Contributions
|
label: Contributions
|
||||||
description: Please confirm the following
|
description: Please confirm the following
|
||||||
options:
|
options:
|
||||||
- label: I have searched through existing ideas in the discussions to check if this is a duplicate
|
- label: I have searched through existing issues and feature requests to see if my idea has already been proposed.
|
||||||
required: true
|
required: true
|
||||||
|
- label: If this feature is accepted, I would be willing to help implement and maintain this feature.
|
||||||
|
required: false
|
||||||
|
- label: If this feature is accepted, I'm willing to sponsor the development of this feature.
|
||||||
|
required: false
|
||||||
BIN
.github/screenshots/1.png
vendored
|
Before Width: | Height: | Size: 53 KiB |
BIN
.github/screenshots/10.png
vendored
|
Before Width: | Height: | Size: 29 KiB |
BIN
.github/screenshots/2.png
vendored
|
Before Width: | Height: | Size: 81 KiB |
BIN
.github/screenshots/3.png
vendored
|
Before Width: | Height: | Size: 87 KiB |
BIN
.github/screenshots/4.png
vendored
|
Before Width: | Height: | Size: 83 KiB |
BIN
.github/screenshots/5.png
vendored
|
Before Width: | Height: | Size: 95 KiB |
BIN
.github/screenshots/6.png
vendored
|
Before Width: | Height: | Size: 61 KiB |
BIN
.github/screenshots/7.png
vendored
|
Before Width: | Height: | Size: 108 KiB |
BIN
.github/screenshots/8.png
vendored
|
Before Width: | Height: | Size: 85 KiB |
BIN
.github/screenshots/9.png
vendored
|
Before Width: | Height: | Size: 86 KiB |
8
.github/screenshots/readme.md
vendored
@@ -1,8 +0,0 @@
|
|||||||
# Screenshots
|
|
||||||
|
|
||||||
These screenshots are taken from our public [Demo](https://demo.homebox.software) instance.
|
|
||||||
Note that whilst we will make every effort to ensure that these are maintained and updated, they may be outdated or missing functionality and we would always advise reviewing our demo instances:
|
|
||||||
|
|
||||||
- [Demo](https://demo.homebox.software)
|
|
||||||
- [Nightly](https://nightly.homebox.software)
|
|
||||||
- [VNext](https://vnext.homebox.software/)
|
|
||||||
213
.github/scripts/update_currencies.py
vendored
@@ -1,213 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import csv
|
|
||||||
import io
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from requests.adapters import HTTPAdapter
|
|
||||||
from urllib3.util.retry import Retry
|
|
||||||
|
|
||||||
API_URL = 'https://restcountries.com/v3.1/all?fields=name,common,currencies'
|
|
||||||
# Default to a pinned commit for supply-chain security
|
|
||||||
DEFAULT_ISO_4217_URL = 'https://raw.githubusercontent.com/datasets/currency-codes/052b3088938ba32028a14e75040c286c5e142145/data/codes-all.csv'
|
|
||||||
ISO_4217_URL = os.environ.get('ISO_4217_URL', DEFAULT_ISO_4217_URL)
|
|
||||||
SAVE_PATH = Path('backend/internal/core/currencies/currencies.json')
|
|
||||||
TIMEOUT = 10 # seconds
|
|
||||||
|
|
||||||
# Known currency decimal overrides
|
|
||||||
CURRENCY_DECIMAL_OVERRIDES = {
|
|
||||||
"BTC": 8, # Bitcoin uses 8 decimal places
|
|
||||||
"JPY": 0, # Japanese Yen has no decimal places
|
|
||||||
"BHD": 3, # Bahraini Dinar uses 3 decimal places
|
|
||||||
}
|
|
||||||
DEFAULT_DECIMALS = 2
|
|
||||||
MIN_DECIMALS = 0
|
|
||||||
MAX_DECIMALS = 6
|
|
||||||
|
|
||||||
|
|
||||||
def setup_logging():
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s %(levelname)s: %(message)s'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_currency_decimals(code, iso_data):
|
|
||||||
"""
|
|
||||||
Get the decimal places for a currency code.
|
|
||||||
Checks overrides first, then ISO data, then uses default.
|
|
||||||
Clamps result to safe range [MIN_DECIMALS, MAX_DECIMALS].
|
|
||||||
"""
|
|
||||||
# Normalize the input code
|
|
||||||
normalized_code = (code or "").strip().upper()
|
|
||||||
|
|
||||||
# First check overrides
|
|
||||||
if normalized_code in CURRENCY_DECIMAL_OVERRIDES:
|
|
||||||
decimals = CURRENCY_DECIMAL_OVERRIDES[normalized_code]
|
|
||||||
# Then check ISO data
|
|
||||||
elif normalized_code in iso_data:
|
|
||||||
decimals = iso_data[normalized_code]
|
|
||||||
# Finally use default
|
|
||||||
else:
|
|
||||||
decimals = DEFAULT_DECIMALS
|
|
||||||
|
|
||||||
# Ensure it's an integer and clamp to safe range
|
|
||||||
try:
|
|
||||||
decimals = int(decimals)
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
decimals = DEFAULT_DECIMALS
|
|
||||||
|
|
||||||
return max(MIN_DECIMALS, min(MAX_DECIMALS, decimals))
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_iso_4217_data():
|
|
||||||
"""
|
|
||||||
Fetch ISO 4217 currency data to get minor units (decimal places).
|
|
||||||
Returns a dict mapping currency code to minor units.
|
|
||||||
"""
|
|
||||||
# Log the resolved URL for transparency
|
|
||||||
logging.info("Fetching ISO 4217 data from: %s", ISO_4217_URL)
|
|
||||||
if not ISO_4217_URL.lower().startswith("https://"):
|
|
||||||
logging.error("Refusing non-HTTPS ISO_4217_URL: %s", ISO_4217_URL)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
session = requests.Session()
|
|
||||||
retries = Retry(
|
|
||||||
total=3,
|
|
||||||
backoff_factor=1,
|
|
||||||
status_forcelist=[429, 500, 502, 503, 504],
|
|
||||||
allowed_methods=frozenset(['GET'])
|
|
||||||
)
|
|
||||||
session.mount('https://', HTTPAdapter(max_retries=retries))
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Add Accept header for CSV content
|
|
||||||
headers = {'Accept': 'text/csv'}
|
|
||||||
resp = session.get(ISO_4217_URL, timeout=TIMEOUT, headers=headers)
|
|
||||||
resp.raise_for_status()
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
logging.error("Failed to fetch ISO 4217 data: %s", e)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Parse CSV data
|
|
||||||
iso_data = {}
|
|
||||||
try:
|
|
||||||
# Decode with utf-8-sig to strip BOM if present
|
|
||||||
csv_content = resp.content.decode('utf-8-sig')
|
|
||||||
csv_reader = csv.DictReader(io.StringIO(csv_content))
|
|
||||||
|
|
||||||
for row in csv_reader:
|
|
||||||
code = row.get('AlphabeticCode', '').strip()
|
|
||||||
minor_unit = row.get('MinorUnit', '').strip()
|
|
||||||
|
|
||||||
if code and minor_unit != 'N.A.':
|
|
||||||
try:
|
|
||||||
# Convert minor unit to int (decimal places)
|
|
||||||
iso_data[code] = int(minor_unit) if minor_unit.isdigit() else 2
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
iso_data[code] = 2 # Default to 2 if parsing fails
|
|
||||||
|
|
||||||
logging.info("Successfully loaded decimal data for %d currencies from ISO 4217", len(iso_data))
|
|
||||||
return iso_data
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.error("Failed to parse ISO 4217 CSV data: %s", e)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_currencies():
|
|
||||||
# First, fetch ISO 4217 data for decimal places
|
|
||||||
iso_data = fetch_iso_4217_data()
|
|
||||||
|
|
||||||
session = requests.Session()
|
|
||||||
retries = Retry(
|
|
||||||
total=3,
|
|
||||||
backoff_factor=1,
|
|
||||||
status_forcelist=[429, 500, 502, 503, 504],
|
|
||||||
allowed_methods=frozenset(['GET'])
|
|
||||||
)
|
|
||||||
session.mount('https://', HTTPAdapter(max_retries=retries))
|
|
||||||
|
|
||||||
try:
|
|
||||||
resp = session.get(API_URL, timeout=TIMEOUT)
|
|
||||||
resp.raise_for_status()
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
logging.error("API request failed: %s", e)
|
|
||||||
return None # signal fatal
|
|
||||||
|
|
||||||
try:
|
|
||||||
countries = resp.json()
|
|
||||||
except ValueError as e:
|
|
||||||
logging.error("Failed to parse JSON response: %s", e)
|
|
||||||
return None # signal fatal
|
|
||||||
|
|
||||||
results = []
|
|
||||||
for country in countries:
|
|
||||||
country_name = country.get('name', {}).get('common') or "Unknown"
|
|
||||||
for code, info in country.get('currencies', {}).items():
|
|
||||||
# Get decimal places using the helper function
|
|
||||||
decimals = get_currency_decimals(code, iso_data)
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
'code': code,
|
|
||||||
'local': country_name,
|
|
||||||
'symbol': info.get('symbol', ''),
|
|
||||||
'name': info.get('name', ''),
|
|
||||||
'decimals': decimals
|
|
||||||
})
|
|
||||||
|
|
||||||
# sort by country name for consistency
|
|
||||||
return sorted(results, key=lambda x: x['local'].lower())
|
|
||||||
|
|
||||||
|
|
||||||
def load_existing(path: Path):
|
|
||||||
if not path.exists():
|
|
||||||
return []
|
|
||||||
try:
|
|
||||||
with path.open('r', encoding='utf-8') as f:
|
|
||||||
return json.load(f)
|
|
||||||
except (IOError, ValueError) as e:
|
|
||||||
logging.warning("Could not load existing file (%s): %s", path, e)
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def save_currencies(data, path: Path):
|
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
with path.open('w', encoding='utf-8') as f:
|
|
||||||
json.dump(data, f, ensure_ascii=False, indent=4)
|
|
||||||
logging.info("Wrote %d entries to %s", len(data), path)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
setup_logging()
|
|
||||||
logging.info("🔄 Starting currency update")
|
|
||||||
|
|
||||||
existing = load_existing(SAVE_PATH)
|
|
||||||
new = fetch_currencies()
|
|
||||||
|
|
||||||
# Fatal API / parse error → exit non-zero so the workflow will fail
|
|
||||||
if new is None:
|
|
||||||
logging.error("Aborting: failed to fetch or parse API data.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Empty array → log & exit zero (no file change)
|
|
||||||
if not new:
|
|
||||||
logging.warning("API returned empty list; skipping write.")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# Identical to existing → nothing to do
|
|
||||||
if new == existing:
|
|
||||||
logging.info("Up-to-date; skipping write.")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# Otherwise actually overwrite
|
|
||||||
save_currencies(new, SAVE_PATH)
|
|
||||||
logging.info("✅ Currency file updated.")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
108
.github/workflows/binaries-publish.yaml
vendored
@@ -1,20 +1,21 @@
|
|||||||
name: Publish Release Binaries
|
name: Publish Release Binaries
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
push:
|
||||||
tags: [ 'v*.*.*' ]
|
tags: [ 'v*.*.*' ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
# backend-tests:
|
||||||
|
# name: "Backend Server Tests"
|
||||||
|
# uses: sysadminsmedia/homebox/.github/workflows/partial-backend.yaml@main
|
||||||
|
|
||||||
|
# frontend-tests:
|
||||||
|
# name: "Frontend and End-to-End Tests"
|
||||||
|
# uses: sysadminsmedia/homebox/.github/workflows/partial-frontend.yaml@main
|
||||||
|
|
||||||
goreleaser:
|
goreleaser:
|
||||||
name: goreleaser
|
name: goreleaser
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
|
||||||
hashes: ${{ steps.binary.outputs.hashes }}
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
packages: write
|
|
||||||
id-token: write
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -23,111 +24,24 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
|
||||||
go-version: "1.24"
|
|
||||||
cache-dependency-path: backend/go.mod
|
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v2
|
- uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
version: 9.15.3
|
version: 7.30.1
|
||||||
|
|
||||||
- name: Build Frontend and Copy to Backend
|
- name: Build Frontend and Copy to Backend
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
run: |
|
run: |
|
||||||
pnpm install
|
pnpm install --shamefully-hoist
|
||||||
pnpm run build
|
pnpm run build
|
||||||
cp -r ./.output/public ../backend/app/api/static/
|
cp -r ./.output/public ../backend/app/api/static/
|
||||||
|
|
||||||
- name: Install CoSign
|
|
||||||
working-directory: backend
|
|
||||||
run: |
|
|
||||||
go install github.com/sigstore/cosign/cmd/cosign@latest
|
|
||||||
|
|
||||||
- name: Install Syft
|
|
||||||
working-directory: backend
|
|
||||||
run: |
|
|
||||||
go install github.com/anchore/syft/cmd/syft@latest
|
|
||||||
|
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
id: releaser
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
uses: goreleaser/goreleaser-action@v5
|
uses: goreleaser/goreleaser-action@v5
|
||||||
with:
|
with:
|
||||||
workdir: "backend"
|
workdir: "backend"
|
||||||
distribution: goreleaser
|
distribution: goreleaser
|
||||||
version: "~> v2"
|
version: latest
|
||||||
args: release --clean
|
args: release --clean
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
COSIGN_PWD: ${{ secrets.COSIGN_PWD }}
|
|
||||||
COSIGN_YES: "true"
|
|
||||||
|
|
||||||
- name: Generate binary hashes
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
id: binary
|
|
||||||
env:
|
|
||||||
ARTIFACTS: "${{ steps.releaser.outputs.artifacts }}"
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
checksum_file=$(echo "$ARTIFACTS" | jq -r '.[] | select (.type=="Checksum") | .path')
|
|
||||||
echo "hashes=$(cat $checksum_file | base64 -w0)" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Run GoReleaser No Release
|
|
||||||
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
|
||||||
uses: goreleaser/goreleaser-action@v5
|
|
||||||
with:
|
|
||||||
workdir: "backend"
|
|
||||||
distribution: goreleaser
|
|
||||||
version: "~> v2"
|
|
||||||
args: release --clean --snapshot --skip=publish
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
COSIGN_PWD: ${{ secrets.COSIGN_PWD }}
|
|
||||||
COSIGN_YES: "true"
|
|
||||||
|
|
||||||
binary-provenance:
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
needs: [ goreleaser ]
|
|
||||||
permissions:
|
|
||||||
actions: read # To read the workflow path.
|
|
||||||
id-token: write # To sign the provenance.
|
|
||||||
contents: write # To add assets to a release.
|
|
||||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.9.0
|
|
||||||
with:
|
|
||||||
base64-subjects: "${{ needs.goreleaser.outputs.hashes }}"
|
|
||||||
upload-assets: true # upload to a new release
|
|
||||||
|
|
||||||
verification-with-slsa-verifier:
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
needs: [goreleaser, binary-provenance]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions: read-all
|
|
||||||
steps:
|
|
||||||
- name: Install the verifier
|
|
||||||
uses: slsa-framework/slsa-verifier/actions/installer@v2.4.0
|
|
||||||
|
|
||||||
- name: Download assets
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
PROVENANCE: "${{ needs.binary-provenance.outputs.provenance-name }}"
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "*.tar.gz"
|
|
||||||
gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "*.zip"
|
|
||||||
gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "$PROVENANCE"
|
|
||||||
- name: Verify assets
|
|
||||||
env:
|
|
||||||
CHECKSUMS: ${{ needs.goreleaser.outputs.hashes }}
|
|
||||||
PROVENANCE: "${{ needs.binary-provenance.outputs.provenance-name }}"
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
checksums=$(echo "$CHECKSUMS" | base64 -d)
|
|
||||||
while read -r line; do
|
|
||||||
fn=$(echo $line | cut -d ' ' -f2)
|
|
||||||
echo "Verifying $fn"
|
|
||||||
slsa-verifier verify-artifact --provenance-path "$PROVENANCE" \
|
|
||||||
--source-uri "github.com/$GITHUB_REPOSITORY" \
|
|
||||||
--source-tag "$GITHUB_REF_NAME" \
|
|
||||||
"$fn"
|
|
||||||
done <<<"$checksums"
|
|
||||||
|
|||||||
41
.github/workflows/clear-stale-docker-images.yml
vendored
@@ -1,41 +0,0 @@
|
|||||||
name: Docker Cleanup
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '00 0 * * *'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
delete-old-images-main:
|
|
||||||
name: Delete Old Images for Main Repo
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
packages: write
|
|
||||||
steps:
|
|
||||||
- uses: dataaxiom/ghcr-cleanup-action@v1
|
|
||||||
with:
|
|
||||||
dry-run: true
|
|
||||||
delete-ghost-images: true
|
|
||||||
delete-partial-images: true
|
|
||||||
delete-orphaned-images: true
|
|
||||||
delete-untagged: true
|
|
||||||
validate: true
|
|
||||||
token: '${{ github.token }}'
|
|
||||||
package: homebox
|
|
||||||
use-regex: true
|
|
||||||
exclude-tags: latest,latest-rootless,main,main-rootless,nightly,nightly-rootless,*.*.*,0.*,0,*.*.*-rootless,0.*-rootless,0-rootless
|
|
||||||
older-than: 180 days
|
|
||||||
|
|
||||||
delete-old-images-devcache:
|
|
||||||
name: Delete Old Devcache Images
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
packages: write
|
|
||||||
steps:
|
|
||||||
- uses: dataaxiom/ghcr-cleanup-action@v1
|
|
||||||
with:
|
|
||||||
dry-run: false
|
|
||||||
delete-untagged: true
|
|
||||||
token: '${{ github.token }}'
|
|
||||||
package: devcache
|
|
||||||
older-than: 90 days
|
|
||||||
52
.github/workflows/copilot-setup-steps.yml
vendored
@@ -1,52 +0,0 @@
|
|||||||
name: "Copilot Setup Steps"
|
|
||||||
|
|
||||||
# Automatically run the setup steps when they are changed to allow for easy validation, and
|
|
||||||
# allow manual testing through the repository's "Actions" tab
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
paths:
|
|
||||||
- .github/workflows/copilot-setup-steps.yml
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- .github/workflows/copilot-setup-steps.yml
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot.
|
|
||||||
copilot-setup-steps:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
# Set the permissions to the lowest permissions possible needed for your steps.
|
|
||||||
# Copilot will be given its own token for its operations.
|
|
||||||
permissions:
|
|
||||||
# If you want to clone the repository as part of your setup steps, for example to install dependencies, you'll need the `contents: read` permission. If you don't clone the repository in your setup steps, Copilot will do this for you automatically after the steps complete.
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
# You can define any steps you want, and they will run before the agent starts.
|
|
||||||
# If you do not check out your code, Copilot will do this for you.
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "22"
|
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v3.0.0
|
|
||||||
with:
|
|
||||||
version: 9.12.2
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: "1.24"
|
|
||||||
cache-dependency-path: backend/go.mod
|
|
||||||
|
|
||||||
- name: Install Task
|
|
||||||
uses: arduino/setup-task@v1
|
|
||||||
with:
|
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Perform setup
|
|
||||||
run: task setup
|
|
||||||
248
.github/workflows/docker-publish-hardened.yaml
vendored
@@ -1,248 +0,0 @@
|
|||||||
name: Docker publish hardened
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '00 0 * * *'
|
|
||||||
push:
|
|
||||||
branches: [ "main" ]
|
|
||||||
paths:
|
|
||||||
- 'backend/**'
|
|
||||||
- 'frontend/**'
|
|
||||||
- 'Dockerfile.hardened'
|
|
||||||
- '.dockerignore'
|
|
||||||
- '.github/workflows/docker-publish-hardened.yaml'
|
|
||||||
tags: [ 'v*.*.*' ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ "main" ]
|
|
||||||
paths:
|
|
||||||
- 'backend/**'
|
|
||||||
- 'frontend/**'
|
|
||||||
- 'Dockerfile.hardened'
|
|
||||||
- '.dockerignore'
|
|
||||||
- '.github/workflows/docker-publish-hardened.yaml'
|
|
||||||
|
|
||||||
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/${{ github.repository }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
id-token: write
|
|
||||||
attestations: write
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
platform:
|
|
||||||
- linux/amd64
|
|
||||||
- linux/arm64
|
|
||||||
- linux/arm/v7
|
|
||||||
|
|
||||||
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: |
|
|
||||||
echo "BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_ENV
|
|
||||||
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
|
|
||||||
|
|
||||||
- name: Docker meta
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
|
|
||||||
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@184bdaa0721073962dff0199f1fb9940f07167d1
|
|
||||||
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@184bdaa0721073962dff0199f1fb9940f07167d1
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392
|
|
||||||
with:
|
|
||||||
image: ghcr.io/sysadminsmedia/binfmt:latest
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
|
|
||||||
with:
|
|
||||||
driver-opts: |
|
|
||||||
image=ghcr.io/sysadminsmedia/buildkit:master
|
|
||||||
|
|
||||||
- name: Build and push by digest
|
|
||||||
id: build
|
|
||||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
|
|
||||||
with:
|
|
||||||
context: . # Explicitly specify the build context
|
|
||||||
file: ./Dockerfile.hardened # 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 }}-hardened
|
|
||||||
cache-to: type=registry,ref=ghcr.io/sysadminsmedia/devcache:${{ env.PLATFORM_PAIR }}-${{ env.BRANCH }}-hardened,mode=max,ignore-error=true
|
|
||||||
build-args: |
|
|
||||||
VERSION=${{ github.ref_name }}
|
|
||||||
COMMIT=${{ github.sha }}
|
|
||||||
BUILD_TIME=${{ env.BUILD_TIME }}
|
|
||||||
provenance: mode=max
|
|
||||||
sbom: true
|
|
||||||
annotations: ${{ steps.meta.outputs.annotations }}
|
|
||||||
|
|
||||||
- name: Attest platform-specific images
|
|
||||||
uses: actions/attest-build-provenance@v1
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
with:
|
|
||||||
subject-name: ${{ env.GHCR_REPO }}
|
|
||||||
subject-digest: ${{ steps.build.outputs.digest }}
|
|
||||||
push-to-registry: true
|
|
||||||
|
|
||||||
- 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@ea165f8d65b6e75b540449e92b4886f43607fa02
|
|
||||||
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@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
|
||||||
with:
|
|
||||||
path: /tmp/digests
|
|
||||||
pattern: digests-*
|
|
||||||
merge-multiple: true
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
|
||||||
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@184bdaa0721073962dff0199f1fb9940f07167d1
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
|
|
||||||
with:
|
|
||||||
driver-opts: |
|
|
||||||
image=ghcr.io/sysadminsmedia/buildkit:master
|
|
||||||
|
|
||||||
- name: Docker meta
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
|
|
||||||
with:
|
|
||||||
images: |
|
|
||||||
name=${{ env.DOCKERHUB_REPO }},enable=${{ github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') }}
|
|
||||||
name=${{ env.GHCR_REPO }}
|
|
||||||
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=-hardened,onlatest=true
|
|
||||||
|
|
||||||
- name: Create manifest list and push GHCR
|
|
||||||
id: push-ghcr
|
|
||||||
working-directory: /tmp/digests
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
|
||||||
$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *) 2>&1 | tee /tmp/push-ghcr.out
|
|
||||||
digest=$(grep -oE 'sha256:[a-f0-9]{64}' /tmp/push-ghcr.out | head -n1 || true)
|
|
||||||
if [ -z "$digest" ]; then
|
|
||||||
echo "No digest found in imagetools output:"
|
|
||||||
cat /tmp/push-ghcr.out
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "digest=$digest" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Attest GHCR images
|
|
||||||
uses: actions/attest-build-provenance@v1
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
with:
|
|
||||||
subject-name: ${{ env.GHCR_REPO }}
|
|
||||||
subject-digest: ${{ steps.push-ghcr.outputs.digest }}
|
|
||||||
push-to-registry: true
|
|
||||||
|
|
||||||
- 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: |
|
|
||||||
set -euo pipefail
|
|
||||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
|
||||||
$(printf '${{ env.DOCKERHUB_REPO }}@sha256:%s ' *) 2>&1 | tee /tmp/push-dockerhub.out
|
|
||||||
digest=$(grep -oE 'sha256:[a-f0-9]{64}' /tmp/push-dockerhub.out | head -n1 || true)
|
|
||||||
if [ -z "$digest" ]; then
|
|
||||||
echo "No digest found in imagetools output:"
|
|
||||||
cat /tmp/push-dockerhub.out
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "digest=$digest" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Attest Dockerhub images
|
|
||||||
uses: actions/attest-build-provenance@v1
|
|
||||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
|
||||||
with:
|
|
||||||
subject-name: ${{ env.DOCKERHUB_REPO }}
|
|
||||||
subject-digest: ${{ steps.push-dockerhub.outputs.digest }}
|
|
||||||
push-to-registry: true
|
|
||||||
246
.github/workflows/docker-publish-rootless.yaml
vendored
@@ -2,196 +2,75 @@ name: Docker publish rootless
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '00 0 * * *'
|
- cron: '00 6 * * *'
|
||||||
push:
|
push:
|
||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
paths:
|
paths:
|
||||||
- 'backend/**'
|
- 'backend/**'
|
||||||
- 'frontend/**'
|
- 'frontend/**'
|
||||||
|
- 'Dockerfile'
|
||||||
- 'Dockerfile.rootless'
|
- 'Dockerfile.rootless'
|
||||||
- '.dockerignore'
|
- '.dockerignore'
|
||||||
- '.github/workflows/docker-publish-rootless.yaml'
|
- '.github/workflows'
|
||||||
ignore:
|
# Publish semver tags as releases.
|
||||||
- 'docs/**'
|
|
||||||
tags: [ 'v*.*.*' ]
|
tags: [ 'v*.*.*' ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
paths:
|
paths:
|
||||||
- 'backend/**'
|
- 'backend/**'
|
||||||
- 'frontend/**'
|
- 'frontend/**'
|
||||||
|
- 'Dockerfile'
|
||||||
- 'Dockerfile.rootless'
|
- 'Dockerfile.rootless'
|
||||||
- '.dockerignore'
|
- '.dockerignore'
|
||||||
- '.github/workflows/docker-publish-rootless.yaml'
|
- '.github/workflows'
|
||||||
ignore:
|
|
||||||
- 'docs/**'
|
|
||||||
|
|
||||||
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:
|
env:
|
||||||
DOCKERHUB_REPO: sysadminsmedia/homebox
|
# Use docker.io for Docker Hub if empty
|
||||||
GHCR_REPO: ghcr.io/${{ github.repository }}
|
REGISTRY: ghcr.io
|
||||||
|
# github.repository as <account>/<repo>
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build-rootless:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
id-token: write
|
# This is used to complete the identity challenge
|
||||||
|
# with sigstore/fulcio when running outside of PRs.
|
||||||
attestations: write
|
attestations: write
|
||||||
|
id-token: write
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
platform:
|
|
||||||
- linux/amd64
|
|
||||||
- linux/arm64
|
|
||||||
- linux/arm/v7
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Enable Debug Logs
|
|
||||||
run: echo "##[debug]Enabling debug logging"
|
|
||||||
env:
|
|
||||||
ACTIONS_RUNNER_DEBUG: true
|
|
||||||
ACTIONS_STEP_DEBUG: true
|
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Prepare
|
# Set up BuildKit Docker container builder to be able to build
|
||||||
run: |
|
# multi-platform images and export cache
|
||||||
platform=${{ matrix.platform }}
|
# https://github.com/docker/setup-buildx-action
|
||||||
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
|
|
||||||
|
|
||||||
- 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 }}
|
|
||||||
|
|
||||||
- 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 QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
with:
|
|
||||||
image: ghcr.io/sysadminsmedia/binfmt:latest
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3.0.0 # v3.0.0
|
||||||
with:
|
|
||||||
driver-opts: |
|
|
||||||
image=ghcr.io/sysadminsmedia/buildkit:master
|
|
||||||
|
|
||||||
- name: Build and push by digest
|
# Login against a Docker registry except on PR
|
||||||
id: build
|
# https://github.com/docker/login-action
|
||||||
uses: docker/build-push-action@v6
|
- name: Log into registry ${{ env.REGISTRY }}
|
||||||
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: mode=max
|
|
||||||
sbom: true
|
|
||||||
annotations: ${{ steps.meta.outputs.annotations }}
|
|
||||||
|
|
||||||
- name: Attest platform-specific images
|
|
||||||
uses: actions/attest-build-provenance@v1
|
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3.0.0 # v3.0.0
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ env.GHCR_REPO }}
|
registry: ${{ env.REGISTRY }}
|
||||||
subject-digest: ${{ steps.build.outputs.digest }}
|
|
||||||
push-to-registry: true
|
|
||||||
|
|
||||||
- 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 }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
# Extract metadata (tags, labels) for Docker
|
||||||
uses: docker/setup-buildx-action@v3
|
# https://github.com/docker/metadata-action
|
||||||
|
- name: Extract Docker metadata
|
||||||
|
id: metadata
|
||||||
|
uses: docker/metadata-action@v5.0.0 # v5.0.0
|
||||||
with:
|
with:
|
||||||
driver-opts: |
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
image=ghcr.io/sysadminsmedia/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 }}
|
|
||||||
tags: |
|
tags: |
|
||||||
type=ref,event=branch
|
type=ref,event=branch
|
||||||
type=ref,event=pr
|
type=ref,event=pr
|
||||||
@@ -201,50 +80,29 @@ jobs:
|
|||||||
type=schedule,pattern=nightly
|
type=schedule,pattern=nightly
|
||||||
flavor: |
|
flavor: |
|
||||||
suffix=-rootless,onlatest=true
|
suffix=-rootless,onlatest=true
|
||||||
|
|
||||||
- name: Create manifest list and push GHCR
|
# Build and push Docker image with Buildx (don't push on PR)
|
||||||
id: push-ghcr
|
# https://github.com/docker/build-push-action
|
||||||
working-directory: /tmp/digests
|
- name: Build and push Docker image
|
||||||
run: |
|
id: build-and-push
|
||||||
set -euo pipefail
|
uses: docker/build-push-action@v5.0.0 # v5.0.0
|
||||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
|
||||||
$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *) 2>&1 | tee /tmp/push-ghcr.out
|
|
||||||
digest=$(grep -oE 'sha256:[a-f0-9]{64}' /tmp/push-ghcr.out | head -n1 || true)
|
|
||||||
if [ -z "$digest" ]; then
|
|
||||||
echo "No digest found in imagetools output:"
|
|
||||||
cat /tmp/push-ghcr.out
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "digest=$digest" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Attest GHCR images
|
|
||||||
uses: actions/attest-build-provenance@v1
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ env.GHCR_REPO }}
|
context: .
|
||||||
subject-digest: ${{ steps.push-ghcr.outputs.digest }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
push-to-registry: true
|
tags: ${{ steps.metadata.outputs.tags }}
|
||||||
|
labels: ${{ steps.metadata.outputs.labels }}
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
|
# cache-from: type=gha
|
||||||
|
# cache-to: type=gha,mode=max
|
||||||
|
build-args: |
|
||||||
|
VERSION=${{ github.ref_name }}
|
||||||
|
COMMIT=${{ github.sha }}
|
||||||
|
|
||||||
- name: Create manifest list and push Dockerhub
|
- name: Attest
|
||||||
id: push-dockerhub
|
|
||||||
working-directory: /tmp/digests
|
|
||||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
|
||||||
$(printf '${{ env.DOCKERHUB_REPO }}@sha256:%s ' *) 2>&1 | tee /tmp/push-dockerhub.out
|
|
||||||
digest=$(grep -oE 'sha256:[a-f0-9]{64}' /tmp/push-dockerhub.out | head -n1 || true)
|
|
||||||
if [ -z "$digest" ]; then
|
|
||||||
echo "No digest found in imagetools output:"
|
|
||||||
cat /tmp/push-dockerhub.out
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "digest=$digest" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Attest Dockerhub images
|
|
||||||
uses: actions/attest-build-provenance@v1
|
uses: actions/attest-build-provenance@v1
|
||||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
id: attest
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ env.DOCKERHUB_REPO }}
|
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
subject-digest: ${{ steps.push-dockerhub.outputs.digest }}
|
subject-digest: ${{ steps.build-and-push.outputs.digest }}
|
||||||
push-to-registry: true
|
push-to-registry: true
|
||||||
|
|||||||
240
.github/workflows/docker-publish.yaml
vendored
@@ -2,17 +2,17 @@ name: Docker publish
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '00 0 * * *'
|
- cron: '00 6 * * *'
|
||||||
push:
|
push:
|
||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
paths:
|
paths:
|
||||||
- 'backend/**'
|
- 'backend/**'
|
||||||
- 'frontend/**'
|
- 'frontend/**'
|
||||||
- 'Dockerfile'
|
- 'Dockerfile'
|
||||||
|
- 'Dockerfile.rootless'
|
||||||
- '.dockerignore'
|
- '.dockerignore'
|
||||||
- '.github/workflows/docker-publish.yaml'
|
- '.github/workflows'
|
||||||
ignore:
|
# Publish semver tags as releases.
|
||||||
- 'docs/**'
|
|
||||||
tags: [ 'v*.*.*' ]
|
tags: [ 'v*.*.*' ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
@@ -20,171 +20,56 @@ on:
|
|||||||
- 'backend/**'
|
- 'backend/**'
|
||||||
- 'frontend/**'
|
- 'frontend/**'
|
||||||
- 'Dockerfile'
|
- 'Dockerfile'
|
||||||
|
- 'Dockerfile.rootless'
|
||||||
- '.dockerignore'
|
- '.dockerignore'
|
||||||
- '.github/workflows/docker-publish.yaml'
|
- '.github/workflows'
|
||||||
ignore:
|
|
||||||
- 'docs/**'
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DOCKERHUB_REPO: sysadminsmedia/homebox
|
# Use docker.io for Docker Hub if empty
|
||||||
GHCR_REPO: ghcr.io/${{ github.repository }}
|
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:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # Allows access to repository contents (read-only)
|
contents: read
|
||||||
packages: write # Allows pushing to GHCR
|
packages: write
|
||||||
id-token: write # Allows identity token write access for authentication
|
# This is used to complete the identity challenge
|
||||||
attestations: write # Needed for signing and attestation (if required)
|
# with sigstore/fulcio when running outside of PRs.
|
||||||
|
attestations: write
|
||||||
strategy:
|
id-token: write
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
platform:
|
|
||||||
- linux/amd64
|
|
||||||
- linux/arm64
|
|
||||||
- linux/arm/v7
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Prepare
|
# Set up BuildKit Docker container builder to be able to build
|
||||||
run: |
|
# multi-platform images and export cache
|
||||||
platform=${{ matrix.platform }}
|
# https://github.com/docker/setup-buildx-action
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
- 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 }}
|
|
||||||
|
|
||||||
- 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 QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
with:
|
|
||||||
image: ghcr.io/sysadminsmedia/binfmt:latest
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3.0.0 # v3.0.0
|
||||||
with:
|
|
||||||
driver-opts: |
|
|
||||||
image=ghcr.io/sysadminsmedia/buildkit:latest
|
|
||||||
|
|
||||||
- name: Build and push by digest
|
# Login against a Docker registry except on PR
|
||||||
id: build
|
# https://github.com/docker/login-action
|
||||||
uses: docker/build-push-action@v6
|
- name: Log into registry ${{ env.REGISTRY }}
|
||||||
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: mode=max
|
|
||||||
sbom: true
|
|
||||||
annotations: ${{ steps.meta.outputs.annotations }}
|
|
||||||
|
|
||||||
- name: Attest platform-specific images
|
|
||||||
uses: actions/attest-build-provenance@v1
|
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3.0.0 # v3.0.0
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ env.GHCR_REPO }}
|
registry: ${{ env.REGISTRY }}
|
||||||
subject-digest: ${{ steps.build.outputs.digest }}
|
|
||||||
push-to-registry: true
|
|
||||||
|
|
||||||
- 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
|
|
||||||
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 }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
# Extract metadata (tags, labels) for Docker
|
||||||
uses: docker/setup-buildx-action@v3
|
# https://github.com/docker/metadata-action
|
||||||
with:
|
- name: Extract Docker metadata
|
||||||
driver-opts: |
|
|
||||||
image=ghcr.io/sysadminsmedia/buildkit:master
|
|
||||||
|
|
||||||
- name: Docker meta
|
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5.0.0 # v5.0.0
|
||||||
with:
|
with:
|
||||||
images: |
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
name=${{ env.DOCKERHUB_REPO }},enable=${{ github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') }}
|
|
||||||
name=${{ env.GHCR_REPO }}
|
|
||||||
tags: |
|
tags: |
|
||||||
type=ref,event=branch
|
type=ref,event=branch
|
||||||
type=ref,event=pr
|
type=ref,event=pr
|
||||||
@@ -193,49 +78,28 @@ jobs:
|
|||||||
type=semver,pattern={{major}}
|
type=semver,pattern={{major}}
|
||||||
type=schedule,pattern=nightly
|
type=schedule,pattern=nightly
|
||||||
|
|
||||||
- name: Create manifest list and push GHCR
|
# Build and push Docker image with Buildx (don't push on PR)
|
||||||
id: push-ghcr
|
# https://github.com/docker/build-push-action
|
||||||
working-directory: /tmp/digests
|
- name: Build and push Docker image
|
||||||
run: |
|
id: build-and-push
|
||||||
set -euo pipefail
|
uses: docker/build-push-action@v5.0.0 # v5.0.0
|
||||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
|
||||||
$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *) 2>&1 | tee /tmp/push-ghcr.out
|
|
||||||
digest=$(grep -oE 'sha256:[a-f0-9]{64}' /tmp/push-ghcr.out | head -n1 || true)
|
|
||||||
if [ -z "$digest" ]; then
|
|
||||||
echo "No digest found in imagetools output:"
|
|
||||||
cat /tmp/push-ghcr.out
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "digest=$digest" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Attest GHCR images
|
|
||||||
uses: actions/attest-build-provenance@v1
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ env.GHCR_REPO }}
|
context: .
|
||||||
subject-digest: ${{ steps.push-ghcr.outputs.digest }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
push-to-registry: true
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
- name: Create manifest list and push Dockerhub
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
id: push-dockerhub
|
# cache-from: type=gha
|
||||||
working-directory: /tmp/digests
|
# cache-to: type=gha,mode=max
|
||||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
build-args: |
|
||||||
run: |
|
VERSION=${{ github.ref_name }}
|
||||||
set -euo pipefail
|
COMMIT=${{ github.sha }}
|
||||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
|
||||||
$(printf '${{ env.DOCKERHUB_REPO }}@sha256:%s ' *) 2>&1 | tee /tmp/push-dockerhub.out
|
- name: Attest
|
||||||
digest=$(grep -oE 'sha256:[a-f0-9]{64}' /tmp/push-dockerhub.out | head -n1 || true)
|
|
||||||
if [ -z "$digest" ]; then
|
|
||||||
echo "No digest found in imagetools output:"
|
|
||||||
cat /tmp/push-dockerhub.out
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "digest=$digest" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Attest Dockerhub images
|
|
||||||
uses: actions/attest-build-provenance@v1
|
uses: actions/attest-build-provenance@v1
|
||||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
id: attest
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ env.DOCKERHUB_REPO }}
|
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
subject-digest: ${{ steps.push-dockerhub.outputs.digest }}
|
subject-digest: ${{ steps.build-and-push.outputs.digest }}
|
||||||
push-to-registry: true
|
push-to-registry: true
|
||||||
|
|||||||
95
.github/workflows/e2e-partial.yaml
vendored
@@ -1,95 +0,0 @@
|
|||||||
name: E2E (Playwright)
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
playwright-tests:
|
|
||||||
timeout-minutes: 60
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
shardIndex: [1,2,3,4]
|
|
||||||
shardTotal: [4]
|
|
||||||
name: E2E Playwright Testing ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
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.23"
|
|
||||||
cache-dependency-path: backend/go.mod
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: lts/*
|
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v3.0.0
|
|
||||||
with:
|
|
||||||
version: 9.12.2
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install
|
|
||||||
working-directory: frontend
|
|
||||||
|
|
||||||
- name: Install Go Dependencies
|
|
||||||
run: go mod download
|
|
||||||
working-directory: backend
|
|
||||||
|
|
||||||
- name: Run E2E Tests
|
|
||||||
run: task test:e2e -- --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
name: Upload partial Playwright report
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
with:
|
|
||||||
name: blob-report-${{ matrix.shardIndex }}
|
|
||||||
path: frontend/blob-report/
|
|
||||||
retention-days: 2
|
|
||||||
|
|
||||||
merge-reports:
|
|
||||||
# Merge reports after playwright-tests, even if some shards have failed
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
needs: [playwright-tests]
|
|
||||||
name: Merge Playwright Reports
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: lts/*
|
|
||||||
- uses: pnpm/action-setup@v3.0.0
|
|
||||||
with:
|
|
||||||
version: 9.12.2
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install
|
|
||||||
working-directory: frontend
|
|
||||||
|
|
||||||
- name: Download blob reports from GitHub Actions Artifacts
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
path: frontend/all-blob-reports
|
|
||||||
pattern: blob-report-*
|
|
||||||
merge-multiple: true
|
|
||||||
|
|
||||||
- name: Merge into HTML Report
|
|
||||||
run: pnpm exec playwright merge-reports --reporter html,github ./all-blob-reports
|
|
||||||
working-directory: frontend
|
|
||||||
|
|
||||||
- name: Upload HTML report
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: html-report--attempt-${{ github.run_attempt }}
|
|
||||||
path: frontend/playwright-report
|
|
||||||
retention-days: 30
|
|
||||||
5
.github/workflows/partial-backend.yaml
vendored
@@ -12,8 +12,7 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.24"
|
go-version: "1.21"
|
||||||
cache-dependency-path: backend/go.mod
|
|
||||||
|
|
||||||
- name: Install Task
|
- name: Install Task
|
||||||
uses: arduino/setup-task@v1
|
uses: arduino/setup-task@v1
|
||||||
@@ -21,7 +20,7 @@ jobs:
|
|||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v7
|
uses: golangci/golangci-lint-action@v4
|
||||||
with:
|
with:
|
||||||
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
|
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
|
||||||
version: latest
|
version: latest
|
||||||
|
|||||||
76
.github/workflows/partial-frontend.yaml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Frontend
|
name: Frontend / E2E
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
@@ -15,10 +15,10 @@ jobs:
|
|||||||
|
|
||||||
- uses: pnpm/action-setup@v3.0.0
|
- uses: pnpm/action-setup@v3.0.0
|
||||||
with:
|
with:
|
||||||
version: 9.12.2
|
version: 6.0.2
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: pnpm install --shamefully-hoist
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
|
|
||||||
- name: Run Lint
|
- name: Run Lint
|
||||||
@@ -32,20 +32,6 @@ jobs:
|
|||||||
integration-tests:
|
integration-tests:
|
||||||
name: Integration Tests
|
name: Integration Tests
|
||||||
runs-on: ubuntu-latest
|
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:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -60,8 +46,7 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.23"
|
go-version: "1.21"
|
||||||
cache-dependency-path: backend/go.mod
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
@@ -69,7 +54,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: pnpm/action-setup@v3.0.0
|
- uses: pnpm/action-setup@v3.0.0
|
||||||
with:
|
with:
|
||||||
version: 9.12.2
|
version: 6.0.2
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
@@ -77,54 +62,3 @@ jobs:
|
|||||||
|
|
||||||
- name: Run Integration Tests
|
- name: Run Integration Tests
|
||||||
run: task test:ci
|
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.23"
|
|
||||||
cache-dependency-path: backend/go.mod
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: lts/*
|
|
||||||
|
|
||||||
- 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
|
|
||||||
13
.github/workflows/pull-requests.yaml
vendored
@@ -4,15 +4,10 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- vnext
|
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
- 'backend/**'
|
- 'backend/**'
|
||||||
- 'frontend/**'
|
- 'frontend/**'
|
||||||
- '.github/workflows/partial-backend.yaml'
|
|
||||||
- '.github/workflows/partial-frontend.yaml'
|
|
||||||
- '.github/workflows/e2e-partial.yaml'
|
|
||||||
- '.github/workflows/pull-requests.yaml'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
backend-tests:
|
backend-tests:
|
||||||
@@ -20,9 +15,5 @@ jobs:
|
|||||||
uses: ./.github/workflows/partial-backend.yaml
|
uses: ./.github/workflows/partial-backend.yaml
|
||||||
|
|
||||||
frontend-tests:
|
frontend-tests:
|
||||||
name: "Frontend Tests"
|
name: "Frontend and End-to-End Tests"
|
||||||
uses: ./.github/workflows/partial-frontend.yaml
|
uses: ./.github/workflows/partial-frontend.yaml
|
||||||
|
|
||||||
e2e-tests:
|
|
||||||
name: "End-to-End Playwright Tests"
|
|
||||||
uses: ./.github/workflows/e2e-partial.yaml
|
|
||||||
60
.github/workflows/update-currencies.yml
vendored
@@ -1,60 +0,0 @@
|
|||||||
name: Update Currencies
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update-currencies:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: '3.8'
|
|
||||||
cache: 'pip'
|
|
||||||
cache-dependency-path: .github/workflows/update-currencies/requirements.txt
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install -r .github/workflows/update-currencies/requirements.txt
|
|
||||||
|
|
||||||
- name: Run currency update script
|
|
||||||
run: python .github/scripts/update_currencies.py
|
|
||||||
|
|
||||||
- name: Check for currencies.json changes
|
|
||||||
run: |
|
|
||||||
if git diff --quiet -- backend/internal/core/currencies/currencies.json; then
|
|
||||||
echo "changed=false" >> $GITHUB_ENV
|
|
||||||
else
|
|
||||||
echo "changed=true" >> $GITHUB_ENV
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Create Pull Request
|
|
||||||
if: env.changed == 'true'
|
|
||||||
uses: peter-evans/create-pull-request@v7
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
branch: update-currencies
|
|
||||||
base: main
|
|
||||||
title: "Update currencies.json"
|
|
||||||
commit-message: "chore: update currencies.json"
|
|
||||||
path: .
|
|
||||||
add-paths: |
|
|
||||||
backend/internal/core/currencies/currencies.json
|
|
||||||
|
|
||||||
- name: No updates needed
|
|
||||||
if: env.changed == 'false'
|
|
||||||
run: echo "✅ currencies.json is already up-to-date"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
requests
|
|
||||||
14
.gitignore
vendored
@@ -2,9 +2,6 @@
|
|||||||
backend/.data/*
|
backend/.data/*
|
||||||
config.yml
|
config.yml
|
||||||
homebox.db
|
homebox.db
|
||||||
homebox.db-journal
|
|
||||||
homebox.db-shm
|
|
||||||
homebox.db-wal
|
|
||||||
.idea
|
.idea
|
||||||
.DS_Store
|
.DS_Store
|
||||||
test-mailer.json
|
test-mailer.json
|
||||||
@@ -36,7 +33,7 @@ go.work
|
|||||||
backend/.env
|
backend/.env
|
||||||
build/*
|
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/*
|
||||||
!backend/app/api/public/.gitkeep
|
!backend/app/api/public/.gitkeep
|
||||||
|
|
||||||
@@ -59,11 +56,4 @@ backend/app/api/static/public/*
|
|||||||
!backend/app/api/static/public/.gitkeep
|
!backend/app/api/static/public/.gitkeep
|
||||||
backend/api
|
backend/api
|
||||||
|
|
||||||
docs/.vitepress/cache/
|
docs/.vitepress/cache/
|
||||||
.data/
|
|
||||||
|
|
||||||
# Playwright
|
|
||||||
frontend/test-results/
|
|
||||||
frontend/playwright-report/
|
|
||||||
frontend/blob-report/
|
|
||||||
frontend/playwright/.cache/
|
|
||||||
603
.gitlab-ci.yml
@@ -1,603 +0,0 @@
|
|||||||
include:
|
|
||||||
- template: Jobs/SAST.gitlab-ci.yml
|
|
||||||
- component: $CI_SERVER_FQDN/components/code-quality-oss/codequality-os-scanners-integration/codequality-oss@1.1.4
|
|
||||||
- component: $CI_SERVER_FQDN/components/code-intelligence/golang-code-intel@v0.3.1
|
|
||||||
- component: $CI_SERVER_FQDN/components/code-intelligence/typescript-code-intel@v0.3.1
|
|
||||||
inputs:
|
|
||||||
node_version: 24
|
|
||||||
- component: $CI_SERVER_FQDN/components/secret-detection/secret-detection@2.1.0
|
|
||||||
|
|
||||||
variables:
|
|
||||||
GITLAB_ADVANCED_SAST_ENABLED: 'true'
|
|
||||||
ADVANCED_SAST_PARTIAL_SCAN: 'differential'
|
|
||||||
DOCKER_DRIVER: overlay2
|
|
||||||
DOCKER_TLS_CERTDIR: "/certs"
|
|
||||||
# Registry configuration - adjust as needed
|
|
||||||
CI_REGISTRY_IMAGE: $CI_REGISTRY/$CI_PROJECT_PATH
|
|
||||||
|
|
||||||
stages:
|
|
||||||
- test
|
|
||||||
- build-binaries
|
|
||||||
- build-docker
|
|
||||||
- release
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# Test Jobs
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
# Backend Tests (Go)
|
|
||||||
test:backend:
|
|
||||||
stage: test
|
|
||||||
image: golang:1.24
|
|
||||||
cache:
|
|
||||||
key:
|
|
||||||
files:
|
|
||||||
- backend/go.sum
|
|
||||||
paths:
|
|
||||||
- backend/.go-pkg-cache/
|
|
||||||
policy: pull-push
|
|
||||||
before_script:
|
|
||||||
- export GOMODCACHE=$(pwd)/backend/.go-pkg-cache
|
|
||||||
# Install Task
|
|
||||||
- sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin
|
|
||||||
script:
|
|
||||||
- cd backend
|
|
||||||
- task go:lint
|
|
||||||
- task go:build
|
|
||||||
- task go:coverage
|
|
||||||
coverage: '/coverage: \d+.\d+% of statements/'
|
|
||||||
artifacts:
|
|
||||||
reports:
|
|
||||||
coverage_report:
|
|
||||||
coverage_format: cobertura
|
|
||||||
path: backend/coverage.out
|
|
||||||
paths:
|
|
||||||
- backend/coverage.out
|
|
||||||
expire_in: 7 days
|
|
||||||
rules:
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
||||||
|
|
||||||
# Frontend Lint and Typecheck
|
|
||||||
test:frontend:lint:
|
|
||||||
stage: test
|
|
||||||
image: node:22-alpine
|
|
||||||
cache:
|
|
||||||
key:
|
|
||||||
files:
|
|
||||||
- frontend/pnpm-lock.yaml
|
|
||||||
paths:
|
|
||||||
- frontend/node_modules/
|
|
||||||
- .pnpm-store/
|
|
||||||
policy: pull-push
|
|
||||||
before_script:
|
|
||||||
- npm install -g pnpm@9.15.3
|
|
||||||
- pnpm config set store-dir $(pwd)/.pnpm-store
|
|
||||||
script:
|
|
||||||
- cd frontend
|
|
||||||
- pnpm install --frozen-lockfile
|
|
||||||
- pnpm run lint:ci
|
|
||||||
- pnpm run typecheck
|
|
||||||
rules:
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
||||||
|
|
||||||
# Frontend Integration Tests (SQLite)
|
|
||||||
test:frontend:integration:
|
|
||||||
stage: test
|
|
||||||
image: node:22
|
|
||||||
cache:
|
|
||||||
- key:
|
|
||||||
files:
|
|
||||||
- frontend/pnpm-lock.yaml
|
|
||||||
paths:
|
|
||||||
- frontend/node_modules/
|
|
||||||
- .pnpm-store/
|
|
||||||
policy: pull-push
|
|
||||||
- key:
|
|
||||||
files:
|
|
||||||
- backend/go.sum
|
|
||||||
paths:
|
|
||||||
- backend/.go-pkg-cache/
|
|
||||||
policy: pull-push
|
|
||||||
before_script:
|
|
||||||
- npm install -g pnpm@9.15.3
|
|
||||||
- pnpm config set store-dir $(pwd)/.pnpm-store
|
|
||||||
# Install Task
|
|
||||||
- sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin
|
|
||||||
# Install Go
|
|
||||||
- wget -q https://go.dev/dl/go1.24.0.linux-amd64.tar.gz
|
|
||||||
- tar -C /usr/local -xzf go1.24.0.linux-amd64.tar.gz
|
|
||||||
- export PATH=$PATH:/usr/local/go/bin
|
|
||||||
- export GOMODCACHE=$(pwd)/backend/.go-pkg-cache
|
|
||||||
script:
|
|
||||||
- cd frontend
|
|
||||||
- pnpm install --frozen-lockfile
|
|
||||||
- cd ..
|
|
||||||
- task test:ci
|
|
||||||
rules:
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
||||||
|
|
||||||
# Frontend Integration Tests (PostgreSQL Matrix)
|
|
||||||
test:frontend:integration:postgresql:
|
|
||||||
stage: test
|
|
||||||
image: node:22
|
|
||||||
services:
|
|
||||||
- name: postgres:${POSTGRES_VERSION}
|
|
||||||
alias: postgres
|
|
||||||
variables:
|
|
||||||
POSTGRES_USER: homebox
|
|
||||||
POSTGRES_PASSWORD: homebox
|
|
||||||
POSTGRES_DB: homebox
|
|
||||||
POSTGRES_HOST_AUTH_METHOD: trust
|
|
||||||
parallel:
|
|
||||||
matrix:
|
|
||||||
- POSTGRES_VERSION: ["17", "16", "15"]
|
|
||||||
cache:
|
|
||||||
- key:
|
|
||||||
files:
|
|
||||||
- frontend/pnpm-lock.yaml
|
|
||||||
paths:
|
|
||||||
- frontend/node_modules/
|
|
||||||
- .pnpm-store/
|
|
||||||
policy: pull-push
|
|
||||||
- key:
|
|
||||||
files:
|
|
||||||
- backend/go.sum
|
|
||||||
paths:
|
|
||||||
- backend/.go-pkg-cache/
|
|
||||||
policy: pull-push
|
|
||||||
before_script:
|
|
||||||
- npm install -g pnpm@9.15.3
|
|
||||||
- pnpm config set store-dir $(pwd)/.pnpm-store
|
|
||||||
# Install Task
|
|
||||||
- sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin
|
|
||||||
# Install Go
|
|
||||||
- wget -q https://go.dev/dl/go1.24.0.linux-amd64.tar.gz
|
|
||||||
- tar -C /usr/local -xzf go1.24.0.linux-amd64.tar.gz
|
|
||||||
- export PATH=$PATH:/usr/local/go/bin
|
|
||||||
- export GOMODCACHE=$(pwd)/backend/.go-pkg-cache
|
|
||||||
script:
|
|
||||||
- cd frontend
|
|
||||||
- pnpm install --frozen-lockfile
|
|
||||||
- cd ..
|
|
||||||
- task test:ci:postgresql
|
|
||||||
rules:
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
||||||
|
|
||||||
# E2E Tests (Playwright) - Sharded
|
|
||||||
test:e2e:playwright:
|
|
||||||
stage: test
|
|
||||||
image: mcr.microsoft.com/playwright:v1.48.2-jammy
|
|
||||||
timeout: 1h
|
|
||||||
parallel:
|
|
||||||
matrix:
|
|
||||||
- SHARD_INDEX: ["1", "2", "3", "4"]
|
|
||||||
SHARD_TOTAL: "4"
|
|
||||||
cache:
|
|
||||||
- key:
|
|
||||||
files:
|
|
||||||
- frontend/pnpm-lock.yaml
|
|
||||||
paths:
|
|
||||||
- frontend/node_modules/
|
|
||||||
- .pnpm-store/
|
|
||||||
policy: pull-push
|
|
||||||
- key:
|
|
||||||
files:
|
|
||||||
- backend/go.sum
|
|
||||||
paths:
|
|
||||||
- backend/.go-pkg-cache/
|
|
||||||
policy: pull-push
|
|
||||||
before_script:
|
|
||||||
- npm install -g pnpm@9.15.3
|
|
||||||
- pnpm config set store-dir $(pwd)/.pnpm-store
|
|
||||||
# Install Task
|
|
||||||
- sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin
|
|
||||||
# Install Go
|
|
||||||
- wget -q https://go.dev/dl/go1.24.0.linux-amd64.tar.gz
|
|
||||||
- tar -C /usr/local -xzf go1.24.0.linux-amd64.tar.gz
|
|
||||||
- export PATH=$PATH:/usr/local/go/bin
|
|
||||||
- export GOMODCACHE=$(pwd)/backend/.go-pkg-cache
|
|
||||||
script:
|
|
||||||
- cd frontend
|
|
||||||
- pnpm install --frozen-lockfile
|
|
||||||
- cd ..
|
|
||||||
- cd backend && go mod download
|
|
||||||
- task test:e2e -- --shard=$SHARD_INDEX/$SHARD_TOTAL
|
|
||||||
artifacts:
|
|
||||||
when: always
|
|
||||||
paths:
|
|
||||||
- frontend/blob-report/
|
|
||||||
expire_in: 2 days
|
|
||||||
rules:
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
||||||
|
|
||||||
# E2E Reports Merge
|
|
||||||
test:e2e:merge-reports:
|
|
||||||
stage: test
|
|
||||||
image: mcr.microsoft.com/playwright:v1.48.2-jammy
|
|
||||||
needs:
|
|
||||||
- test:e2e:playwright
|
|
||||||
cache:
|
|
||||||
key:
|
|
||||||
files:
|
|
||||||
- frontend/pnpm-lock.yaml
|
|
||||||
paths:
|
|
||||||
- frontend/node_modules/
|
|
||||||
- .pnpm-store/
|
|
||||||
policy: pull
|
|
||||||
before_script:
|
|
||||||
- npm install -g pnpm@9.15.3
|
|
||||||
- pnpm config set store-dir $(pwd)/.pnpm-store
|
|
||||||
script:
|
|
||||||
- cd frontend
|
|
||||||
- pnpm install --frozen-lockfile
|
|
||||||
# Download all blob reports
|
|
||||||
- mkdir -p all-blob-reports
|
|
||||||
# GitLab automatically downloads artifacts from dependencies
|
|
||||||
- cp -r ../frontend/blob-report/* all-blob-reports/ || true
|
|
||||||
- pnpm exec playwright merge-reports --reporter html,github ./all-blob-reports || true
|
|
||||||
artifacts:
|
|
||||||
when: always
|
|
||||||
paths:
|
|
||||||
- frontend/playwright-report/
|
|
||||||
expire_in: 30 days
|
|
||||||
rules:
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
when: always
|
|
||||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
||||||
when: always
|
|
||||||
|
|
||||||
# Update Currencies (Scheduled Job)
|
|
||||||
update:currencies:
|
|
||||||
stage: test
|
|
||||||
image: python:3.11
|
|
||||||
cache:
|
|
||||||
key: python-currencies
|
|
||||||
paths:
|
|
||||||
- .pip-cache/
|
|
||||||
before_script:
|
|
||||||
- pip install --cache-dir .pip-cache -r .github/workflows/update-currencies/requirements.txt
|
|
||||||
script:
|
|
||||||
- python .github/scripts/update_currencies.py
|
|
||||||
- |
|
|
||||||
if git diff --quiet -- backend/internal/core/currencies/currencies.json; then
|
|
||||||
echo "✅ currencies.json is already up-to-date"
|
|
||||||
exit 0
|
|
||||||
else
|
|
||||||
echo "Changes detected in currencies.json"
|
|
||||||
git config user.name "GitLab CI"
|
|
||||||
git config user.email "ci@gitlab.com"
|
|
||||||
git checkout -b update-currencies-$CI_COMMIT_SHORT_SHA
|
|
||||||
git add backend/internal/core/currencies/currencies.json
|
|
||||||
git commit -m "chore: update currencies.json"
|
|
||||||
git push -o merge_request.create -o merge_request.target=$CI_DEFAULT_BRANCH -o merge_request.title="Update currencies.json" origin update-currencies-$CI_COMMIT_SHORT_SHA
|
|
||||||
fi
|
|
||||||
rules:
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "schedule" && $UPDATE_CURRENCIES == "true"
|
|
||||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $UPDATE_CURRENCIES == "true"
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# Binary Build with GoReleaser
|
|
||||||
# ==========================================
|
|
||||||
build:binaries:
|
|
||||||
stage: build-binaries
|
|
||||||
image: golang:1.24
|
|
||||||
cache:
|
|
||||||
- key:
|
|
||||||
files:
|
|
||||||
- frontend/pnpm-lock.yaml
|
|
||||||
paths:
|
|
||||||
- frontend/node_modules/
|
|
||||||
- .pnpm-store/
|
|
||||||
policy: pull-push
|
|
||||||
- key:
|
|
||||||
files:
|
|
||||||
- backend/go.sum
|
|
||||||
paths:
|
|
||||||
- backend/.go-pkg-cache/
|
|
||||||
policy: pull-push
|
|
||||||
before_script:
|
|
||||||
# Install Node.js and pnpm for frontend build
|
|
||||||
- curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
|
||||||
- apt-get install -y nodejs
|
|
||||||
- npm install -g pnpm@9.15.3
|
|
||||||
# Configure pnpm store
|
|
||||||
- pnpm config set store-dir $(pwd)/.pnpm-store
|
|
||||||
# Install GoReleaser
|
|
||||||
- curl -sfL https://goreleaser.com/static/run | bash -s -- check
|
|
||||||
- curl -sfL https://goreleaser.com/static/run | bash -s -- --version
|
|
||||||
# Configure Go cache
|
|
||||||
- export GOMODCACHE=$(pwd)/backend/.go-pkg-cache
|
|
||||||
script:
|
|
||||||
# Build frontend
|
|
||||||
- cd frontend
|
|
||||||
- pnpm install --frozen-lockfile
|
|
||||||
- pnpm run build
|
|
||||||
- cp -r ./.output/public ../backend/app/api/static/
|
|
||||||
- cd ..
|
|
||||||
# Run GoReleaser
|
|
||||||
- cd backend
|
|
||||||
- |
|
|
||||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
|
||||||
echo "Building release for tag: $CI_COMMIT_TAG"
|
|
||||||
curl -sfL https://goreleaser.com/static/run | bash -s -- release --clean --skip=publish
|
|
||||||
else
|
|
||||||
echo "Building snapshot"
|
|
||||||
curl -sfL https://goreleaser.com/static/run | bash -s -- release --clean --snapshot --skip=publish
|
|
||||||
fi
|
|
||||||
artifacts:
|
|
||||||
name: "homebox-binaries-$CI_COMMIT_REF_SLUG"
|
|
||||||
paths:
|
|
||||||
- backend/dist/
|
|
||||||
expire_in: 30 days
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_TAG
|
|
||||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# Docker Build Jobs - Regular
|
|
||||||
# ==========================================
|
|
||||||
.docker_build_template:
|
|
||||||
stage: build-docker
|
|
||||||
image: docker:latest
|
|
||||||
services:
|
|
||||||
- docker:dind
|
|
||||||
before_script:
|
|
||||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
|
||||||
variables:
|
|
||||||
DOCKER_BUILDKIT: 1
|
|
||||||
DOCKERFILE: Dockerfile
|
|
||||||
IMAGE_SUFFIX: ""
|
|
||||||
script:
|
|
||||||
- export VERSION=${CI_COMMIT_TAG:-$CI_COMMIT_REF_NAME}
|
|
||||||
- export COMMIT=$CI_COMMIT_SHA
|
|
||||||
- export BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
||||||
- export CACHE_TAG=${IMAGE_SUFFIX:-regular}
|
|
||||||
# Build and push for the specific platform with layer caching
|
|
||||||
- |
|
|
||||||
docker buildx create --use --name builder-${CI_JOB_ID} || true
|
|
||||||
docker buildx build \
|
|
||||||
--platform $PLATFORM \
|
|
||||||
--build-arg VERSION=$VERSION \
|
|
||||||
--build-arg COMMIT=$COMMIT \
|
|
||||||
--build-arg BUILD_TIME=$BUILD_TIME \
|
|
||||||
--cache-from type=registry,ref=$CI_REGISTRY_IMAGE/cache:${PLATFORM_PAIR}-${CACHE_TAG}-$CI_COMMIT_REF_SLUG \
|
|
||||||
--cache-from type=registry,ref=$CI_REGISTRY_IMAGE/cache:${PLATFORM_PAIR}-${CACHE_TAG}-$CI_DEFAULT_BRANCH \
|
|
||||||
--cache-to type=registry,ref=$CI_REGISTRY_IMAGE/cache:${PLATFORM_PAIR}-${CACHE_TAG}-$CI_COMMIT_REF_SLUG,mode=max \
|
|
||||||
--file ./$DOCKERFILE \
|
|
||||||
--tag $CI_REGISTRY_IMAGE${IMAGE_SUFFIX}:${CI_COMMIT_REF_SLUG}-${PLATFORM_PAIR} \
|
|
||||||
--push \
|
|
||||||
.
|
|
||||||
docker buildx rm builder-${CI_JOB_ID} || true
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_TAG
|
|
||||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
|
|
||||||
docker:build:amd64:
|
|
||||||
extends: .docker_build_template
|
|
||||||
variables:
|
|
||||||
PLATFORM: linux/amd64
|
|
||||||
PLATFORM_PAIR: linux-amd64
|
|
||||||
|
|
||||||
docker:build:arm64:
|
|
||||||
extends: .docker_build_template
|
|
||||||
variables:
|
|
||||||
PLATFORM: linux/arm64
|
|
||||||
PLATFORM_PAIR: linux-arm64
|
|
||||||
|
|
||||||
docker:build:armv7:
|
|
||||||
extends: .docker_build_template
|
|
||||||
variables:
|
|
||||||
PLATFORM: linux/arm/v7
|
|
||||||
PLATFORM_PAIR: linux-arm-v7
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# Docker Build Jobs - Rootless
|
|
||||||
# ==========================================
|
|
||||||
docker:build:rootless:amd64:
|
|
||||||
extends: .docker_build_template
|
|
||||||
variables:
|
|
||||||
PLATFORM: linux/amd64
|
|
||||||
PLATFORM_PAIR: linux-amd64
|
|
||||||
DOCKERFILE: Dockerfile.rootless
|
|
||||||
IMAGE_SUFFIX: -rootless
|
|
||||||
|
|
||||||
docker:build:rootless:arm64:
|
|
||||||
extends: .docker_build_template
|
|
||||||
variables:
|
|
||||||
PLATFORM: linux/arm64
|
|
||||||
PLATFORM_PAIR: linux-arm64
|
|
||||||
DOCKERFILE: Dockerfile.rootless
|
|
||||||
IMAGE_SUFFIX: -rootless
|
|
||||||
|
|
||||||
docker:build:rootless:armv7:
|
|
||||||
extends: .docker_build_template
|
|
||||||
variables:
|
|
||||||
PLATFORM: linux/arm/v7
|
|
||||||
PLATFORM_PAIR: linux-arm-v7
|
|
||||||
DOCKERFILE: Dockerfile.rootless
|
|
||||||
IMAGE_SUFFIX: -rootless
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# Docker Build Jobs - Hardened
|
|
||||||
# ==========================================
|
|
||||||
docker:build:hardened:amd64:
|
|
||||||
extends: .docker_build_template
|
|
||||||
variables:
|
|
||||||
PLATFORM: linux/amd64
|
|
||||||
PLATFORM_PAIR: linux-amd64
|
|
||||||
DOCKERFILE: Dockerfile.hardened
|
|
||||||
IMAGE_SUFFIX: -hardened
|
|
||||||
|
|
||||||
docker:build:hardened:arm64:
|
|
||||||
extends: .docker_build_template
|
|
||||||
variables:
|
|
||||||
PLATFORM: linux/arm64
|
|
||||||
PLATFORM_PAIR: linux-arm64
|
|
||||||
DOCKERFILE: Dockerfile.hardened
|
|
||||||
IMAGE_SUFFIX: -hardened
|
|
||||||
|
|
||||||
docker:build:hardened:armv7:
|
|
||||||
extends: .docker_build_template
|
|
||||||
variables:
|
|
||||||
PLATFORM: linux/arm/v7
|
|
||||||
PLATFORM_PAIR: linux-arm-v7
|
|
||||||
DOCKERFILE: Dockerfile.hardened
|
|
||||||
IMAGE_SUFFIX: -hardened
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# Docker Manifest Creation - Regular
|
|
||||||
# ==========================================
|
|
||||||
docker:manifest:
|
|
||||||
stage: release
|
|
||||||
image: docker:latest
|
|
||||||
services:
|
|
||||||
- docker:dind
|
|
||||||
before_script:
|
|
||||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
|
||||||
script:
|
|
||||||
- export VERSION=${CI_COMMIT_TAG:-$CI_COMMIT_REF_NAME}
|
|
||||||
# Create manifest for regular image
|
|
||||||
- |
|
|
||||||
docker manifest create $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG \
|
|
||||||
$CI_REGISTRY_IMAGE:${CI_COMMIT_REF_SLUG}-linux-amd64 \
|
|
||||||
$CI_REGISTRY_IMAGE:${CI_COMMIT_REF_SLUG}-linux-arm64 \
|
|
||||||
$CI_REGISTRY_IMAGE:${CI_COMMIT_REF_SLUG}-linux-arm-v7
|
|
||||||
docker manifest push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
|
|
||||||
# Tag as latest on main branch or create version tags
|
|
||||||
- |
|
|
||||||
if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then
|
|
||||||
docker manifest create $CI_REGISTRY_IMAGE:latest \
|
|
||||||
$CI_REGISTRY_IMAGE:${CI_COMMIT_REF_SLUG}-linux-amd64 \
|
|
||||||
$CI_REGISTRY_IMAGE:${CI_COMMIT_REF_SLUG}-linux-arm64 \
|
|
||||||
$CI_REGISTRY_IMAGE:${CI_COMMIT_REF_SLUG}-linux-arm-v7
|
|
||||||
docker manifest push $CI_REGISTRY_IMAGE:latest
|
|
||||||
fi
|
|
||||||
- |
|
|
||||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
|
||||||
# Create version tag
|
|
||||||
docker manifest create $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG \
|
|
||||||
$CI_REGISTRY_IMAGE:${CI_COMMIT_REF_SLUG}-linux-amd64 \
|
|
||||||
$CI_REGISTRY_IMAGE:${CI_COMMIT_REF_SLUG}-linux-arm64 \
|
|
||||||
$CI_REGISTRY_IMAGE:${CI_COMMIT_REF_SLUG}-linux-arm-v7
|
|
||||||
docker manifest push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
|
|
||||||
|
|
||||||
# Create major.minor tag if semantic version
|
|
||||||
MAJOR_MINOR=$(echo $CI_COMMIT_TAG | sed -E 's/^v?([0-9]+\.[0-9]+)\..*/\1/')
|
|
||||||
if [ -n "$MAJOR_MINOR" ]; then
|
|
||||||
docker manifest create $CI_REGISTRY_IMAGE:$MAJOR_MINOR \
|
|
||||||
$CI_REGISTRY_IMAGE:${CI_COMMIT_REF_SLUG}-linux-amd64 \
|
|
||||||
$CI_REGISTRY_IMAGE:${CI_COMMIT_REF_SLUG}-linux-arm64 \
|
|
||||||
$CI_REGISTRY_IMAGE:${CI_COMMIT_REF_SLUG}-linux-arm-v7
|
|
||||||
docker manifest push $CI_REGISTRY_IMAGE:$MAJOR_MINOR
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
needs:
|
|
||||||
- docker:build:amd64
|
|
||||||
- docker:build:arm64
|
|
||||||
- docker:build:armv7
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_TAG
|
|
||||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# Docker Manifest Creation - Rootless
|
|
||||||
# ==========================================
|
|
||||||
docker:manifest:rootless:
|
|
||||||
stage: release
|
|
||||||
image: docker:latest
|
|
||||||
services:
|
|
||||||
- docker:dind
|
|
||||||
before_script:
|
|
||||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
|
||||||
script:
|
|
||||||
- export VERSION=${CI_COMMIT_TAG:-$CI_COMMIT_REF_NAME}
|
|
||||||
# Create manifest for rootless image
|
|
||||||
- |
|
|
||||||
docker manifest create $CI_REGISTRY_IMAGE-rootless:$CI_COMMIT_REF_SLUG \
|
|
||||||
$CI_REGISTRY_IMAGE-rootless:${CI_COMMIT_REF_SLUG}-linux-amd64 \
|
|
||||||
$CI_REGISTRY_IMAGE-rootless:${CI_COMMIT_REF_SLUG}-linux-arm64
|
|
||||||
docker manifest push $CI_REGISTRY_IMAGE-rootless:$CI_COMMIT_REF_SLUG
|
|
||||||
# Tag as latest on main branch or create version tags
|
|
||||||
- |
|
|
||||||
if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then
|
|
||||||
docker manifest create $CI_REGISTRY_IMAGE-rootless:latest \
|
|
||||||
$CI_REGISTRY_IMAGE-rootless:${CI_COMMIT_REF_SLUG}-linux-amd64 \
|
|
||||||
$CI_REGISTRY_IMAGE-rootless:${CI_COMMIT_REF_SLUG}-linux-arm64
|
|
||||||
docker manifest push $CI_REGISTRY_IMAGE-rootless:latest
|
|
||||||
fi
|
|
||||||
- |
|
|
||||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
|
||||||
docker manifest create $CI_REGISTRY_IMAGE-rootless:$CI_COMMIT_TAG \
|
|
||||||
$CI_REGISTRY_IMAGE-rootless:${CI_COMMIT_REF_SLUG}-linux-amd64 \
|
|
||||||
$CI_REGISTRY_IMAGE-rootless:${CI_COMMIT_REF_SLUG}-linux-arm64
|
|
||||||
docker manifest push $CI_REGISTRY_IMAGE-rootless:$CI_COMMIT_TAG
|
|
||||||
|
|
||||||
MAJOR_MINOR=$(echo $CI_COMMIT_TAG | sed -E 's/^v?([0-9]+\.[0-9]+)\..*/\1/')
|
|
||||||
if [ -n "$MAJOR_MINOR" ]; then
|
|
||||||
docker manifest create $CI_REGISTRY_IMAGE-rootless:$MAJOR_MINOR \
|
|
||||||
$CI_REGISTRY_IMAGE-rootless:${CI_COMMIT_REF_SLUG}-linux-amd64 \
|
|
||||||
$CI_REGISTRY_IMAGE-rootless:${CI_COMMIT_REF_SLUG}-linux-arm64
|
|
||||||
docker manifest push $CI_REGISTRY_IMAGE-rootless:$MAJOR_MINOR
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
needs:
|
|
||||||
- docker:build:rootless:amd64
|
|
||||||
- docker:build:rootless:arm64
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_TAG
|
|
||||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# Docker Manifest Creation - Hardened
|
|
||||||
# ==========================================
|
|
||||||
docker:manifest:hardened:
|
|
||||||
stage: release
|
|
||||||
image: docker:latest
|
|
||||||
services:
|
|
||||||
- docker:dind
|
|
||||||
before_script:
|
|
||||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
|
||||||
script:
|
|
||||||
- export VERSION=${CI_COMMIT_TAG:-$CI_COMMIT_REF_NAME}
|
|
||||||
# Create manifest for hardened image
|
|
||||||
- |
|
|
||||||
docker manifest create $CI_REGISTRY_IMAGE-hardened:$CI_COMMIT_REF_SLUG \
|
|
||||||
$CI_REGISTRY_IMAGE-hardened:${CI_COMMIT_REF_SLUG}-linux-amd64 \
|
|
||||||
$CI_REGISTRY_IMAGE-hardened:${CI_COMMIT_REF_SLUG}-linux-arm64
|
|
||||||
docker manifest push $CI_REGISTRY_IMAGE-hardened:$CI_COMMIT_REF_SLUG
|
|
||||||
# Tag as latest on main branch or create version tags
|
|
||||||
- |
|
|
||||||
if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then
|
|
||||||
docker manifest create $CI_REGISTRY_IMAGE-hardened:latest \
|
|
||||||
$CI_REGISTRY_IMAGE-hardened:${CI_COMMIT_REF_SLUG}-linux-amd64 \
|
|
||||||
$CI_REGISTRY_IMAGE-hardened:${CI_COMMIT_REF_SLUG}-linux-arm64
|
|
||||||
docker manifest push $CI_REGISTRY_IMAGE-hardened:latest
|
|
||||||
fi
|
|
||||||
- |
|
|
||||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
|
||||||
docker manifest create $CI_REGISTRY_IMAGE-hardened:$CI_COMMIT_TAG \
|
|
||||||
$CI_REGISTRY_IMAGE-hardened:${CI_COMMIT_REF_SLUG}-linux-amd64 \
|
|
||||||
$CI_REGISTRY_IMAGE-hardened:${CI_COMMIT_REF_SLUG}-linux-arm64
|
|
||||||
docker manifest push $CI_REGISTRY_IMAGE-hardened:$CI_COMMIT_TAG
|
|
||||||
|
|
||||||
MAJOR_MINOR=$(echo $CI_COMMIT_TAG | sed -E 's/^v?([0-9]+\.[0-9]+)\..*/\1/')
|
|
||||||
if [ -n "$MAJOR_MINOR" ]; then
|
|
||||||
docker manifest create $CI_REGISTRY_IMAGE-hardened:$MAJOR_MINOR \
|
|
||||||
$CI_REGISTRY_IMAGE-hardened:${CI_COMMIT_REF_SLUG}-linux-amd64 \
|
|
||||||
$CI_REGISTRY_IMAGE-hardened:${CI_COMMIT_REF_SLUG}-linux-arm64
|
|
||||||
docker manifest push $CI_REGISTRY_IMAGE-hardened:$MAJOR_MINOR
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
needs:
|
|
||||||
- docker:build:hardened:amd64
|
|
||||||
- docker:build:hardened:arm64
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_TAG
|
|
||||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
||||||
8
.vscode/launch.json
vendored
@@ -16,12 +16,14 @@
|
|||||||
"type": "go",
|
"type": "go",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"mode": "debug",
|
"mode": "debug",
|
||||||
"program": "${workspaceFolder}/backend/app/api/",
|
"program": "${workspaceRoot}/backend/app/api/",
|
||||||
"args": [],
|
"args": [],
|
||||||
"env": {
|
"env": {
|
||||||
"HBOX_DEMO": "true",
|
"HBOX_DEMO": "true",
|
||||||
"HBOX_LOG_LEVEL": "debug",
|
"HBOX_LOG_LEVEL": "debug",
|
||||||
"HBOX_DEBUG_ENABLED": "true"
|
"HBOX_DEBUG_ENABLED": "true",
|
||||||
|
"HBOX_STORAGE_DATA": "${workspaceRoot}/backend/.data",
|
||||||
|
"HBOX_STORAGE_SQLITE_URL": "${workspaceRoot}/backend/.data/homebox.db?_fk=1"
|
||||||
},
|
},
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
},
|
},
|
||||||
@@ -44,4 +46,4 @@
|
|||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
9
.vscode/settings.json
vendored
@@ -4,7 +4,7 @@
|
|||||||
},
|
},
|
||||||
"explorer.fileNesting.enabled": true,
|
"explorer.fileNesting.enabled": true,
|
||||||
"explorer.fileNesting.patterns": {
|
"explorer.fileNesting.patterns": {
|
||||||
"package.json": "package-lock.json, yarn.lock, eslint.config.mjs, tsconfig.json, .prettierrc, .editorconfig, pnpm-lock.yaml, postcss.config.js, tailwind.config.js",
|
"package.json": "package-lock.json, yarn.lock, .eslintrc.js, tsconfig.json, .prettierrc, .editorconfig, pnpm-lock.yaml, postcss.config.js, tailwind.config.js",
|
||||||
"docker-compose.yml": "Dockerfile, .dockerignore, docker-compose.dev.yml, docker-compose.yml",
|
"docker-compose.yml": "Dockerfile, .dockerignore, docker-compose.dev.yml, docker-compose.yml",
|
||||||
"README.md": "LICENSE, SECURITY.md"
|
"README.md": "LICENSE, SECURITY.md"
|
||||||
},
|
},
|
||||||
@@ -22,8 +22,6 @@
|
|||||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||||
},
|
},
|
||||||
"eslint.format.enable": true,
|
"eslint.format.enable": true,
|
||||||
"eslint.validate": ["javascript", "typescript", "vue"],
|
|
||||||
"eslint.useFlatConfig": true,
|
|
||||||
"css.validate": false,
|
"css.validate": false,
|
||||||
"tailwindCSS.includeLanguages": {
|
"tailwindCSS.includeLanguages": {
|
||||||
"vue": "html",
|
"vue": "html",
|
||||||
@@ -32,8 +30,5 @@
|
|||||||
"editor.quickSuggestions": {
|
"editor.quickSuggestions": {
|
||||||
"strings": true
|
"strings": true
|
||||||
},
|
},
|
||||||
"tailwindCSS.experimental.configFile": "./frontend/tailwind.config.js",
|
"tailwindCSS.experimental.configFile": "./frontend/tailwind.config.js"
|
||||||
"[go]": {
|
|
||||||
"editor.defaultFormatter": "golang.go"
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ representative at an online or offline event.
|
|||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
reported to the community leaders responsible for enforcement at
|
reported to the community leaders responsible for enforcement at
|
||||||
[support@sysadminsmedia.com](mailto:support@sysadminsmedia.com).
|
[support@sysadminemedia.com](mailto:support@sysadminemedia.com).
|
||||||
All complaints will be reviewed and investigated promptly and fairly.
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
All community leaders are obligated to respect the privacy and security of the
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ If you're using `taskfile` you can run `task --list-all` for a list of all comma
|
|||||||
|
|
||||||
If you're using the taskfile, you can use the `task setup` command to run the required setup commands. Otherwise, you can review the commands required in the `Taskfile.yml` file.
|
If you're using the taskfile, you can use the `task setup` command to run the required setup commands. Otherwise, you can review the commands required in the `Taskfile.yml` file.
|
||||||
|
|
||||||
|
Note that when installing dependencies with pnpm you must use the `--shamefully-hoist` flag. If you don't use this flag, you will get an error when running the frontend server.
|
||||||
|
|
||||||
### API Development Notes
|
### API Development Notes
|
||||||
|
|
||||||
start command `task go:run`
|
start command `task go:run`
|
||||||
@@ -42,9 +44,9 @@ start command `task go:run`
|
|||||||
|
|
||||||
### Frontend Development Notes
|
### 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 Shadcn-vue for styling.
|
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`.
|
2. We're using Vitest for our automated testing. You can run these with `task ui:watch`.
|
||||||
3. Tests require the API server to be running, and in some cases the first run will fail due to a race condition. If this happens, just run the tests again and they should pass.
|
3. Tests require the API server to be running, and in some cases the first run will fail due to a race condition. If this happens, just run the tests again and they should pass.
|
||||||
|
|
||||||
@@ -52,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.
|
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
|
||||||
99
Dockerfile
@@ -1,101 +1,58 @@
|
|||||||
# Node dependencies stage
|
|
||||||
FROM public.ecr.aws/docker/library/node:22-alpine AS frontend-dependencies
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Install pnpm globally (caching layer)
|
# Build Nuxt
|
||||||
|
FROM node:18-alpine AS frontend-builder
|
||||||
|
WORKDIR /app
|
||||||
RUN npm install -g pnpm
|
RUN npm install -g pnpm
|
||||||
|
|
||||||
# Copy package.json and lockfile to leverage caching
|
|
||||||
COPY frontend/package.json frontend/pnpm-lock.yaml ./
|
COPY frontend/package.json frontend/pnpm-lock.yaml ./
|
||||||
RUN pnpm install --frozen-lockfile
|
RUN pnpm install --frozen-lockfile --shamefully-hoist
|
||||||
|
COPY frontend .
|
||||||
# Build Nuxt (frontend) stage
|
|
||||||
FROM public.ecr.aws/docker/library/node:22-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 --from=frontend-dependencies /app/node_modules ./node_modules
|
|
||||||
RUN pnpm build
|
RUN pnpm build
|
||||||
|
|
||||||
# Go dependencies stage
|
# Build API
|
||||||
FROM public.ecr.aws/docker/library/golang:alpine AS builder-dependencies
|
FROM golang:alpine AS builder
|
||||||
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
|
|
||||||
ARG TARGETOS
|
|
||||||
ARG TARGETARCH
|
|
||||||
ARG BUILD_TIME
|
ARG BUILD_TIME
|
||||||
ARG COMMIT
|
ARG COMMIT
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
|
|
||||||
# Install necessary build tools
|
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk upgrade && \
|
apk upgrade && \
|
||||||
apk add --no-cache git build-base gcc g++ && \
|
apk add --update git build-base gcc g++
|
||||||
if [ "$TARGETARCH" != "arm" ] || [ "$TARGETARCH" != "riscv64" ]; then apk --no-cache add libwebp libavif libheif libjxl; fi
|
|
||||||
|
|
||||||
WORKDIR /go/src/app
|
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 .
|
COPY ./backend .
|
||||||
|
RUN go get -d -v ./...
|
||||||
# Clear old public files and copy new ones from frontend build
|
|
||||||
RUN rm -rf ./app/api/public
|
RUN rm -rf ./app/api/public
|
||||||
COPY --from=frontend-builder /app/.output/public ./app/api/static/public
|
COPY --from=frontend-builder /app/.output/public ./app/api/static/public
|
||||||
|
RUN 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
|
||||||
|
|
||||||
# Use cache for Go build artifacts
|
FROM gcr.io/distroless/java:latest
|
||||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
|
||||||
if [ "$TARGETARCH" = "arm" ] || [ "$TARGETARCH" = "riscv64" ]; \
|
# Production Stage
|
||||||
then echo "nodynamic" $TARGETOS $TARGETARCH; CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build \
|
FROM alpine:latest
|
||||||
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
|
|
||||||
-tags nodynamic -o /go/bin/api -v ./app/api/*.go; \
|
|
||||||
else \
|
|
||||||
echo $TARGETOS $TARGETARCH; CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH 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; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Production stage
|
|
||||||
FROM public.ecr.aws/docker/library/alpine:latest
|
|
||||||
ENV HBOX_MODE=production
|
ENV HBOX_MODE=production
|
||||||
ENV HBOX_STORAGE_CONN_STRING=file:///?no_tmp_dir=true
|
ENV HBOX_STORAGE_DATA=/data/
|
||||||
ENV HBOX_STORAGE_PREFIX_PATH=data
|
ENV HBOX_STORAGE_SQLITE_URL=/data/homebox.db?_pragma=busy_timeout=2000&_pragma=journal_mode=WAL&_fk=1
|
||||||
ENV HBOX_DATABASE_SQLITE_PATH=/data/homebox.db?_pragma=busy_timeout=2000&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite
|
|
||||||
|
|
||||||
# Install necessary runtime dependencies
|
RUN apk --no-cache add ca-certificates
|
||||||
RUN apk --no-cache add ca-certificates wget && \
|
|
||||||
if [ "$TARGETARCH" != "arm" ] || [ "$TARGETARCH" != "riscv64" ]; then apk --no-cache add libwebp libavif libheif libjxl; fi
|
|
||||||
|
|
||||||
# Create application directory and copy over built Go binary
|
|
||||||
RUN mkdir /app
|
RUN mkdir /app
|
||||||
COPY --from=builder /go/bin/api /app
|
COPY --from=builder /go/bin/api /app
|
||||||
RUN chmod +x /app/api
|
|
||||||
|
|
||||||
# Labels and configuration for the final image
|
RUN chmod +x /app/api
|
||||||
|
RUN apk add --no-cache wget
|
||||||
|
|
||||||
LABEL Name=homebox Version=0.0.1
|
LABEL Name=homebox Version=0.0.1
|
||||||
LABEL org.opencontainers.image.source="https://github.com/sysadminsmedia/homebox"
|
LABEL org.opencontainers.image.source="https://github.com/sysadminsmedia/homebox"
|
||||||
|
|
||||||
# Expose necessary ports for Homebox
|
|
||||||
EXPOSE 7745
|
EXPOSE 7745
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
HEALTHCHECK --interval=30s \
|
||||||
# Healthcheck configuration
|
--timeout=5s \
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
--start-period=5s \
|
||||||
CMD [ "wget", "--no-verbose", "--tries=1", "-O", "-", "http://localhost:7745/api/v1/status" ]
|
--retries=3 \
|
||||||
|
CMD [ "/usr/bin/wget", "--no-verbose", "--tries=1", "-O -", "http://localhost:7745/api/v1/status" ]
|
||||||
# Persist volume
|
|
||||||
VOLUME [ "/data" ]
|
VOLUME [ "/data" ]
|
||||||
|
|
||||||
# Entrypoint and CMD
|
|
||||||
ENTRYPOINT [ "/app/api" ]
|
ENTRYPOINT [ "/app/api" ]
|
||||||
CMD [ "/data/config.yml" ]
|
CMD [ "/data/config.yml" ]
|
||||||
|
|||||||
@@ -1,136 +0,0 @@
|
|||||||
# ---------------------------------------
|
|
||||||
# Node dependencies stage
|
|
||||||
# ---------------------------------------
|
|
||||||
FROM public.ecr.aws/docker/library/node:22-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
|
|
||||||
|
|
||||||
# ---------------------------------------
|
|
||||||
# Build Nuxt (frontend) stage
|
|
||||||
# ---------------------------------------
|
|
||||||
FROM public.ecr.aws/docker/library/node:22-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 --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
|
|
||||||
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 + healthcheck stage
|
|
||||||
# ---------------------------------------
|
|
||||||
FROM public.ecr.aws/docker/library/golang:alpine AS builder
|
|
||||||
ARG TARGETOS
|
|
||||||
ARG TARGETARCH
|
|
||||||
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++
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# Use cache for Go build artifacts to build Homebox API
|
|
||||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
|
||||||
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build \
|
|
||||||
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
|
|
||||||
-tags nodynamic -o /go/bin/api -v ./app/api/*.go
|
|
||||||
|
|
||||||
RUN chmod +x /go/bin/api
|
|
||||||
RUN mkdir /app
|
|
||||||
RUN mkdir /data
|
|
||||||
|
|
||||||
# ---------- Build static healthcheck helper ----------
|
|
||||||
# A small Go program that GETs the status URL and exits 0 on 2xx.
|
|
||||||
RUN cat > /tmp/healthcheck.go <<'EOF'
|
|
||||||
package main
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
func main() {
|
|
||||||
url := "http://127.0.0.1:7745/api/v1/status"
|
|
||||||
if len(os.Args) > 1 { url = os.Args[1] }
|
|
||||||
c := &http.Client{ Timeout: 3 * time.Second }
|
|
||||||
resp, err := c.Get(url)
|
|
||||||
if err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) }
|
|
||||||
resp.Body.Close()
|
|
||||||
if resp.StatusCode/100 != 2 {
|
|
||||||
fmt.Fprintln(os.Stderr, "unexpected status:", resp.StatusCode)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
|
||||||
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
|
|
||||||
go build -ldflags "-s -w" -o /go/bin/hc /tmp/healthcheck.go
|
|
||||||
|
|
||||||
# ---------------------------------------
|
|
||||||
# Production stage
|
|
||||||
# ---------------------------------------
|
|
||||||
FROM gcr.io/distroless/static:nonroot
|
|
||||||
ENV HBOX_MODE=production
|
|
||||||
ENV HBOX_STORAGE_CONN_STRING=file:///?no_tmp_dir=true
|
|
||||||
ENV HBOX_STORAGE_PREFIX_PATH=data
|
|
||||||
ENV HBOX_DATABASE_SQLITE_PATH=/data/homebox.db?_pragma=busy_timeout=2000&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite
|
|
||||||
|
|
||||||
# Create application directory and copy over built Go binary and assets
|
|
||||||
COPY --from=builder --chown=65532:65532 /app /app
|
|
||||||
COPY --from=builder --chown=65532:65532 --chmod=755 /go/bin/api /app
|
|
||||||
COPY --from=builder --chown=65532:65532 /data /data
|
|
||||||
|
|
||||||
# Copy the healthcheck helper
|
|
||||||
COPY --from=builder --chown=65532:65532 --chmod=755 /go/bin/hc /app/healthcheck
|
|
||||||
|
|
||||||
# Labels and configuration for the final image
|
|
||||||
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
|
|
||||||
|
|
||||||
# Persist volume for data
|
|
||||||
VOLUME [ "/data" ]
|
|
||||||
|
|
||||||
# Entrypoint and CMD
|
|
||||||
USER 65532
|
|
||||||
ENTRYPOINT [ "/app/api" ]
|
|
||||||
CMD [ "/data/config.yml" ]
|
|
||||||
|
|
||||||
# JSON exec-form healthcheck (no shell, no wget)
|
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
|
||||||
CMD ["/app/healthcheck", "http://127.0.0.1:7745/api/v1/status"]
|
|
||||||
@@ -1,108 +1,63 @@
|
|||||||
# Node dependencies stage
|
|
||||||
FROM public.ecr.aws/docker/library/node:22-alpine AS frontend-dependencies
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Install pnpm globally (caching layer)
|
# Build Nuxt
|
||||||
|
FROM node:18-alpine AS frontend-builder
|
||||||
|
WORKDIR /app
|
||||||
RUN npm install -g pnpm
|
RUN npm install -g pnpm
|
||||||
|
|
||||||
# Copy package.json and lockfile to leverage caching
|
|
||||||
COPY frontend/package.json frontend/pnpm-lock.yaml ./
|
COPY frontend/package.json frontend/pnpm-lock.yaml ./
|
||||||
RUN pnpm install --frozen-lockfile
|
RUN pnpm install --frozen-lockfile --shamefully-hoist
|
||||||
|
|
||||||
# Build Nuxt (frontend) stage
|
|
||||||
FROM public.ecr.aws/docker/library/node:22-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
|
RUN pnpm build
|
||||||
|
|
||||||
# Go dependencies stage
|
# Build API
|
||||||
FROM public.ecr.aws/docker/library/golang:alpine AS builder-dependencies
|
FROM golang:alpine AS builder
|
||||||
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
|
|
||||||
ARG TARGETOS
|
|
||||||
ARG TARGETARCH
|
|
||||||
ARG BUILD_TIME
|
ARG BUILD_TIME
|
||||||
ARG COMMIT
|
ARG COMMIT
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
|
ARG BUSYBOX_VERSION=1.36.1-r31
|
||||||
# Install necessary build tools
|
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk upgrade && \
|
apk upgrade && \
|
||||||
apk add --no-cache git build-base gcc g++ && \
|
apk add --update git build-base gcc g++
|
||||||
if [ "$TARGETARCH" != "arm" ] || [ "$TARGETARCH" != "riscv64" ]; then apk --no-cache add libwebp libavif libheif libjxl; fi
|
|
||||||
|
|
||||||
WORKDIR /go/src/app
|
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 .
|
COPY ./backend .
|
||||||
|
RUN go get -d -v ./...
|
||||||
# Clear old public files and copy new ones from frontend build
|
|
||||||
RUN rm -rf ./app/api/public
|
RUN rm -rf ./app/api/public
|
||||||
COPY --from=frontend-builder /app/.output/public ./app/api/static/public
|
COPY --from=frontend-builder /app/.output/public ./app/api/static/public
|
||||||
|
RUN 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 && \
|
||||||
|
chmod +x /go/bin/api && \
|
||||||
|
# create a directory so that we can copy it in the next stage
|
||||||
|
mkdir /data
|
||||||
|
|
||||||
# Use cache for Go build artifacts
|
FROM gcr.io/distroless/java:latest
|
||||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
|
||||||
if [ "$TARGETARCH" = "arm" ] || [ "$TARGETARCH" = "riscv64" ]; \
|
|
||||||
then echo "nodynamic" $TARGETOS $TARGETARCH; CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build \
|
|
||||||
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
|
|
||||||
-tags nodynamic -o /go/bin/api -v ./app/api/*.go; \
|
|
||||||
else \
|
|
||||||
echo $TARGETOS $TARGETARCH; CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH 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; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
RUN mkdir /data
|
# Production Stage
|
||||||
|
FROM gcr.io/distroless/static:latest
|
||||||
|
|
||||||
# Production stage
|
|
||||||
FROM public.ecr.aws/docker/library/alpine:latest
|
|
||||||
ENV HBOX_MODE=production
|
ENV HBOX_MODE=production
|
||||||
ENV HBOX_STORAGE_CONN_STRING=file:///?no_tmp_dir=true
|
ENV HBOX_STORAGE_DATA=/data/
|
||||||
ENV HBOX_STORAGE_PREFIX_PATH=data
|
ENV HBOX_STORAGE_SQLITE_URL=/data/homebox.db?_fk=1
|
||||||
ENV HBOX_DATABASE_SQLITE_PATH=/data/homebox.db?_pragma=busy_timeout=2000&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite
|
|
||||||
|
|
||||||
# Install necessary runtime dependencies
|
# Copy the binary and the (empty) /data dir and
|
||||||
RUN apk --no-cache add ca-certificates wget && \
|
# change the ownership to the low-privileged user
|
||||||
if [ "$TARGETARCH" != "arm" ] || [ "$TARGETARCH" != "riscv64" ]; then apk --no-cache add libwebp libavif libheif libjxl; fi
|
|
||||||
|
|
||||||
# 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 --from=builder --chown=nonroot /go/bin/api /app
|
COPY --from=builder --chown=nonroot /go/bin/api /app
|
||||||
COPY --from=builder --chown=nonroot /data /data
|
COPY --from=builder --chown=nonroot /data /data
|
||||||
RUN chmod +x /app/api
|
|
||||||
|
|
||||||
# Labels and configuration for the final image
|
RUN apk add --no-cache wget
|
||||||
|
|
||||||
LABEL Name=homebox Version=0.0.1
|
LABEL Name=homebox Version=0.0.1
|
||||||
LABEL org.opencontainers.image.source="https://github.com/sysadminsmedia/homebox"
|
LABEL org.opencontainers.image.source="https://github.com/sysadminsmedia/homebox"
|
||||||
|
|
||||||
# Expose necessary ports for Homebox
|
|
||||||
EXPOSE 7745
|
EXPOSE 7745
|
||||||
WORKDIR /app
|
HEALTHCHECK --interval=30s \
|
||||||
|
--timeout=5s \
|
||||||
# Healthcheck configuration
|
--start-period=5s \
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
--retries=3 \
|
||||||
CMD [ "wget", "--no-verbose", "--tries=1", "-O", "-", "http://localhost:7745/api/v1/status" ]
|
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 low-privileged user
|
||||||
USER nonroot
|
USER nonroot
|
||||||
ENTRYPOINT [ "/app/api" ]
|
ENTRYPOINT [ "/app" ]
|
||||||
CMD [ "/data/config.yml" ]
|
CMD [ "/data/config.yml" ]
|
||||||
|
|||||||
67
README.md
@@ -2,59 +2,32 @@
|
|||||||
<img src="/docs/public/lilbox.svg" height="200"/>
|
<img src="/docs/public/lilbox.svg" height="200"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 align="center" style="margin-top: -10px;"> HomeBox </h1>
|
<h1 align="center" style="margin-top: -10px"> HomeBox </h1>
|
||||||
<p align="center" style="width: 100%;">
|
<p align="center" style="width: 100;">
|
||||||
<a href="https://homebox.software/en/">Docs</a>
|
<a href="https://homebox.sysadminsmedia.com">Docs</a>
|
||||||
|
|
|
|
||||||
<a href="https://demo.homebox.software">Demo</a>
|
<a href="https://homebox.fly.dev">Demo</a>
|
||||||
|
|
|
|
||||||
<a href="https://discord.gg/aY4DCkpNA9">Discord</a>
|
<a href="https://discord.gg/aY4DCkpNA9">Discord</a>
|
||||||
</p>
|
</p>
|
||||||
<p align="center" style="width: 100%;">
|
|
||||||
<img src="https://img.shields.io/github/check-runs/sysadminsmedia/homebox/main" alt="Github Checks"/>
|
|
||||||
<img src="https://img.shields.io/github/license/sysadminsmedia/homebox"/>
|
|
||||||
<img src="https://img.shields.io/github/v/release/sysadminsmedia/homebox?sort=semver&display_name=release"/>
|
|
||||||
<img src="https://img.shields.io/weblate/progress/homebox?server=https%3A%2F%2Ftranslate.sysadminsmedia.com"/>
|
|
||||||
</p>
|
|
||||||
<p align="center" style="width: 100%;">
|
|
||||||
<img src="https://img.shields.io/reddit/subreddit-subscribers/homebox"/>
|
|
||||||
<img src="https://img.shields.io/mastodon/follow/110749314839831923?domain=infosec.exchange"/>
|
|
||||||
<img src="https://img.shields.io/lemmy/homebox%40lemmy.world?label=lemmy"/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## What is HomeBox
|
## 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, We'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 but Expandable_ - Homebox is designed to be simple and easy to use. No complicated setup or configuration required. But expandable to whatever level of infrastructure you want to put into it.
|
- _Simple_ - Homebox is designed to be simple and easy to use. No complicated setup or configuration required. Use either a single docker container, or deploy yourself by compiling the binary for your platform of choice.
|
||||||
- 🚀 _Blazingly Fast_ - Homebox is written in Go, which makes it extremely fast and requires minimal resources to deploy. In general, idle memory usage is less than 50MB for the whole container.
|
- _Blazingly Fast_ - Homebox is written in Go, which makes it extremely fast and requires minimal resources to deploy. In general, idle memory usage is less than 50MB for the whole container.
|
||||||
- 📦 _Portable_ - Homebox is designed to be portable and run on anywhere. We use SQLite and an embedded Web UI to make it easy to deploy, use, and backup.
|
- _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.
|
||||||
|
|
||||||
### Key Features
|
# Screenshots
|
||||||
- 📇 Rich Organization - Organize your items into categories, locations, and tags. You can also create custom fields to store additional information about your items.
|
Check out screenshots of the project [here](https://imgur.com/a/5gLWt2j).
|
||||||
- 🔍 Powerful Search - Quickly find items in your inventory using the powerful search feature.
|
|
||||||
- 📸 Image Upload - Upload images of your items to make it easy to identify them.
|
|
||||||
- 📄 Document and Warranty Tracking - Keep track of important documents and warranties for your items.
|
|
||||||
- 💰 Purchase & Maintenance Tracking - Track purchase dates, prices, and maintenance schedules for your items.
|
|
||||||
- 📱 Responsive Design - Homebox is designed to work on any device, including desktops, tablets, and smartphones.
|
|
||||||
|
|
||||||
## Screenshots
|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
You can also try the demo instances of Homebox:
|
|
||||||
- [Demo](https://demo.homebox.software)
|
|
||||||
- [Nightly](https://nightly.homebox.software)
|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
[Configuration & Docker Compose](https://homebox.software/en/quick-start.html)
|
[Configuration & Docker Compose](https://homebox.sysadminsmedia.com/en/quick-start.html)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# If using the rootless or hardened image, ensure data
|
# If using the rootless image, ensure data
|
||||||
# folder has correct permissions
|
# folder has correct permissions
|
||||||
mkdir -p /path/to/data/folder
|
mkdir -p /path/to/data/folder
|
||||||
chown 65532:65532 -R /path/to/data/folder
|
chown 65532:65532 -R /path/to/data/folder
|
||||||
@@ -66,7 +39,6 @@ docker run -d \
|
|||||||
--volume /path/to/data/folder/:/data \
|
--volume /path/to/data/folder/:/data \
|
||||||
ghcr.io/sysadminsmedia/homebox:latest
|
ghcr.io/sysadminsmedia/homebox:latest
|
||||||
# ghcr.io/sysadminsmedia/homebox:latest-rootless
|
# ghcr.io/sysadminsmedia/homebox:latest-rootless
|
||||||
# ghcr.io/sysadminsmedia/homebox:latest-hardened
|
|
||||||
```
|
```
|
||||||
|
|
||||||
<!-- CONTRIBUTING -->
|
<!-- CONTRIBUTING -->
|
||||||
@@ -75,20 +47,9 @@ docker run -d \
|
|||||||
|
|
||||||
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
|
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
|
||||||
|
|
||||||
To get started with code based contributions, please see our [contributing guide](https://homebox.software/en/contribute/get-started.html).
|
If you are not a coder, you can still contribute financially. Financial contributions help me prioritize working on this project over others and helps me know that there is a real demand for project development.
|
||||||
|
|
||||||
If you are not a coder and can't help translate, you can still contribute financially. Financial contributions help us maintain the project and keep demos running.
|
|
||||||
|
|
||||||
## Help us Translate
|
|
||||||
We want to make sure that Homebox is available in as many languages as possible. If you are interested in helping us translate Homebox, please help us via our [Weblate instance](https://translate.sysadminsmedia.com/projects/homebox/).
|
|
||||||
|
|
||||||
[](https://translate.sysadminsmedia.com/engage/homebox/)
|
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
- Original project by [@hay-kot](https://github.com/hay-kot)
|
- Original project by [@hay-kot](https://github.com/hay-kot)
|
||||||
- Logo by [@lakotelman](https://github.com/lakotelman)
|
- Logo by [@lakotelman](https://github.com/lakotelman)
|
||||||
|
|
||||||
### Contributors
|
|
||||||
<a href="https://github.com/sysadminsmedia/homebox/graphs/contributors">
|
|
||||||
<img src="https://contrib.rocks/image?repo=sysadminsmedia/homebox" />
|
|
||||||
</a>
|
|
||||||
|
|||||||
105
Taskfile.yml
@@ -2,8 +2,7 @@ version: "3"
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
HBOX_LOG_LEVEL: debug
|
HBOX_LOG_LEVEL: debug
|
||||||
HBOX_DATABASE_DRIVER: sqlite3
|
HBOX_STORAGE_SQLITE_URL: .data/homebox.db?_pragma=busy_timeout=1000&_pragma=journal_mode=WAL&_fk=1
|
||||||
HBOX_DATABASE_SQLITE_PATH: .data/homebox.db?_pragma=busy_timeout=1000&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite
|
|
||||||
HBOX_OPTIONS_ALLOW_REGISTRATION: true
|
HBOX_OPTIONS_ALLOW_REGISTRATION: true
|
||||||
UNSAFE_DISABLE_PASSWORD_PROJECTION: "yes_i_am_sure"
|
UNSAFE_DISABLE_PASSWORD_PROJECTION: "yes_i_am_sure"
|
||||||
tasks:
|
tasks:
|
||||||
@@ -11,9 +10,8 @@ tasks:
|
|||||||
desc: Install development dependencies
|
desc: Install development dependencies
|
||||||
cmds:
|
cmds:
|
||||||
- go install github.com/swaggo/swag/cmd/swag@latest
|
- go install github.com/swaggo/swag/cmd/swag@latest
|
||||||
- go install github.com/pressly/goose/v3/cmd/goose@v3.8.0
|
|
||||||
- cd backend && go mod tidy
|
- cd backend && go mod tidy
|
||||||
- cd frontend && pnpm install
|
- cd frontend && pnpm install --shamefully-hoist
|
||||||
|
|
||||||
swag:
|
swag:
|
||||||
desc: Generate swagger docs
|
desc: Generate swagger docs
|
||||||
@@ -23,23 +21,19 @@ tasks:
|
|||||||
INTERNAL: "../../../internal"
|
INTERNAL: "../../../internal"
|
||||||
PKGS: "../../../pkgs"
|
PKGS: "../../../pkgs"
|
||||||
cmds:
|
cmds:
|
||||||
|
- swag fmt --dir={{ .API }}
|
||||||
- swag init --dir={{ .API }},{{ .INTERNAL }}/core/services,{{ .INTERNAL }}/data/repo --parseDependency
|
- swag init --dir={{ .API }},{{ .INTERNAL }}/core/services,{{ .INTERNAL }}/data/repo --parseDependency
|
||||||
- npx -y -p swagger2openapi swagger2openapi --outfile ./docs/openapi-3.json ./docs/swagger.json
|
|
||||||
- npx -y -p swagger2openapi swagger2openapi --yaml --outfile ./docs/openapi-3.yaml ./docs/swagger.json
|
|
||||||
- cp -r ./docs/swagger.json ../../../../docs/en/api/swagger-2.0.json
|
|
||||||
- cp -r ./docs/swagger.yaml ../../../../docs/en/api/swagger-2.0.yaml
|
|
||||||
- cp -r ./docs/openapi-3.json ../../../../docs/en/api/openapi-3.0.json
|
|
||||||
- cp -r ./docs/openapi-3.yaml ../../../../docs/en/api/openapi-3.0.yaml
|
|
||||||
sources:
|
sources:
|
||||||
- "./backend/app/api/**/*"
|
- "./backend/app/api/**/*"
|
||||||
- "./backend/internal/data/**"
|
- "./backend/internal/data/**"
|
||||||
- "./backend/internal/core/services/**/*"
|
- "./backend/internal/core/services/**/*"
|
||||||
- "./backend/app/tools/typegen/main.go"
|
- "./backend/app/tools/typegen/main.go"
|
||||||
|
|
||||||
typescript-types:
|
typescript-types:
|
||||||
desc: Generates typescript types from swagger definition
|
desc: Generates typescript types from swagger definition
|
||||||
cmds:
|
cmds:
|
||||||
- |
|
- |
|
||||||
pnpm dlx swagger-typescript-api generate \
|
npx swagger-typescript-api \
|
||||||
--no-client \
|
--no-client \
|
||||||
--modular \
|
--modular \
|
||||||
--path ./backend/app/api/static/docs/swagger.json \
|
--path ./backend/app/api/static/docs/swagger.json \
|
||||||
@@ -55,12 +49,9 @@ tasks:
|
|||||||
cmds:
|
cmds:
|
||||||
- task: swag
|
- task: swag
|
||||||
- task: typescript-types
|
- task: typescript-types
|
||||||
- cp ./backend/app/api/static/docs/swagger.json docs/en/api/openapi-2.0.json
|
- cp ./backend/app/api/static/docs/swagger.json docs/docs/api/openapi-2.0.json
|
||||||
- cp ./backend/app/api/static/docs/swagger.yaml docs/en/api/openapi-2.0.yaml
|
|
||||||
|
|
||||||
go:run:
|
go:run:
|
||||||
env:
|
|
||||||
HBOX_DEMO: true
|
|
||||||
desc: Starts the backend api server (depends on generate task)
|
desc: Starts the backend api server (depends on generate task)
|
||||||
dir: backend
|
dir: backend
|
||||||
deps:
|
deps:
|
||||||
@@ -69,48 +60,11 @@ tasks:
|
|||||||
- go run ./app/api/ {{ .CLI_ARGS }}
|
- go run ./app/api/ {{ .CLI_ARGS }}
|
||||||
silent: false
|
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:ci:
|
|
||||||
env:
|
|
||||||
HBOX_DEMO: true
|
|
||||||
desc: Runs all go test and lint related tasks
|
|
||||||
dir: backend
|
|
||||||
cmds:
|
|
||||||
- go run ./app/api/ {{ .CLI_ARGS }} &
|
|
||||||
silent: true
|
|
||||||
|
|
||||||
go:ci:with-frontend:
|
|
||||||
desc: Run backend with frontend in CI mode
|
|
||||||
dir: frontend
|
|
||||||
cmds:
|
|
||||||
- pnpm install
|
|
||||||
- pnpm run build
|
|
||||||
- cp -r ./.output/public ../backend/app/api/static/
|
|
||||||
- task: go:ci
|
|
||||||
silent: true
|
|
||||||
|
|
||||||
go:test:
|
go:test:
|
||||||
desc: Runs all go tests using gotestsum - supports passing gotestsum args
|
desc: Runs all go tests using gotestsum - supports passing gotestsum args
|
||||||
dir: backend
|
dir: backend
|
||||||
cmds:
|
cmds:
|
||||||
- go test {{ .CLI_ARGS }} ./...
|
- gotestsum {{ .CLI_ARGS }} ./...
|
||||||
|
|
||||||
go:coverage:
|
go:coverage:
|
||||||
desc: Runs all go tests with -race flag and generates a coverage report
|
desc: Runs all go tests with -race flag and generates a coverage report
|
||||||
@@ -153,6 +107,13 @@ tasks:
|
|||||||
sources:
|
sources:
|
||||||
- "./backend/internal/data/ent/schema/**/*"
|
- "./backend/internal/data/ent/schema/**/*"
|
||||||
|
|
||||||
|
db:migration:
|
||||||
|
desc: Runs the database diff engine to generate a SQL migration files
|
||||||
|
deps:
|
||||||
|
- db:generate
|
||||||
|
cmds:
|
||||||
|
- cd backend && go run app/tools/migrations/main.go {{ .CLI_ARGS }}
|
||||||
|
|
||||||
ui:watch:
|
ui:watch:
|
||||||
desc: Starts the vitest test runner in watch mode
|
desc: Starts the vitest test runner in watch mode
|
||||||
dir: frontend
|
dir: frontend
|
||||||
@@ -163,14 +124,7 @@ tasks:
|
|||||||
desc: Run frontend development server
|
desc: Run frontend development server
|
||||||
dir: frontend
|
dir: frontend
|
||||||
cmds:
|
cmds:
|
||||||
- pnpm dev --no-fork
|
- pnpm dev
|
||||||
|
|
||||||
ui:ci:
|
|
||||||
desc: Run frontend build in CI mode
|
|
||||||
dir: frontend
|
|
||||||
cmds:
|
|
||||||
- pnpm dev &
|
|
||||||
silent: true
|
|
||||||
|
|
||||||
ui:fix:
|
ui:fix:
|
||||||
desc: Runs prettier and eslint on the frontend
|
desc: Runs prettier and eslint on the frontend
|
||||||
@@ -189,37 +143,10 @@ tasks:
|
|||||||
cmds:
|
cmds:
|
||||||
- cd backend && go build ./app/api
|
- cd backend && go build ./app/api
|
||||||
- backend/api &
|
- backend/api &
|
||||||
- sleep 15
|
- sleep 5
|
||||||
- cd frontend && pnpm run test:ci
|
- cd frontend && pnpm run test:ci
|
||||||
silent: true
|
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 15
|
|
||||||
- cd frontend && pnpm run test:ci
|
|
||||||
silent: true
|
|
||||||
|
|
||||||
test:e2e:
|
|
||||||
desc: Runs end-to-end test on a live server
|
|
||||||
dir: frontend
|
|
||||||
cmds:
|
|
||||||
- task: go:ci:with-frontend
|
|
||||||
- pnpm exec playwright install-deps
|
|
||||||
- pnpm exec playwright install
|
|
||||||
- sleep 30
|
|
||||||
- TEST_SHUTDOWN_API_SERVER=true E2E_BASE_URL=http://localhost:7745 pnpm exec playwright test -c ./test/playwright.config.ts {{ .CLI_ARGS }}
|
|
||||||
|
|
||||||
pr:
|
pr:
|
||||||
desc: Runs all tasks required for a PR
|
desc: Runs all tasks required for a PR
|
||||||
cmds:
|
cmds:
|
||||||
|
|||||||
@@ -1,81 +1,74 @@
|
|||||||
version: "2"
|
run:
|
||||||
|
timeout: 10m
|
||||||
|
skip-dirs:
|
||||||
|
- internal/data/ent.*
|
||||||
|
linters-settings:
|
||||||
|
goconst:
|
||||||
|
min-len: 5
|
||||||
|
min-occurrences: 5
|
||||||
|
exhaustive:
|
||||||
|
default-signifies-exhaustive: true
|
||||||
|
revive:
|
||||||
|
ignore-generated-header: false
|
||||||
|
severity: warning
|
||||||
|
confidence: 3
|
||||||
|
depguard:
|
||||||
|
rules:
|
||||||
|
main:
|
||||||
|
deny:
|
||||||
|
- pkg: io/util
|
||||||
|
desc: |
|
||||||
|
Deprecated: As of Go 1.16, the same functionality is now provided by
|
||||||
|
package io or package os, and those implementations should be
|
||||||
|
preferred in new code. See the specific function documentation for
|
||||||
|
details.
|
||||||
|
gocritic:
|
||||||
|
enabled-checks:
|
||||||
|
- ruleguard
|
||||||
|
testifylint:
|
||||||
|
enable-all: true
|
||||||
|
tagalign:
|
||||||
|
order:
|
||||||
|
- json
|
||||||
|
- schema
|
||||||
|
- yaml
|
||||||
|
- yml
|
||||||
|
- toml
|
||||||
|
- validate
|
||||||
linters:
|
linters:
|
||||||
default: none
|
disable-all: true
|
||||||
enable:
|
enable:
|
||||||
- asciicheck
|
- asciicheck
|
||||||
- bodyclose
|
- bodyclose
|
||||||
- copyloopvar
|
|
||||||
- depguard
|
- depguard
|
||||||
- dogsled
|
- dogsled
|
||||||
- errcheck
|
- errcheck
|
||||||
- errorlint
|
- errorlint
|
||||||
- exhaustive
|
- exhaustive
|
||||||
|
- exportloopref
|
||||||
- gochecknoinits
|
- gochecknoinits
|
||||||
- goconst
|
- goconst
|
||||||
- gocritic
|
- gocritic
|
||||||
- gocyclo
|
- gocyclo
|
||||||
|
- gofmt
|
||||||
- goprintffuncname
|
- goprintffuncname
|
||||||
|
- gosimple
|
||||||
- govet
|
- govet
|
||||||
- ineffassign
|
- ineffassign
|
||||||
- misspell
|
- misspell
|
||||||
- nakedret
|
- nakedret
|
||||||
- revive
|
- revive
|
||||||
- sqlclosecheck
|
|
||||||
- staticcheck
|
- staticcheck
|
||||||
|
- stylecheck
|
||||||
- tagalign
|
- tagalign
|
||||||
- testifylint
|
- testifylint
|
||||||
|
- typecheck
|
||||||
|
- typecheck
|
||||||
- unconvert
|
- unconvert
|
||||||
- unused
|
- unused
|
||||||
- whitespace
|
- whitespace
|
||||||
- zerologlint
|
- zerologlint
|
||||||
settings:
|
- sqlclosecheck
|
||||||
depguard:
|
|
||||||
rules:
|
|
||||||
main:
|
|
||||||
deny:
|
|
||||||
- pkg: io/util
|
|
||||||
desc: |
|
|
||||||
Deprecated: As of Go 1.16, the same functionality is now provided by
|
|
||||||
package io or package os, and those implementations should be
|
|
||||||
preferred in new code. See the specific function documentation for
|
|
||||||
details.
|
|
||||||
exhaustive:
|
|
||||||
default-signifies-exhaustive: true
|
|
||||||
goconst:
|
|
||||||
min-len: 5
|
|
||||||
min-occurrences: 5
|
|
||||||
gocritic:
|
|
||||||
enabled-checks:
|
|
||||||
- ruleguard
|
|
||||||
revive:
|
|
||||||
confidence: 3
|
|
||||||
severity: warning
|
|
||||||
tagalign:
|
|
||||||
order:
|
|
||||||
- json
|
|
||||||
- schema
|
|
||||||
- yaml
|
|
||||||
- yml
|
|
||||||
- toml
|
|
||||||
- validate
|
|
||||||
testifylint:
|
|
||||||
enable-all: true
|
|
||||||
exclusions:
|
|
||||||
generated: lax
|
|
||||||
paths:
|
|
||||||
- internal/data/ent.*
|
|
||||||
- third_party$
|
|
||||||
- builtin$
|
|
||||||
- examples$
|
|
||||||
issues:
|
issues:
|
||||||
|
exclude-use-default: false
|
||||||
fix: true
|
fix: true
|
||||||
formatters:
|
|
||||||
enable:
|
|
||||||
- gofmt
|
|
||||||
exclusions:
|
|
||||||
generated: lax
|
|
||||||
paths:
|
|
||||||
- internal/data/ent.*
|
|
||||||
- third_party$
|
|
||||||
- builtin$
|
|
||||||
- examples$
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
version: 2
|
|
||||||
|
|
||||||
# This is an example .goreleaser.yml file with some sensible defaults.
|
# This is an example .goreleaser.yml file with some sensible defaults.
|
||||||
# Make sure to check the documentation at https://goreleaser.com
|
# Make sure to check the documentation at https://goreleaser.com
|
||||||
before:
|
before:
|
||||||
@@ -14,49 +12,19 @@ builds:
|
|||||||
- linux
|
- linux
|
||||||
- windows
|
- windows
|
||||||
- darwin
|
- darwin
|
||||||
- freebsd
|
|
||||||
goarch:
|
goarch:
|
||||||
- amd64
|
- amd64
|
||||||
- "386"
|
- "386"
|
||||||
- arm
|
- arm
|
||||||
- arm64
|
- arm64
|
||||||
- riscv64
|
|
||||||
flags:
|
|
||||||
- -trimpath
|
|
||||||
ldflags:
|
|
||||||
- -s -w
|
|
||||||
- -X main.version={{.Version}}
|
|
||||||
- -X main.commit={{.Commit}}
|
|
||||||
- -X main.date={{.Date}}
|
|
||||||
ignore:
|
ignore:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
goarch: arm
|
goarch: arm
|
||||||
- goos: windows
|
- goos: windows
|
||||||
goarch: "386"
|
goarch: "386"
|
||||||
- goos: freebsd
|
|
||||||
goarch: arm
|
|
||||||
- goos: freebsd
|
|
||||||
goarch: "386"
|
|
||||||
tags:
|
|
||||||
- >-
|
|
||||||
{{- if eq .Arch "riscv64" }}nodynamic
|
|
||||||
{{- else if eq .Arch "arm" }}nodynamic
|
|
||||||
{{- else if eq .Arch "386" }}nodynamic
|
|
||||||
{{- else if eq .Os "freebsd" }}nodynamic
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
signs:
|
|
||||||
- cmd: cosign
|
|
||||||
signature: "${artifact}.sigstore.json"
|
|
||||||
args:
|
|
||||||
- sign-blob
|
|
||||||
- "--bundle=${signature}"
|
|
||||||
- "${artifact}"
|
|
||||||
- "--yes"
|
|
||||||
artifacts: checksum
|
|
||||||
output: true
|
|
||||||
archives:
|
archives:
|
||||||
- formats: [ 'tar.gz' ]
|
- format: tar.gz
|
||||||
# this name template makes the OS and Arch compatible with the results of uname.
|
# this name template makes the OS and Arch compatible with the results of uname.
|
||||||
name_template: >-
|
name_template: >-
|
||||||
{{ .ProjectName }}_
|
{{ .ProjectName }}_
|
||||||
@@ -68,16 +36,11 @@ archives:
|
|||||||
# use zip for windows archives
|
# use zip for windows archives
|
||||||
format_overrides:
|
format_overrides:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
formats: [ 'zip' ]
|
format: zip
|
||||||
sboms:
|
|
||||||
- artifacts: archive
|
|
||||||
release:
|
|
||||||
extra_files:
|
|
||||||
- glob: dist/*.sig
|
|
||||||
checksum:
|
checksum:
|
||||||
name_template: 'checksums.txt'
|
name_template: 'checksums.txt'
|
||||||
snapshot:
|
snapshot:
|
||||||
version_template: "{{ incpatch .Version }}-next"
|
name_template: "{{ incpatch .Version }}-next"
|
||||||
changelog:
|
changelog:
|
||||||
sort: asc
|
sort: asc
|
||||||
filters:
|
filters:
|
||||||
|
|||||||
@@ -23,9 +23,8 @@ func NewTask(name string, interval time.Duration, fn func(context.Context)) *Bac
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (tsk *BackgroundTask) Start(ctx context.Context) error {
|
func (tsk *BackgroundTask) Start(ctx context.Context) error {
|
||||||
tsk.Fn(ctx)
|
|
||||||
|
|
||||||
timer := time.NewTimer(tsk.Interval)
|
timer := time.NewTimer(tsk.Interval)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -10,7 +9,7 @@ import (
|
|||||||
"github.com/sysadminsmedia/homebox/backend/internal/core/services"
|
"github.com/sysadminsmedia/homebox/backend/internal/core/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *app) SetupDemo() error {
|
func (a *app) SetupDemo() {
|
||||||
csvText := `HB.import_ref,HB.location,HB.labels,HB.quantity,HB.name,HB.description,HB.insured,HB.serial_number,HB.model_number,HB.manufacturer,HB.notes,HB.purchase_from,HB.purchase_price,HB.purchase_time,HB.lifetime_warranty,HB.warranty_expires,HB.warranty_details,HB.sold_to,HB.sold_price,HB.sold_time,HB.sold_notes
|
csvText := `HB.import_ref,HB.location,HB.labels,HB.quantity,HB.name,HB.description,HB.insured,HB.serial_number,HB.model_number,HB.manufacturer,HB.notes,HB.purchase_from,HB.purchase_price,HB.purchase_time,HB.lifetime_warranty,HB.warranty_expires,HB.warranty_details,HB.sold_to,HB.sold_price,HB.sold_time,HB.sold_notes
|
||||||
,Garage,IOT;Home Assistant; Z-Wave,1,Zooz Universal Relay ZEN17,"Zooz 700 Series Z-Wave Universal Relay ZEN17 for Awnings, Garage Doors, Sprinklers, and More | 2 NO-C-NC Relays (20A, 10A) | Signal Repeater | Hub Required (Compatible with SmartThings and Hubitat)",,,ZEN17,Zooz,,Amazon,39.95,10/13/2021,,,,,,,
|
,Garage,IOT;Home Assistant; Z-Wave,1,Zooz Universal Relay ZEN17,"Zooz 700 Series Z-Wave Universal Relay ZEN17 for Awnings, Garage Doors, Sprinklers, and More | 2 NO-C-NC Relays (20A, 10A) | Signal Repeater | Hub Required (Compatible with SmartThings and Hubitat)",,,ZEN17,Zooz,,Amazon,39.95,10/13/2021,,,,,,,
|
||||||
,Living Room,IOT;Home Assistant; Z-Wave,1,Zooz Motion Sensor,"Zooz Z-Wave Plus S2 Motion Sensor ZSE18 with Magnetic Mount, Works with Vera and SmartThings",,,ZSE18,Zooz,,Amazon,29.95,10/15/2021,,,,,,,
|
,Living Room,IOT;Home Assistant; Z-Wave,1,Zooz Motion Sensor,"Zooz Z-Wave Plus S2 Motion Sensor ZSE18 with Magnetic Mount, Works with Vera and SmartThings",,,ZSE18,Zooz,,Amazon,29.95,10/15/2021,,,,,,,
|
||||||
@@ -34,34 +33,34 @@ func (a *app) SetupDemo() error {
|
|||||||
_, err := a.services.User.Login(ctx, registration.Email, registration.Password, false)
|
_, err := a.services.User.Login(ctx, registration.Email, registration.Password, false)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.Info().Msg("Demo user already exists, skipping setup")
|
log.Info().Msg("Demo user already exists, skipping setup")
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Demo user does not exist, setting up demo")
|
log.Debug().Msg("Demo user does not exist, setting up demo")
|
||||||
_, err = a.services.User.RegisterUser(ctx, registration)
|
_, err = a.services.User.RegisterUser(ctx, registration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("Failed to register demo user")
|
log.Err(err).Msg("Failed to register demo user")
|
||||||
return errors.New("failed to setup demo")
|
log.Fatal().Msg("Failed to setup demo")
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := a.services.User.Login(ctx, registration.Email, registration.Password, false)
|
token, err := a.services.User.Login(ctx, registration.Email, registration.Password, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("Failed to login demo user")
|
log.Err(err).Msg("Failed to login demo user")
|
||||||
return errors.New("failed to setup demo")
|
log.Fatal().Msg("Failed to setup demo")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
self, err := a.services.User.GetSelf(ctx, token.Raw)
|
self, err := a.services.User.GetSelf(ctx, token.Raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("Failed to get self")
|
log.Err(err).Msg("Failed to get self")
|
||||||
return errors.New("failed to setup demo")
|
log.Fatal().Msg("Failed to setup demo")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = a.services.Items.CsvImport(ctx, self.GroupID, strings.NewReader(csvText))
|
_, err = a.services.Items.CsvImport(ctx, self.GroupID, strings.NewReader(csvText))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("Failed to import CSV")
|
log.Err(err).Msg("Failed to import CSV")
|
||||||
return errors.New("failed to setup demo")
|
log.Fatal().Msg("Failed to setup demo")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Msg("Demo setup complete")
|
log.Info().Msg("Demo setup complete")
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,9 @@ import (
|
|||||||
"github.com/hay-kot/httpkit/errchain"
|
"github.com/hay-kot/httpkit/errchain"
|
||||||
"github.com/hay-kot/httpkit/server"
|
"github.com/hay-kot/httpkit/server"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/sysadminsmedia/homebox/backend/app/api/providers"
|
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/core/services"
|
"github.com/sysadminsmedia/homebox/backend/internal/core/services"
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/core/services/reporting/eventbus"
|
"github.com/sysadminsmedia/homebox/backend/internal/core/services/reporting/eventbus"
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
|
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
|
|
||||||
|
|
||||||
"github.com/olahol/melody"
|
"github.com/olahol/melody"
|
||||||
)
|
)
|
||||||
@@ -59,12 +57,6 @@ func WithSecureCookies(secure bool) func(*V1Controller) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithURL(url string) func(*V1Controller) {
|
|
||||||
return func(ctrl *V1Controller) {
|
|
||||||
ctrl.url = url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type V1Controller struct {
|
type V1Controller struct {
|
||||||
cookieSecure bool
|
cookieSecure bool
|
||||||
repo *repo.AllRepos
|
repo *repo.AllRepos
|
||||||
@@ -73,9 +65,6 @@ type V1Controller struct {
|
|||||||
isDemo bool
|
isDemo bool
|
||||||
allowRegistration bool
|
allowRegistration bool
|
||||||
bus *eventbus.EventBus
|
bus *eventbus.EventBus
|
||||||
url string
|
|
||||||
config *config.Config
|
|
||||||
oidcProvider *providers.OIDCProvider
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -88,63 +77,38 @@ type (
|
|||||||
}
|
}
|
||||||
|
|
||||||
APISummary struct {
|
APISummary struct {
|
||||||
Healthy bool `json:"health"`
|
Healthy bool `json:"health"`
|
||||||
Versions []string `json:"versions"`
|
Versions []string `json:"versions"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Build Build `json:"build"`
|
Build Build `json:"build"`
|
||||||
Latest services.Latest `json:"latest"`
|
Demo bool `json:"demo"`
|
||||||
Demo bool `json:"demo"`
|
AllowRegistration bool `json:"allowRegistration"`
|
||||||
AllowRegistration bool `json:"allowRegistration"`
|
|
||||||
LabelPrinting bool `json:"labelPrinting"`
|
|
||||||
OIDC OIDCStatus `json:"oidc"`
|
|
||||||
}
|
|
||||||
|
|
||||||
OIDCStatus struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
ButtonText string `json:"buttonText,omitempty"`
|
|
||||||
AutoRedirect bool `json:"autoRedirect,omitempty"`
|
|
||||||
AllowLocal bool `json:"allowLocal"`
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
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{
|
ctrl := &V1Controller{
|
||||||
repo: repos,
|
repo: repos,
|
||||||
svc: svc,
|
svc: svc,
|
||||||
allowRegistration: true,
|
allowRegistration: true,
|
||||||
bus: bus,
|
bus: bus,
|
||||||
config: config,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, opt := range options {
|
for _, opt := range options {
|
||||||
opt(ctrl)
|
opt(ctrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctrl.initOIDCProvider()
|
|
||||||
|
|
||||||
return ctrl
|
return ctrl
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctrl *V1Controller) initOIDCProvider() {
|
|
||||||
if ctrl.config.OIDC.Enabled {
|
|
||||||
oidcProvider, err := providers.NewOIDCProvider(ctrl.svc.User, &ctrl.config.OIDC, &ctrl.config.Options, ctrl.cookieSecure)
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("failed to initialize OIDC provider at startup")
|
|
||||||
} else {
|
|
||||||
ctrl.oidcProvider = oidcProvider
|
|
||||||
log.Info().Msg("OIDC provider initialized successfully at startup")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleBase godoc
|
// HandleBase godoc
|
||||||
//
|
//
|
||||||
// @Summary Application Info
|
// @Summary Application Info
|
||||||
// @Tags Base
|
// @Tags Base
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} APISummary
|
// @Success 200 {object} APISummary
|
||||||
// @Router /v1/status [GET]
|
// @Router /v1/status [GET]
|
||||||
func (ctrl *V1Controller) HandleBase(ready ReadyFunc, build Build) errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleBase(ready ReadyFunc, build Build) errchain.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) error {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
return server.JSON(w, http.StatusOK, APISummary{
|
return server.JSON(w, http.StatusOK, APISummary{
|
||||||
@@ -152,27 +116,19 @@ func (ctrl *V1Controller) HandleBase(ready ReadyFunc, build Build) errchain.Hand
|
|||||||
Title: "Homebox",
|
Title: "Homebox",
|
||||||
Message: "Track, Manage, and Organize your Things",
|
Message: "Track, Manage, and Organize your Things",
|
||||||
Build: build,
|
Build: build,
|
||||||
Latest: ctrl.svc.BackgroundService.GetLatestVersion(),
|
|
||||||
Demo: ctrl.isDemo,
|
Demo: ctrl.isDemo,
|
||||||
AllowRegistration: ctrl.allowRegistration,
|
AllowRegistration: ctrl.allowRegistration,
|
||||||
LabelPrinting: ctrl.config.LabelMaker.PrintCommand != nil,
|
|
||||||
OIDC: OIDCStatus{
|
|
||||||
Enabled: ctrl.config.OIDC.Enabled,
|
|
||||||
ButtonText: ctrl.config.OIDC.ButtonText,
|
|
||||||
AutoRedirect: ctrl.config.OIDC.AutoRedirect,
|
|
||||||
AllowLocal: ctrl.config.Options.AllowLocalLogin,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleCurrency godoc
|
// HandleCurrency godoc
|
||||||
//
|
//
|
||||||
// @Summary Currency
|
// @Summary Currency
|
||||||
// @Tags Base
|
// @Tags Base
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} currencies.Currency
|
// @Success 200 {object} currencies.Currency
|
||||||
// @Router /v1/currency [GET]
|
// @Router /v1/currency [GET]
|
||||||
func (ctrl *V1Controller) HandleCurrency() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleCurrency() errchain.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) error {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
// Set Cache for 10 Minutes
|
// Set Cache for 10 Minutes
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -32,65 +32,52 @@ func actionHandlerFactory(ref string, fn func(context.Context, uuid.UUID) (int,
|
|||||||
|
|
||||||
// HandleEnsureAssetID godoc
|
// HandleEnsureAssetID godoc
|
||||||
//
|
//
|
||||||
// @Summary Ensure Asset IDs
|
// @Summary Ensure Asset IDs
|
||||||
// @Description Ensures all items in the database have an asset ID
|
// @Description Ensures all items in the database have an asset ID
|
||||||
// @Tags Actions
|
// @Tags Actions
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} ActionAmountResult
|
// @Success 200 {object} ActionAmountResult
|
||||||
// @Router /v1/actions/ensure-asset-ids [Post]
|
// @Router /v1/actions/ensure-asset-ids [Post]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleEnsureAssetID() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleEnsureAssetID() errchain.HandlerFunc {
|
||||||
return actionHandlerFactory("ensure asset IDs", ctrl.svc.Items.EnsureAssetID)
|
return actionHandlerFactory("ensure asset IDs", ctrl.svc.Items.EnsureAssetID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleEnsureImportRefs godoc
|
// HandleEnsureImportRefs godoc
|
||||||
//
|
//
|
||||||
// @Summary Ensures Import Refs
|
// @Summary Ensures Import Refs
|
||||||
// @Description Ensures all items in the database have an import ref
|
// @Description Ensures all items in the database have an import ref
|
||||||
// @Tags Actions
|
// @Tags Actions
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} ActionAmountResult
|
// @Success 200 {object} ActionAmountResult
|
||||||
// @Router /v1/actions/ensure-import-refs [Post]
|
// @Router /v1/actions/ensure-import-refs [Post]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleEnsureImportRefs() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleEnsureImportRefs() errchain.HandlerFunc {
|
||||||
return actionHandlerFactory("ensure import refs", ctrl.svc.Items.EnsureImportRef)
|
return actionHandlerFactory("ensure import refs", ctrl.svc.Items.EnsureImportRef)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleItemDateZeroOut godoc
|
// HandleItemDateZeroOut godoc
|
||||||
//
|
//
|
||||||
// @Summary Zero Out Time Fields
|
// @Summary Zero Out Time Fields
|
||||||
// @Description Resets all item date fields to the beginning of the day
|
// @Description Resets all item date fields to the beginning of the day
|
||||||
// @Tags Actions
|
// @Tags Actions
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} ActionAmountResult
|
// @Success 200 {object} ActionAmountResult
|
||||||
// @Router /v1/actions/zero-item-time-fields [Post]
|
// @Router /v1/actions/zero-item-time-fields [Post]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleItemDateZeroOut() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemDateZeroOut() errchain.HandlerFunc {
|
||||||
return actionHandlerFactory("zero out date time", ctrl.repo.Items.ZeroOutTimeFields)
|
return actionHandlerFactory("zero out date time", ctrl.repo.Items.ZeroOutTimeFields)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleSetPrimaryPhotos godoc
|
// HandleSetPrimaryPhotos godoc
|
||||||
//
|
//
|
||||||
// @Summary Set Primary Photos
|
// @Summary Set Primary Photos
|
||||||
// @Description Sets the first photo of each item as the primary photo
|
// @Description Sets the first photo of each item as the primary photo
|
||||||
// @Tags Actions
|
// @Tags Actions
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} ActionAmountResult
|
// @Success 200 {object} ActionAmountResult
|
||||||
// @Router /v1/actions/set-primary-photos [Post]
|
// @Router /v1/actions/set-primary-photos [Post]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleSetPrimaryPhotos() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleSetPrimaryPhotos() errchain.HandlerFunc {
|
||||||
return actionHandlerFactory("ensure asset IDs", ctrl.repo.Items.SetPrimaryPhotos)
|
return actionHandlerFactory("ensure asset IDs", ctrl.repo.Items.SetPrimaryPhotos)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleCreateMissingThumbnails godoc
|
|
||||||
//
|
|
||||||
// @Summary Create Missing Thumbnails
|
|
||||||
// @Description Creates thumbnails for items that are missing them
|
|
||||||
// @Tags Actions
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} ActionAmountResult
|
|
||||||
// @Router /v1/actions/create-missing-thumbnails [Post]
|
|
||||||
// @Security Bearer
|
|
||||||
func (ctrl *V1Controller) HandleCreateMissingThumbnails() errchain.HandlerFunc {
|
|
||||||
return actionHandlerFactory("create missing thumbnails", ctrl.repo.Attachments.CreateMissingThumbnails)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ import (
|
|||||||
|
|
||||||
// HandleAssetGet godocs
|
// HandleAssetGet godocs
|
||||||
//
|
//
|
||||||
// @Summary Get Item by Asset ID
|
// @Summary Get Item by Asset ID
|
||||||
// @Tags Items
|
// @Tags Items
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "Asset ID"
|
// @Param id path string true "Asset ID"
|
||||||
// @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{}
|
// @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{}
|
||||||
// @Router /v1/assets/{id} [GET]
|
// @Router /v1/assets/{id} [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleAssetGet() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleAssetGet() errchain.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) error {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
ctx := services.NewContext(r.Context())
|
ctx := services.NewContext(r.Context())
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package v1
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -29,8 +28,8 @@ type (
|
|||||||
}
|
}
|
||||||
|
|
||||||
LoginForm struct {
|
LoginForm struct {
|
||||||
Username string `json:"username" example:"admin@admin.com"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password" example:"admin"`
|
Password string `json:"password"`
|
||||||
StayLoggedIn bool `json:"stayLoggedIn"`
|
StayLoggedIn bool `json:"stayLoggedIn"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -80,15 +79,17 @@ type AuthProvider interface {
|
|||||||
|
|
||||||
// HandleAuthLogin godoc
|
// HandleAuthLogin godoc
|
||||||
//
|
//
|
||||||
// @Summary User Login
|
// @Summary User Login
|
||||||
// @Tags Authentication
|
// @Tags Authentication
|
||||||
// @Accept x-www-form-urlencoded
|
// @Accept x-www-form-urlencoded
|
||||||
// @Accept application/json
|
// @Accept application/json
|
||||||
// @Param payload body LoginForm true "Login Data"
|
// @Param username formData string false "string" example(admin@admin.com)
|
||||||
// @Param provider query string false "auth provider"
|
// @Param password formData string false "string" example(admin)
|
||||||
// @Produce json
|
// @Param payload body LoginForm true "Login Data"
|
||||||
// @Success 200 {object} TokenResponse
|
// @Param provider query string false "auth provider"
|
||||||
// @Router /v1/users/login [POST]
|
// @Produce json
|
||||||
|
// @Success 200 {object} TokenResponse
|
||||||
|
// @Router /v1/users/login [POST]
|
||||||
func (ctrl *V1Controller) HandleAuthLogin(ps ...AuthProvider) errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleAuthLogin(ps ...AuthProvider) errchain.HandlerFunc {
|
||||||
if len(ps) == 0 {
|
if len(ps) == 0 {
|
||||||
panic("no auth providers provided")
|
panic("no auth providers provided")
|
||||||
@@ -107,11 +108,6 @@ func (ctrl *V1Controller) HandleAuthLogin(ps ...AuthProvider) errchain.HandlerFu
|
|||||||
provider = "local"
|
provider = "local"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Block local only when disabled
|
|
||||||
if provider == "local" && !ctrl.config.Options.AllowLocalLogin {
|
|
||||||
return validate.NewRequestError(fmt.Errorf("local login is not enabled"), http.StatusForbidden)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the provider
|
// Get the provider
|
||||||
p, ok := providers[provider]
|
p, ok := providers[provider]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -120,8 +116,8 @@ func (ctrl *V1Controller) HandleAuthLogin(ps ...AuthProvider) errchain.HandlerFu
|
|||||||
|
|
||||||
newToken, err := p.Authenticate(w, r)
|
newToken, err := p.Authenticate(w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msg("authentication failed")
|
log.Err(err).Msg("failed to authenticate")
|
||||||
return validate.NewUnauthorizedError()
|
return server.JSON(w, http.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
ctrl.setCookies(w, noPort(r.Host), newToken.Raw, newToken.ExpiresAt, true)
|
ctrl.setCookies(w, noPort(r.Host), newToken.Raw, newToken.ExpiresAt, true)
|
||||||
@@ -135,11 +131,11 @@ func (ctrl *V1Controller) HandleAuthLogin(ps ...AuthProvider) errchain.HandlerFu
|
|||||||
|
|
||||||
// HandleAuthLogout godoc
|
// HandleAuthLogout godoc
|
||||||
//
|
//
|
||||||
// @Summary User Logout
|
// @Summary User Logout
|
||||||
// @Tags Authentication
|
// @Tags Authentication
|
||||||
// @Success 204
|
// @Success 204
|
||||||
// @Router /v1/users/logout [POST]
|
// @Router /v1/users/logout [POST]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleAuthLogout() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleAuthLogout() errchain.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) error {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
token := services.UseTokenCtx(r.Context())
|
token := services.UseTokenCtx(r.Context())
|
||||||
@@ -159,13 +155,13 @@ func (ctrl *V1Controller) HandleAuthLogout() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleAuthRefresh godoc
|
// HandleAuthRefresh godoc
|
||||||
//
|
//
|
||||||
// @Summary User Token Refresh
|
// @Summary User Token Refresh
|
||||||
// @Description handleAuthRefresh returns a handler that will issue a new token from an existing token.
|
// @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.
|
// @Description This does not validate that the user still exists within the database.
|
||||||
// @Tags Authentication
|
// @Tags Authentication
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Router /v1/users/refresh [GET]
|
// @Router /v1/users/refresh [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleAuthRefresh() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleAuthRefresh() errchain.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) error {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
requestToken := services.UseTokenCtx(r.Context())
|
requestToken := services.UseTokenCtx(r.Context())
|
||||||
@@ -253,65 +249,3 @@ func (ctrl *V1Controller) unsetCookies(w http.ResponseWriter, domain string) {
|
|||||||
Path: "/",
|
Path: "/",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleOIDCLogin godoc
|
|
||||||
//
|
|
||||||
// @Summary OIDC Login Initiation
|
|
||||||
// @Tags Authentication
|
|
||||||
// @Produce json
|
|
||||||
// @Success 302
|
|
||||||
// @Router /v1/users/login/oidc [GET]
|
|
||||||
func (ctrl *V1Controller) HandleOIDCLogin() errchain.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
// Forbidden if OIDC is not enabled
|
|
||||||
if !ctrl.config.OIDC.Enabled {
|
|
||||||
return validate.NewRequestError(fmt.Errorf("OIDC is not enabled"), http.StatusForbidden)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if OIDC provider is available
|
|
||||||
if ctrl.oidcProvider == nil {
|
|
||||||
log.Error().Msg("OIDC provider not initialized")
|
|
||||||
return validate.NewRequestError(errors.New("OIDC provider not available"), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initiate OIDC flow
|
|
||||||
_, err := ctrl.oidcProvider.InitiateOIDCFlow(w, r)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleOIDCCallback godoc
|
|
||||||
//
|
|
||||||
// @Summary OIDC Callback Handler
|
|
||||||
// @Tags Authentication
|
|
||||||
// @Param code query string true "Authorization code"
|
|
||||||
// @Param state query string true "State parameter"
|
|
||||||
// @Success 302
|
|
||||||
// @Router /v1/users/login/oidc/callback [GET]
|
|
||||||
func (ctrl *V1Controller) HandleOIDCCallback() errchain.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
// Forbidden if OIDC is not enabled
|
|
||||||
if !ctrl.config.OIDC.Enabled {
|
|
||||||
return validate.NewRequestError(fmt.Errorf("OIDC is not enabled"), http.StatusForbidden)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if OIDC provider is available
|
|
||||||
if ctrl.oidcProvider == nil {
|
|
||||||
log.Error().Msg("OIDC provider not initialized")
|
|
||||||
return validate.NewRequestError(errors.New("OIDC provider not available"), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle callback
|
|
||||||
newToken, err := ctrl.oidcProvider.HandleCallback(w, r)
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("OIDC callback failed")
|
|
||||||
http.Redirect(w, r, "/?oidc_error=oidc_auth_failed", http.StatusFound)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set cookies and redirect to home
|
|
||||||
ctrl.setCookies(w, noPort(r.Host), newToken.Raw, newToken.ExpiresAt, true)
|
|
||||||
http.Redirect(w, r, "/home", http.StatusFound)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ type (
|
|||||||
|
|
||||||
// HandleGroupGet godoc
|
// HandleGroupGet godoc
|
||||||
//
|
//
|
||||||
// @Summary Get Group
|
// @Summary Get Group
|
||||||
// @Tags Group
|
// @Tags Group
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} repo.Group
|
// @Success 200 {object} repo.Group
|
||||||
// @Router /v1/groups [Get]
|
// @Router /v1/groups [Get]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleGroupGet() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleGroupGet() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request) (repo.Group, error) {
|
fn := func(r *http.Request) (repo.Group, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
@@ -43,13 +43,13 @@ func (ctrl *V1Controller) HandleGroupGet() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleGroupUpdate godoc
|
// HandleGroupUpdate godoc
|
||||||
//
|
//
|
||||||
// @Summary Update Group
|
// @Summary Update Group
|
||||||
// @Tags Group
|
// @Tags Group
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param payload body repo.GroupUpdate true "User Data"
|
// @Param payload body repo.GroupUpdate true "User Data"
|
||||||
// @Success 200 {object} repo.Group
|
// @Success 200 {object} repo.Group
|
||||||
// @Router /v1/groups [Put]
|
// @Router /v1/groups [Put]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleGroupUpdate() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleGroupUpdate() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request, body repo.GroupUpdate) (repo.Group, error) {
|
fn := func(r *http.Request, body repo.GroupUpdate) (repo.Group, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
@@ -69,13 +69,13 @@ func (ctrl *V1Controller) HandleGroupUpdate() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleGroupInvitationsCreate godoc
|
// HandleGroupInvitationsCreate godoc
|
||||||
//
|
//
|
||||||
// @Summary Create Group Invitation
|
// @Summary Create Group Invitation
|
||||||
// @Tags Group
|
// @Tags Group
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param payload body GroupInvitationCreate true "User Data"
|
// @Param payload body GroupInvitationCreate true "User Data"
|
||||||
// @Success 200 {object} GroupInvitation
|
// @Success 200 {object} GroupInvitation
|
||||||
// @Router /v1/groups/invitations [Post]
|
// @Router /v1/groups/invitations [Post]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleGroupInvitationsCreate() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleGroupInvitationsCreate() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request, body GroupInvitationCreate) (GroupInvitation, error) {
|
fn := func(r *http.Request, body GroupInvitationCreate) (GroupInvitation, error) {
|
||||||
if body.ExpiresAt.IsZero() {
|
if body.ExpiresAt.IsZero() {
|
||||||
|
|||||||
@@ -1,164 +0,0 @@
|
|||||||
package v1
|
|
||||||
|
|
||||||
import (
|
|
||||||
"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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HandleItemTemplatesGetAll godoc
|
|
||||||
//
|
|
||||||
// @Summary Get All Item Templates
|
|
||||||
// @Tags Item Templates
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} []repo.ItemTemplateSummary
|
|
||||||
// @Router /v1/templates [GET]
|
|
||||||
// @Security Bearer
|
|
||||||
func (ctrl *V1Controller) HandleItemTemplatesGetAll() errchain.HandlerFunc {
|
|
||||||
fn := func(r *http.Request) ([]repo.ItemTemplateSummary, error) {
|
|
||||||
auth := services.NewContext(r.Context())
|
|
||||||
return ctrl.repo.ItemTemplates.GetAll(r.Context(), auth.GID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return adapters.Command(fn, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleItemTemplatesGet godoc
|
|
||||||
//
|
|
||||||
// @Summary Get Item Template
|
|
||||||
// @Tags Item Templates
|
|
||||||
// @Produce json
|
|
||||||
// @Param id path string true "Template ID"
|
|
||||||
// @Success 200 {object} repo.ItemTemplateOut
|
|
||||||
// @Router /v1/templates/{id} [GET]
|
|
||||||
// @Security Bearer
|
|
||||||
func (ctrl *V1Controller) HandleItemTemplatesGet() errchain.HandlerFunc {
|
|
||||||
fn := func(r *http.Request, ID uuid.UUID) (repo.ItemTemplateOut, error) {
|
|
||||||
auth := services.NewContext(r.Context())
|
|
||||||
return ctrl.repo.ItemTemplates.GetOne(r.Context(), auth.GID, ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return adapters.CommandID("id", fn, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleItemTemplatesCreate godoc
|
|
||||||
//
|
|
||||||
// @Summary Create Item Template
|
|
||||||
// @Tags Item Templates
|
|
||||||
// @Produce json
|
|
||||||
// @Param payload body repo.ItemTemplateCreate true "Template Data"
|
|
||||||
// @Success 201 {object} repo.ItemTemplateOut
|
|
||||||
// @Router /v1/templates [POST]
|
|
||||||
// @Security Bearer
|
|
||||||
func (ctrl *V1Controller) HandleItemTemplatesCreate() errchain.HandlerFunc {
|
|
||||||
fn := func(r *http.Request, body repo.ItemTemplateCreate) (repo.ItemTemplateOut, error) {
|
|
||||||
auth := services.NewContext(r.Context())
|
|
||||||
return ctrl.repo.ItemTemplates.Create(r.Context(), auth.GID, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
return adapters.Action(fn, http.StatusCreated)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleItemTemplatesUpdate godoc
|
|
||||||
//
|
|
||||||
// @Summary Update Item Template
|
|
||||||
// @Tags Item Templates
|
|
||||||
// @Produce json
|
|
||||||
// @Param id path string true "Template ID"
|
|
||||||
// @Param payload body repo.ItemTemplateUpdate true "Template Data"
|
|
||||||
// @Success 200 {object} repo.ItemTemplateOut
|
|
||||||
// @Router /v1/templates/{id} [PUT]
|
|
||||||
// @Security Bearer
|
|
||||||
func (ctrl *V1Controller) HandleItemTemplatesUpdate() errchain.HandlerFunc {
|
|
||||||
fn := func(r *http.Request, ID uuid.UUID, body repo.ItemTemplateUpdate) (repo.ItemTemplateOut, error) {
|
|
||||||
auth := services.NewContext(r.Context())
|
|
||||||
body.ID = ID
|
|
||||||
return ctrl.repo.ItemTemplates.Update(r.Context(), auth.GID, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
return adapters.ActionID("id", fn, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleItemTemplatesDelete godoc
|
|
||||||
//
|
|
||||||
// @Summary Delete Item Template
|
|
||||||
// @Tags Item Templates
|
|
||||||
// @Produce json
|
|
||||||
// @Param id path string true "Template ID"
|
|
||||||
// @Success 204
|
|
||||||
// @Router /v1/templates/{id} [DELETE]
|
|
||||||
// @Security Bearer
|
|
||||||
func (ctrl *V1Controller) HandleItemTemplatesDelete() errchain.HandlerFunc {
|
|
||||||
fn := func(r *http.Request, ID uuid.UUID) (any, error) {
|
|
||||||
auth := services.NewContext(r.Context())
|
|
||||||
err := ctrl.repo.ItemTemplates.Delete(r.Context(), auth.GID, ID)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return adapters.CommandID("id", fn, http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ItemTemplateCreateItemRequest struct {
|
|
||||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
|
||||||
Description string `json:"description" validate:"max=1000"`
|
|
||||||
LocationID uuid.UUID `json:"locationId" validate:"required"`
|
|
||||||
LabelIDs []uuid.UUID `json:"labelIds"`
|
|
||||||
Quantity *int `json:"quantity"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleItemTemplatesCreateItem godoc
|
|
||||||
//
|
|
||||||
// @Summary Create Item from Template
|
|
||||||
// @Tags Item Templates
|
|
||||||
// @Produce json
|
|
||||||
// @Param id path string true "Template ID"
|
|
||||||
// @Param payload body ItemTemplateCreateItemRequest true "Item Data"
|
|
||||||
// @Success 201 {object} repo.ItemOut
|
|
||||||
// @Router /v1/templates/{id}/create-item [POST]
|
|
||||||
// @Security Bearer
|
|
||||||
func (ctrl *V1Controller) HandleItemTemplatesCreateItem() errchain.HandlerFunc {
|
|
||||||
fn := func(r *http.Request, templateID uuid.UUID, body ItemTemplateCreateItemRequest) (repo.ItemOut, error) {
|
|
||||||
auth := services.NewContext(r.Context())
|
|
||||||
|
|
||||||
template, err := ctrl.repo.ItemTemplates.GetOne(r.Context(), auth.GID, templateID)
|
|
||||||
if err != nil {
|
|
||||||
return repo.ItemOut{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
quantity := template.DefaultQuantity
|
|
||||||
if body.Quantity != nil {
|
|
||||||
quantity = *body.Quantity
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build custom fields from template
|
|
||||||
fields := make([]repo.ItemField, len(template.Fields))
|
|
||||||
for i, f := range template.Fields {
|
|
||||||
fields[i] = repo.ItemField{
|
|
||||||
Type: f.Type,
|
|
||||||
Name: f.Name,
|
|
||||||
TextValue: f.TextValue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create item with all template data in a single transaction
|
|
||||||
return ctrl.repo.Items.CreateFromTemplate(r.Context(), auth.GID, repo.ItemCreateFromTemplate{
|
|
||||||
Name: body.Name,
|
|
||||||
Description: body.Description,
|
|
||||||
Quantity: quantity,
|
|
||||||
LocationID: body.LocationID,
|
|
||||||
LabelIDs: body.LabelIDs,
|
|
||||||
Insured: template.DefaultInsured,
|
|
||||||
Manufacturer: template.DefaultManufacturer,
|
|
||||||
ModelNumber: template.DefaultModelNumber,
|
|
||||||
LifetimeWarranty: template.DefaultLifetimeWarranty,
|
|
||||||
WarrantyDetails: template.DefaultWarrantyDetails,
|
|
||||||
Fields: fields,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return adapters.ActionID("id", fn, http.StatusCreated)
|
|
||||||
}
|
|
||||||
@@ -4,11 +4,9 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"math/big"
|
"math/big"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/hay-kot/httpkit/errchain"
|
"github.com/hay-kot/httpkit/errchain"
|
||||||
@@ -22,18 +20,18 @@ import (
|
|||||||
|
|
||||||
// HandleItemsGetAll godoc
|
// HandleItemsGetAll godoc
|
||||||
//
|
//
|
||||||
// @Summary Query All Items
|
// @Summary Query All Items
|
||||||
// @Tags Items
|
// @Tags Items
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param q query string false "search string"
|
// @Param q query string false "search string"
|
||||||
// @Param page query int false "page number"
|
// @Param page query int false "page number"
|
||||||
// @Param pageSize query int false "items per page"
|
// @Param pageSize query int false "items per page"
|
||||||
// @Param labels query []string false "label Ids" collectionFormat(multi)
|
// @Param labels query []string false "label Ids" collectionFormat(multi)
|
||||||
// @Param locations query []string false "location Ids" collectionFormat(multi)
|
// @Param locations query []string false "location Ids" collectionFormat(multi)
|
||||||
// @Param parentIds query []string false "parent Ids" collectionFormat(multi)
|
// @Param parentIds query []string false "parent Ids" collectionFormat(multi)
|
||||||
// @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{}
|
// @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{}
|
||||||
// @Router /v1/items [GET]
|
// @Router /v1/items [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleItemsGetAll() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemsGetAll() errchain.HandlerFunc {
|
||||||
extractQuery := func(r *http.Request) repo.ItemQuery {
|
extractQuery := func(r *http.Request) repo.ItemQuery {
|
||||||
params := r.URL.Query()
|
params := r.URL.Query()
|
||||||
@@ -55,18 +53,16 @@ func (ctrl *V1Controller) HandleItemsGetAll() errchain.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
v := repo.ItemQuery{
|
v := repo.ItemQuery{
|
||||||
Page: queryIntOrNegativeOne(params.Get("page")),
|
Page: queryIntOrNegativeOne(params.Get("page")),
|
||||||
PageSize: queryIntOrNegativeOne(params.Get("pageSize")),
|
PageSize: queryIntOrNegativeOne(params.Get("pageSize")),
|
||||||
Search: params.Get("q"),
|
Search: params.Get("q"),
|
||||||
LocationIDs: queryUUIDList(params, "locations"),
|
LocationIDs: queryUUIDList(params, "locations"),
|
||||||
LabelIDs: queryUUIDList(params, "labels"),
|
LabelIDs: queryUUIDList(params, "labels"),
|
||||||
NegateLabels: queryBool(params.Get("negateLabels")),
|
NegateLabels: queryBool(params.Get("negateLabels")),
|
||||||
OnlyWithoutPhoto: queryBool(params.Get("onlyWithoutPhoto")),
|
ParentItemIDs: queryUUIDList(params, "parentIds"),
|
||||||
OnlyWithPhoto: queryBool(params.Get("onlyWithPhoto")),
|
IncludeArchived: queryBool(params.Get("includeArchived")),
|
||||||
ParentItemIDs: queryUUIDList(params, "parentIds"),
|
Fields: filterFieldItems(params["fields"]),
|
||||||
IncludeArchived: queryBool(params.Get("includeArchived")),
|
OrderBy: params.Get("orderBy"),
|
||||||
Fields: filterFieldItems(params["fields"]),
|
|
||||||
OrderBy: params.Get("orderBy"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(v.Search, "#") {
|
if strings.HasPrefix(v.Search, "#") {
|
||||||
@@ -88,9 +84,6 @@ func (ctrl *V1Controller) HandleItemsGetAll() errchain.HandlerFunc {
|
|||||||
items, err := ctrl.repo.Items.QueryByGroup(ctx, ctx.GID, extractQuery(r))
|
items, err := ctrl.repo.Items.QueryByGroup(ctx, ctx.GID, extractQuery(r))
|
||||||
totalPrice := new(big.Int)
|
totalPrice := new(big.Int)
|
||||||
for _, item := range items.Items {
|
for _, item := range items.Items {
|
||||||
if !item.SoldTime.IsZero() { // Skip items with a non-null SoldDate
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
totalPrice.Add(totalPrice, big.NewInt(int64(item.PurchasePrice*100)))
|
totalPrice.Add(totalPrice, big.NewInt(int64(item.PurchasePrice*100)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,13 +105,13 @@ func (ctrl *V1Controller) HandleItemsGetAll() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleItemFullPath godoc
|
// HandleItemFullPath godoc
|
||||||
//
|
//
|
||||||
// @Summary Get the full path of an item
|
// @Summary Get the full path of an item
|
||||||
// @Tags Items
|
// @Tags Items
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "Item ID"
|
// @Param id path string true "Item ID"
|
||||||
// @Success 200 {object} []repo.ItemPath
|
// @Success 200 {object} []repo.ItemPath
|
||||||
// @Router /v1/items/{id}/path [GET]
|
// @Router /v1/items/{id}/path [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleItemFullPath() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemFullPath() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request, ID uuid.UUID) ([]repo.ItemPath, error) {
|
fn := func(r *http.Request, ID uuid.UUID) ([]repo.ItemPath, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
@@ -154,13 +147,13 @@ func (ctrl *V1Controller) HandleItemFullPath() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleItemsCreate godoc
|
// HandleItemsCreate godoc
|
||||||
//
|
//
|
||||||
// @Summary Create Item
|
// @Summary Create Item
|
||||||
// @Tags Items
|
// @Tags Items
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param payload body repo.ItemCreate true "Item Data"
|
// @Param payload body repo.ItemCreate true "Item Data"
|
||||||
// @Success 201 {object} repo.ItemSummary
|
// @Success 201 {object} repo.ItemSummary
|
||||||
// @Router /v1/items [POST]
|
// @Router /v1/items [POST]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleItemsCreate() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemsCreate() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request, body repo.ItemCreate) (repo.ItemOut, error) {
|
fn := func(r *http.Request, body repo.ItemCreate) (repo.ItemOut, error) {
|
||||||
return ctrl.svc.Items.Create(services.NewContext(r.Context()), body)
|
return ctrl.svc.Items.Create(services.NewContext(r.Context()), body)
|
||||||
@@ -171,13 +164,13 @@ func (ctrl *V1Controller) HandleItemsCreate() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleItemGet godocs
|
// HandleItemGet godocs
|
||||||
//
|
//
|
||||||
// @Summary Get Item
|
// @Summary Get Item
|
||||||
// @Tags Items
|
// @Tags Items
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "Item ID"
|
// @Param id path string true "Item ID"
|
||||||
// @Success 200 {object} repo.ItemOut
|
// @Success 200 {object} repo.ItemOut
|
||||||
// @Router /v1/items/{id} [GET]
|
// @Router /v1/items/{id} [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleItemGet() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemGet() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request, ID uuid.UUID) (repo.ItemOut, error) {
|
fn := func(r *http.Request, ID uuid.UUID) (repo.ItemOut, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
@@ -190,13 +183,13 @@ func (ctrl *V1Controller) HandleItemGet() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleItemDelete godocs
|
// HandleItemDelete godocs
|
||||||
//
|
//
|
||||||
// @Summary Delete Item
|
// @Summary Delete Item
|
||||||
// @Tags Items
|
// @Tags Items
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "Item ID"
|
// @Param id path string true "Item ID"
|
||||||
// @Success 204
|
// @Success 204
|
||||||
// @Router /v1/items/{id} [DELETE]
|
// @Router /v1/items/{id} [DELETE]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleItemDelete() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemDelete() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request, ID uuid.UUID) (any, error) {
|
fn := func(r *http.Request, ID uuid.UUID) (any, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
@@ -209,14 +202,14 @@ func (ctrl *V1Controller) HandleItemDelete() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleItemUpdate godocs
|
// HandleItemUpdate godocs
|
||||||
//
|
//
|
||||||
// @Summary Update Item
|
// @Summary Update Item
|
||||||
// @Tags Items
|
// @Tags Items
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "Item ID"
|
// @Param id path string true "Item ID"
|
||||||
// @Param payload body repo.ItemUpdate true "Item Data"
|
// @Param payload body repo.ItemUpdate true "Item Data"
|
||||||
// @Success 200 {object} repo.ItemOut
|
// @Success 200 {object} repo.ItemOut
|
||||||
// @Router /v1/items/{id} [PUT]
|
// @Router /v1/items/{id} [PUT]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleItemUpdate() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemUpdate() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request, ID uuid.UUID, body repo.ItemUpdate) (repo.ItemOut, error) {
|
fn := func(r *http.Request, ID uuid.UUID, body repo.ItemUpdate) (repo.ItemOut, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
@@ -230,14 +223,14 @@ func (ctrl *V1Controller) HandleItemUpdate() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleItemPatch godocs
|
// HandleItemPatch godocs
|
||||||
//
|
//
|
||||||
// @Summary Update Item
|
// @Summary Update Item
|
||||||
// @Tags Items
|
// @Tags Items
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "Item ID"
|
// @Param id path string true "Item ID"
|
||||||
// @Param payload body repo.ItemPatch true "Item Data"
|
// @Param payload body repo.ItemPatch true "Item Data"
|
||||||
// @Success 200 {object} repo.ItemOut
|
// @Success 200 {object} repo.ItemOut
|
||||||
// @Router /v1/items/{id} [Patch]
|
// @Router /v1/items/{id} [Patch]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleItemPatch() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemPatch() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request, ID uuid.UUID, body repo.ItemPatch) (repo.ItemOut, error) {
|
fn := func(r *http.Request, ID uuid.UUID, body repo.ItemPatch) (repo.ItemOut, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
@@ -254,34 +247,15 @@ func (ctrl *V1Controller) HandleItemPatch() errchain.HandlerFunc {
|
|||||||
return adapters.ActionID("id", fn, http.StatusOK)
|
return adapters.ActionID("id", fn, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleItemDuplicate godocs
|
|
||||||
//
|
|
||||||
// @Summary Duplicate Item
|
|
||||||
// @Tags Items
|
|
||||||
// @Produce json
|
|
||||||
// @Param id path string true "Item ID"
|
|
||||||
// @Param payload body repo.DuplicateOptions true "Duplicate Options"
|
|
||||||
// @Success 201 {object} repo.ItemOut
|
|
||||||
// @Router /v1/items/{id}/duplicate [POST]
|
|
||||||
// @Security Bearer
|
|
||||||
func (ctrl *V1Controller) HandleItemDuplicate() errchain.HandlerFunc {
|
|
||||||
fn := func(r *http.Request, ID uuid.UUID, options repo.DuplicateOptions) (repo.ItemOut, error) {
|
|
||||||
ctx := services.NewContext(r.Context())
|
|
||||||
return ctrl.svc.Items.Duplicate(ctx, ctx.GID, ID, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
return adapters.ActionID("id", fn, http.StatusCreated)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleGetAllCustomFieldNames godocs
|
// HandleGetAllCustomFieldNames godocs
|
||||||
//
|
//
|
||||||
// @Summary Get All Custom Field Names
|
// @Summary Get All Custom Field Names
|
||||||
// @Tags Items
|
// @Tags Items
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Router /v1/items/fields [GET]
|
// @Router /v1/items/fields [GET]
|
||||||
// @Success 200 {object} []string
|
// @Success 200 {object} []string
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleGetAllCustomFieldNames() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleGetAllCustomFieldNames() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request) ([]string, error) {
|
fn := func(r *http.Request) ([]string, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
@@ -293,13 +267,13 @@ func (ctrl *V1Controller) HandleGetAllCustomFieldNames() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleGetAllCustomFieldValues godocs
|
// HandleGetAllCustomFieldValues godocs
|
||||||
//
|
//
|
||||||
// @Summary Get All Custom Field Values
|
// @Summary Get All Custom Field Values
|
||||||
// @Tags Items
|
// @Tags Items
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Router /v1/items/fields/values [GET]
|
// @Router /v1/items/fields/values [GET]
|
||||||
// @Success 200 {object} []string
|
// @Success 200 {object} []string
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleGetAllCustomFieldValues() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleGetAllCustomFieldValues() errchain.HandlerFunc {
|
||||||
type query struct {
|
type query struct {
|
||||||
Field string `schema:"field" validate:"required"`
|
Field string `schema:"field" validate:"required"`
|
||||||
@@ -315,14 +289,13 @@ func (ctrl *V1Controller) HandleGetAllCustomFieldValues() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleItemsImport godocs
|
// HandleItemsImport godocs
|
||||||
//
|
//
|
||||||
// @Summary Import Items
|
// @Summary Import Items
|
||||||
// @Tags Items
|
// @Tags Items
|
||||||
// @Accept multipart/form-data
|
// @Produce json
|
||||||
// @Produce json
|
// @Success 204
|
||||||
// @Success 204
|
// @Param csv formData file true "Image to upload"
|
||||||
// @Param csv formData file true "Image to upload"
|
// @Router /v1/items/import [Post]
|
||||||
// @Router /v1/items/import [Post]
|
// @Security Bearer
|
||||||
// @Security Bearer
|
|
||||||
func (ctrl *V1Controller) HandleItemsImport() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemsImport() errchain.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) error {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
err := r.ParseMultipartForm(ctrl.maxUploadSize << 20)
|
err := r.ParseMultipartForm(ctrl.maxUploadSize << 20)
|
||||||
@@ -351,26 +324,23 @@ func (ctrl *V1Controller) HandleItemsImport() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleItemsExport godocs
|
// HandleItemsExport godocs
|
||||||
//
|
//
|
||||||
// @Summary Export Items
|
// @Summary Export Items
|
||||||
// @Tags Items
|
// @Tags Items
|
||||||
// @Success 200 {string} string "text/csv"
|
// @Success 200 {string} string "text/csv"
|
||||||
// @Router /v1/items/export [GET]
|
// @Router /v1/items/export [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleItemsExport() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemsExport() errchain.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) error {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
ctx := services.NewContext(r.Context())
|
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)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to export items")
|
log.Err(err).Msg("failed to export items")
|
||||||
return validate.NewRequestError(err, http.StatusInternalServerError)
|
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-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 := csv.NewWriter(w)
|
||||||
writer.Comma = ','
|
writer.Comma = ','
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ package v1
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hay-kot/httpkit/errchain"
|
"github.com/hay-kot/httpkit/errchain"
|
||||||
@@ -15,13 +13,6 @@ import (
|
|||||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/attachment"
|
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/attachment"
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
|
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/validate"
|
"github.com/sysadminsmedia/homebox/backend/internal/sys/validate"
|
||||||
|
|
||||||
"gocloud.dev/blob"
|
|
||||||
_ "gocloud.dev/blob/azureblob"
|
|
||||||
_ "gocloud.dev/blob/fileblob"
|
|
||||||
_ "gocloud.dev/blob/gcsblob"
|
|
||||||
_ "gocloud.dev/blob/memblob"
|
|
||||||
_ "gocloud.dev/blob/s3blob"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -32,19 +23,17 @@ type (
|
|||||||
|
|
||||||
// HandleItemAttachmentCreate godocs
|
// HandleItemAttachmentCreate godocs
|
||||||
//
|
//
|
||||||
// @Summary Create Item Attachment
|
// @Summary Create Item Attachment
|
||||||
// @Tags Items Attachments
|
// @Tags Items Attachments
|
||||||
// @Accept multipart/form-data
|
// @Produce json
|
||||||
// @Produce json
|
// @Param id path string true "Item ID"
|
||||||
// @Param id path string true "Item ID"
|
// @Param file formData file true "File attachment"
|
||||||
// @Param file formData file true "File attachment"
|
// @Param type formData string true "Type of file"
|
||||||
// @Param type formData string false "Type of file"
|
// @Param name formData string true "name of the file including extension"
|
||||||
// @Param primary formData bool false "Is this the primary attachment"
|
// @Success 200 {object} repo.ItemOut
|
||||||
// @Param name formData string true "name of the file including extension"
|
// @Failure 422 {object} validate.ErrorResponse
|
||||||
// @Success 200 {object} repo.ItemOut
|
// @Router /v1/items/{id}/attachments [POST]
|
||||||
// @Failure 422 {object} validate.ErrorResponse
|
// @Security Bearer
|
||||||
// @Router /v1/items/{id}/attachments [POST]
|
|
||||||
// @Security Bearer
|
|
||||||
func (ctrl *V1Controller) HandleItemAttachmentCreate() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemAttachmentCreate() errchain.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) error {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
err := r.ParseMultipartForm(ctrl.maxUploadSize << 20)
|
err := r.ParseMultipartForm(ctrl.maxUploadSize << 20)
|
||||||
@@ -83,19 +72,13 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() errchain.HandlerFunc {
|
|||||||
ext := filepath.Ext(attachmentName)
|
ext := filepath.Ext(attachmentName)
|
||||||
|
|
||||||
switch strings.ToLower(ext) {
|
switch strings.ToLower(ext) {
|
||||||
case ".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tiff", ".avif", ".ico", ".heic", ".jxl":
|
case ".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tiff":
|
||||||
attachmentType = attachment.TypePhoto.String()
|
attachmentType = attachment.TypePhoto.String()
|
||||||
default:
|
default:
|
||||||
attachmentType = attachment.TypeAttachment.String()
|
attachmentType = attachment.TypeAttachment.String()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
primary, err := strconv.ParseBool(r.FormValue("primary"))
|
|
||||||
if err != nil {
|
|
||||||
log.Debug().Msg("failed to parse primary from form")
|
|
||||||
primary = false
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := ctrl.routeID(r)
|
id, err := ctrl.routeID(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -108,7 +91,6 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() errchain.HandlerFunc {
|
|||||||
id,
|
id,
|
||||||
attachmentName,
|
attachmentName,
|
||||||
attachment.Type(attachmentType),
|
attachment.Type(attachmentType),
|
||||||
primary,
|
|
||||||
file,
|
file,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -122,41 +104,41 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleItemAttachmentGet godocs
|
// HandleItemAttachmentGet godocs
|
||||||
//
|
//
|
||||||
// @Summary Get Item Attachment
|
// @Summary Get Item Attachment
|
||||||
// @Tags Items Attachments
|
// @Tags Items Attachments
|
||||||
// @Produce application/octet-stream
|
// @Produce application/octet-stream
|
||||||
// @Param id path string true "Item ID"
|
// @Param id path string true "Item ID"
|
||||||
// @Param attachment_id path string true "Attachment ID"
|
// @Param attachment_id path string true "Attachment ID"
|
||||||
// @Success 200 {object} ItemAttachmentToken
|
// @Success 200 {object} ItemAttachmentToken
|
||||||
// @Router /v1/items/{id}/attachments/{attachment_id} [GET]
|
// @Router /v1/items/{id}/attachments/{attachment_id} [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleItemAttachmentGet() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemAttachmentGet() errchain.HandlerFunc {
|
||||||
return ctrl.handleItemAttachmentsHandler
|
return ctrl.handleItemAttachmentsHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleItemAttachmentDelete godocs
|
// HandleItemAttachmentDelete godocs
|
||||||
//
|
//
|
||||||
// @Summary Delete Item Attachment
|
// @Summary Delete Item Attachment
|
||||||
// @Tags Items Attachments
|
// @Tags Items Attachments
|
||||||
// @Param id path string true "Item ID"
|
// @Param id path string true "Item ID"
|
||||||
// @Param attachment_id path string true "Attachment ID"
|
// @Param attachment_id path string true "Attachment ID"
|
||||||
// @Success 204
|
// @Success 204
|
||||||
// @Router /v1/items/{id}/attachments/{attachment_id} [DELETE]
|
// @Router /v1/items/{id}/attachments/{attachment_id} [DELETE]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleItemAttachmentDelete() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemAttachmentDelete() errchain.HandlerFunc {
|
||||||
return ctrl.handleItemAttachmentsHandler
|
return ctrl.handleItemAttachmentsHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleItemAttachmentUpdate godocs
|
// HandleItemAttachmentUpdate godocs
|
||||||
//
|
//
|
||||||
// @Summary Update Item Attachment
|
// @Summary Update Item Attachment
|
||||||
// @Tags Items Attachments
|
// @Tags Items Attachments
|
||||||
// @Param id path string true "Item ID"
|
// @Param id path string true "Item ID"
|
||||||
// @Param attachment_id path string true "Attachment ID"
|
// @Param attachment_id path string true "Attachment ID"
|
||||||
// @Param payload body repo.ItemAttachmentUpdate true "Attachment Update"
|
// @Param payload body repo.ItemAttachmentUpdate true "Attachment Update"
|
||||||
// @Success 200 {object} repo.ItemOut
|
// @Success 200 {object} repo.ItemOut
|
||||||
// @Router /v1/items/{id}/attachments/{attachment_id} [PUT]
|
// @Router /v1/items/{id}/attachments/{attachment_id} [PUT]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleItemAttachmentUpdate() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleItemAttachmentUpdate() errchain.HandlerFunc {
|
||||||
return ctrl.handleItemAttachmentsHandler
|
return ctrl.handleItemAttachmentsHandler
|
||||||
}
|
}
|
||||||
@@ -175,39 +157,13 @@ func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r
|
|||||||
ctx := services.NewContext(r.Context())
|
ctx := services.NewContext(r.Context())
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
doc, err := ctrl.svc.Items.AttachmentPath(r.Context(), ctx.GID, attachmentID)
|
doc, err := ctrl.svc.Items.AttachmentPath(r.Context(), attachmentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to get attachment path")
|
log.Err(err).Msg("failed to get attachment path")
|
||||||
return validate.NewRequestError(err, http.StatusInternalServerError)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
bucket, err := blob.OpenBucket(ctx, ctrl.repo.Attachments.GetConnString())
|
http.ServeFile(w, r, doc.Path)
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("failed to open bucket")
|
|
||||||
return validate.NewRequestError(err, http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
file, err := bucket.NewReader(ctx, ctrl.repo.Attachments.GetFullPath(doc.Path), nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("failed to open file")
|
|
||||||
return validate.NewRequestError(err, http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
defer func(file *blob.Reader) {
|
|
||||||
err := file.Close()
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("failed to close file")
|
|
||||||
}
|
|
||||||
}(file)
|
|
||||||
defer func(bucket *blob.Bucket) {
|
|
||||||
err := bucket.Close()
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("failed to close bucket")
|
|
||||||
}
|
|
||||||
}(bucket)
|
|
||||||
|
|
||||||
// Set the Content-Disposition header for RFC6266 compliance
|
|
||||||
disposition := "inline; filename*=UTF-8''" + url.QueryEscape(doc.Title)
|
|
||||||
w.Header().Set("Content-Disposition", disposition)
|
|
||||||
http.ServeContent(w, r, doc.Title, doc.CreatedAt, file)
|
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
// Delete Attachment Handler
|
// Delete Attachment Handler
|
||||||
@@ -230,9 +186,9 @@ func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
attachment.ID = attachmentID
|
attachment.ID = attachmentID
|
||||||
val, err := ctrl.svc.Items.AttachmentUpdate(ctx, ctx.GID, ID, &attachment)
|
val, err := ctrl.svc.Items.AttachmentUpdate(ctx, ID, &attachment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to update attachment")
|
log.Err(err).Msg("failed to delete attachment")
|
||||||
return validate.NewRequestError(err, http.StatusInternalServerError)
|
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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, ¶ms)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = w.Write([]byte("Printed!"))
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
return labelmaker.GenerateLabel(w, ¶ms, ctrl.config)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,12 +12,12 @@ import (
|
|||||||
|
|
||||||
// HandleLabelsGetAll godoc
|
// HandleLabelsGetAll godoc
|
||||||
//
|
//
|
||||||
// @Summary Get All Labels
|
// @Summary Get All Labels
|
||||||
// @Tags Labels
|
// @Tags Labels
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} []repo.LabelOut
|
// @Success 200 {object} []repo.LabelOut
|
||||||
// @Router /v1/labels [GET]
|
// @Router /v1/labels [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleLabelsGetAll() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleLabelsGetAll() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request) ([]repo.LabelSummary, error) {
|
fn := func(r *http.Request) ([]repo.LabelSummary, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
@@ -29,13 +29,13 @@ func (ctrl *V1Controller) HandleLabelsGetAll() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleLabelsCreate godoc
|
// HandleLabelsCreate godoc
|
||||||
//
|
//
|
||||||
// @Summary Create Label
|
// @Summary Create Label
|
||||||
// @Tags Labels
|
// @Tags Labels
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param payload body repo.LabelCreate true "Label Data"
|
// @Param payload body repo.LabelCreate true "Label Data"
|
||||||
// @Success 200 {object} repo.LabelSummary
|
// @Success 200 {object} repo.LabelSummary
|
||||||
// @Router /v1/labels [POST]
|
// @Router /v1/labels [POST]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleLabelsCreate() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleLabelsCreate() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request, data repo.LabelCreate) (repo.LabelOut, error) {
|
fn := func(r *http.Request, data repo.LabelCreate) (repo.LabelOut, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
@@ -47,13 +47,13 @@ func (ctrl *V1Controller) HandleLabelsCreate() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleLabelDelete godocs
|
// HandleLabelDelete godocs
|
||||||
//
|
//
|
||||||
// @Summary Delete Label
|
// @Summary Delete Label
|
||||||
// @Tags Labels
|
// @Tags Labels
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "Label ID"
|
// @Param id path string true "Label ID"
|
||||||
// @Success 204
|
// @Success 204
|
||||||
// @Router /v1/labels/{id} [DELETE]
|
// @Router /v1/labels/{id} [DELETE]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleLabelDelete() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleLabelDelete() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request, ID uuid.UUID) (any, error) {
|
fn := func(r *http.Request, ID uuid.UUID) (any, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
@@ -66,13 +66,13 @@ func (ctrl *V1Controller) HandleLabelDelete() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleLabelGet godocs
|
// HandleLabelGet godocs
|
||||||
//
|
//
|
||||||
// @Summary Get Label
|
// @Summary Get Label
|
||||||
// @Tags Labels
|
// @Tags Labels
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "Label ID"
|
// @Param id path string true "Label ID"
|
||||||
// @Success 200 {object} repo.LabelOut
|
// @Success 200 {object} repo.LabelOut
|
||||||
// @Router /v1/labels/{id} [GET]
|
// @Router /v1/labels/{id} [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleLabelGet() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleLabelGet() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request, ID uuid.UUID) (repo.LabelOut, error) {
|
fn := func(r *http.Request, ID uuid.UUID) (repo.LabelOut, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
@@ -84,13 +84,13 @@ func (ctrl *V1Controller) HandleLabelGet() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleLabelUpdate godocs
|
// HandleLabelUpdate godocs
|
||||||
//
|
//
|
||||||
// @Summary Update Label
|
// @Summary Update Label
|
||||||
// @Tags Labels
|
// @Tags Labels
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "Label ID"
|
// @Param id path string true "Label ID"
|
||||||
// @Success 200 {object} repo.LabelOut
|
// @Success 200 {object} repo.LabelOut
|
||||||
// @Router /v1/labels/{id} [PUT]
|
// @Router /v1/labels/{id} [PUT]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleLabelUpdate() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleLabelUpdate() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request, ID uuid.UUID, data repo.LabelUpdate) (repo.LabelOut, error) {
|
fn := func(r *http.Request, ID uuid.UUID, data repo.LabelUpdate) (repo.LabelOut, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ import (
|
|||||||
|
|
||||||
// HandleLocationTreeQuery godoc
|
// HandleLocationTreeQuery godoc
|
||||||
//
|
//
|
||||||
// @Summary Get Locations Tree
|
// @Summary Get Locations Tree
|
||||||
// @Tags Locations
|
// @Tags Locations
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param withItems query bool false "include items in response tree"
|
// @Param withItems query bool false "include items in response tree"
|
||||||
// @Success 200 {object} []repo.TreeItem
|
// @Success 200 {object} []repo.TreeItem
|
||||||
// @Router /v1/locations/tree [GET]
|
// @Router /v1/locations/tree [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleLocationTreeQuery() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleLocationTreeQuery() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request, query repo.TreeQuery) ([]repo.TreeItem, error) {
|
fn := func(r *http.Request, query repo.TreeQuery) ([]repo.TreeItem, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
@@ -32,13 +32,13 @@ func (ctrl *V1Controller) HandleLocationTreeQuery() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleLocationGetAll godoc
|
// HandleLocationGetAll godoc
|
||||||
//
|
//
|
||||||
// @Summary Get All Locations
|
// @Summary Get All Locations
|
||||||
// @Tags Locations
|
// @Tags Locations
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param filterChildren query bool false "Filter locations with parents"
|
// @Param filterChildren query bool false "Filter locations with parents"
|
||||||
// @Success 200 {object} []repo.LocationOutCount
|
// @Success 200 {object} []repo.LocationOutCount
|
||||||
// @Router /v1/locations [GET]
|
// @Router /v1/locations [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleLocationGetAll() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleLocationGetAll() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request, q repo.LocationQuery) ([]repo.LocationOutCount, error) {
|
fn := func(r *http.Request, q repo.LocationQuery) ([]repo.LocationOutCount, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
@@ -50,13 +50,13 @@ func (ctrl *V1Controller) HandleLocationGetAll() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleLocationCreate godoc
|
// HandleLocationCreate godoc
|
||||||
//
|
//
|
||||||
// @Summary Create Location
|
// @Summary Create Location
|
||||||
// @Tags Locations
|
// @Tags Locations
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param payload body repo.LocationCreate true "Location Data"
|
// @Param payload body repo.LocationCreate true "Location Data"
|
||||||
// @Success 200 {object} repo.LocationSummary
|
// @Success 200 {object} repo.LocationSummary
|
||||||
// @Router /v1/locations [POST]
|
// @Router /v1/locations [POST]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleLocationCreate() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleLocationCreate() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request, createData repo.LocationCreate) (repo.LocationOut, error) {
|
fn := func(r *http.Request, createData repo.LocationCreate) (repo.LocationOut, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
@@ -68,13 +68,13 @@ func (ctrl *V1Controller) HandleLocationCreate() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleLocationDelete godoc
|
// HandleLocationDelete godoc
|
||||||
//
|
//
|
||||||
// @Summary Delete Location
|
// @Summary Delete Location
|
||||||
// @Tags Locations
|
// @Tags Locations
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "Location ID"
|
// @Param id path string true "Location ID"
|
||||||
// @Success 204
|
// @Success 204
|
||||||
// @Router /v1/locations/{id} [DELETE]
|
// @Router /v1/locations/{id} [DELETE]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleLocationDelete() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleLocationDelete() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request, ID uuid.UUID) (any, error) {
|
fn := func(r *http.Request, ID uuid.UUID) (any, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
@@ -85,25 +85,20 @@ func (ctrl *V1Controller) HandleLocationDelete() errchain.HandlerFunc {
|
|||||||
return adapters.CommandID("id", fn, http.StatusNoContent)
|
return adapters.CommandID("id", fn, http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctrl *V1Controller) GetLocationWithPrice(auth context.Context, gid uuid.UUID, id uuid.UUID) (repo.LocationOut, error) {
|
func (ctrl *V1Controller) GetLocationWithPrice(auth context.Context, GID uuid.UUID, ID uuid.UUID) (repo.LocationOut, error) {
|
||||||
var location, err = ctrl.repo.Locations.GetOneByGroup(auth, gid, id)
|
var location, err = ctrl.repo.Locations.GetOneByGroup(auth, GID, ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return repo.LocationOut{}, err
|
return repo.LocationOut{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add direct child items price
|
// Add direct child items price
|
||||||
totalPrice := new(big.Int)
|
totalPrice := new(big.Int)
|
||||||
items, err := ctrl.repo.Items.QueryByGroup(auth, gid, repo.ItemQuery{LocationIDs: []uuid.UUID{id}})
|
items, err := ctrl.repo.Items.QueryByGroup(auth, GID, repo.ItemQuery{LocationIDs: []uuid.UUID{ID}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return repo.LocationOut{}, err
|
return repo.LocationOut{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, item := range items.Items {
|
for _, item := range items.Items {
|
||||||
// Skip items with a non-zero SoldTime
|
|
||||||
if !item.SoldTime.IsZero() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert item.Quantity to float64 for multiplication
|
// Convert item.Quantity to float64 for multiplication
|
||||||
quantity := float64(item.Quantity)
|
quantity := float64(item.Quantity)
|
||||||
itemTotal := big.NewInt(int64(item.PurchasePrice * quantity * 100))
|
itemTotal := big.NewInt(int64(item.PurchasePrice * quantity * 100))
|
||||||
@@ -117,7 +112,7 @@ func (ctrl *V1Controller) GetLocationWithPrice(auth context.Context, gid uuid.UU
|
|||||||
// Add price from child locations
|
// Add price from child locations
|
||||||
for _, childLocation := range location.Children {
|
for _, childLocation := range location.Children {
|
||||||
var childLocationWithPrice repo.LocationOut
|
var childLocationWithPrice repo.LocationOut
|
||||||
childLocationWithPrice, err = ctrl.GetLocationWithPrice(auth, gid, childLocation.ID)
|
childLocationWithPrice, err = ctrl.GetLocationWithPrice(auth, GID, childLocation.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return repo.LocationOut{}, err
|
return repo.LocationOut{}, err
|
||||||
}
|
}
|
||||||
@@ -129,13 +124,13 @@ func (ctrl *V1Controller) GetLocationWithPrice(auth context.Context, gid uuid.UU
|
|||||||
|
|
||||||
// HandleLocationGet godoc
|
// HandleLocationGet godoc
|
||||||
//
|
//
|
||||||
// @Summary Get Location
|
// @Summary Get Location
|
||||||
// @Tags Locations
|
// @Tags Locations
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "Location ID"
|
// @Param id path string true "Location ID"
|
||||||
// @Success 200 {object} repo.LocationOut
|
// @Success 200 {object} repo.LocationOut
|
||||||
// @Router /v1/locations/{id} [GET]
|
// @Router /v1/locations/{id} [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleLocationGet() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleLocationGet() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request, ID uuid.UUID) (repo.LocationOut, error) {
|
fn := func(r *http.Request, ID uuid.UUID) (repo.LocationOut, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
@@ -149,14 +144,14 @@ func (ctrl *V1Controller) HandleLocationGet() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleLocationUpdate godoc
|
// HandleLocationUpdate godoc
|
||||||
//
|
//
|
||||||
// @Summary Update Location
|
// @Summary Update Location
|
||||||
// @Tags Locations
|
// @Tags Locations
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "Location ID"
|
// @Param id path string true "Location ID"
|
||||||
// @Param payload body repo.LocationUpdate true "Location Data"
|
// @Param payload body repo.LocationUpdate true "Location Data"
|
||||||
// @Success 200 {object} repo.LocationOut
|
// @Success 200 {object} repo.LocationOut
|
||||||
// @Router /v1/locations/{id} [PUT]
|
// @Router /v1/locations/{id} [PUT]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleLocationUpdate() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleLocationUpdate() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request, ID uuid.UUID, body repo.LocationUpdate) (repo.LocationOut, error) {
|
fn := func(r *http.Request, ID uuid.UUID, body repo.LocationUpdate) (repo.LocationOut, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
|
|||||||
@@ -12,18 +12,16 @@ import (
|
|||||||
|
|
||||||
// HandleMaintenanceLogGet godoc
|
// HandleMaintenanceLogGet godoc
|
||||||
//
|
//
|
||||||
// @Summary Get Maintenance Log
|
// @Summary Get Maintenance Log
|
||||||
// @Tags Item Maintenance
|
// @Tags Maintenance
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "Item ID"
|
// @Success 200 {object} repo.MaintenanceLog
|
||||||
// @Param filters query repo.MaintenanceFilters false "which maintenance to retrieve"
|
// @Router /v1/items/{id}/maintenance [GET]
|
||||||
// @Success 200 {array} repo.MaintenanceEntryWithDetails[]
|
// @Security Bearer
|
||||||
// @Router /v1/items/{id}/maintenance [GET]
|
|
||||||
// @Security Bearer
|
|
||||||
func (ctrl *V1Controller) HandleMaintenanceLogGet() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleMaintenanceLogGet() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request, ID uuid.UUID, filters repo.MaintenanceFilters) ([]repo.MaintenanceEntryWithDetails, error) {
|
fn := func(r *http.Request, ID uuid.UUID, q repo.MaintenanceLogQuery) (repo.MaintenanceLog, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
return ctrl.repo.MaintEntry.GetMaintenanceByItemID(auth, auth.GID, ID, filters)
|
return ctrl.repo.MaintEntry.GetLog(auth, auth.GID, ID, q)
|
||||||
}
|
}
|
||||||
|
|
||||||
return adapters.QueryID("id", fn, http.StatusOK)
|
return adapters.QueryID("id", fn, http.StatusOK)
|
||||||
@@ -31,14 +29,13 @@ func (ctrl *V1Controller) HandleMaintenanceLogGet() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleMaintenanceEntryCreate godoc
|
// HandleMaintenanceEntryCreate godoc
|
||||||
//
|
//
|
||||||
// @Summary Create Maintenance Entry
|
// @Summary Create Maintenance Entry
|
||||||
// @Tags Item Maintenance
|
// @Tags Maintenance
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "Item ID"
|
// @Param payload body repo.MaintenanceEntryCreate true "Entry Data"
|
||||||
// @Param payload body repo.MaintenanceEntryCreate true "Entry Data"
|
// @Success 201 {object} repo.MaintenanceEntry
|
||||||
// @Success 201 {object} repo.MaintenanceEntry
|
// @Router /v1/items/{id}/maintenance [POST]
|
||||||
// @Router /v1/items/{id}/maintenance [POST]
|
// @Security Bearer
|
||||||
// @Security Bearer
|
|
||||||
func (ctrl *V1Controller) HandleMaintenanceEntryCreate() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleMaintenanceEntryCreate() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request, itemID uuid.UUID, body repo.MaintenanceEntryCreate) (repo.MaintenanceEntry, error) {
|
fn := func(r *http.Request, itemID uuid.UUID, body repo.MaintenanceEntryCreate) (repo.MaintenanceEntry, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
@@ -47,3 +44,39 @@ func (ctrl *V1Controller) HandleMaintenanceEntryCreate() errchain.HandlerFunc {
|
|||||||
|
|
||||||
return adapters.ActionID("id", fn, http.StatusCreated)
|
return adapters.ActionID("id", fn, http.StatusCreated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleMaintenanceEntryDelete godoc
|
||||||
|
//
|
||||||
|
// @Summary Delete Maintenance Entry
|
||||||
|
// @Tags Maintenance
|
||||||
|
// @Produce json
|
||||||
|
// @Success 204
|
||||||
|
// @Router /v1/items/{id}/maintenance/{entry_id} [DELETE]
|
||||||
|
// @Security Bearer
|
||||||
|
func (ctrl *V1Controller) HandleMaintenanceEntryDelete() errchain.HandlerFunc {
|
||||||
|
fn := func(r *http.Request, entryID uuid.UUID) (any, error) {
|
||||||
|
auth := services.NewContext(r.Context())
|
||||||
|
err := ctrl.repo.MaintEntry.Delete(auth, entryID)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return adapters.CommandID("entry_id", fn, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleMaintenanceEntryUpdate godoc
|
||||||
|
//
|
||||||
|
// @Summary Update Maintenance Entry
|
||||||
|
// @Tags Maintenance
|
||||||
|
// @Produce json
|
||||||
|
// @Param payload body repo.MaintenanceEntryUpdate true "Entry Data"
|
||||||
|
// @Success 200 {object} repo.MaintenanceEntry
|
||||||
|
// @Router /v1/items/{id}/maintenance/{entry_id} [PUT]
|
||||||
|
// @Security Bearer
|
||||||
|
func (ctrl *V1Controller) HandleMaintenanceEntryUpdate() errchain.HandlerFunc {
|
||||||
|
fn := func(r *http.Request, entryID uuid.UUID, body repo.MaintenanceEntryUpdate) (repo.MaintenanceEntry, error) {
|
||||||
|
auth := services.NewContext(r.Context())
|
||||||
|
return ctrl.repo.MaintEntry.Update(auth, entryID, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return adapters.ActionID("entry_id", fn, http.StatusOK)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
package v1
|
|
||||||
|
|
||||||
import (
|
|
||||||
"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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
func (ctrl *V1Controller) HandleMaintenanceGetAll() errchain.HandlerFunc {
|
|
||||||
fn := func(r *http.Request, filters repo.MaintenanceFilters) ([]repo.MaintenanceEntryWithDetails, error) {
|
|
||||||
auth := services.NewContext(r.Context())
|
|
||||||
return ctrl.repo.MaintEntry.GetAllMaintenance(auth, auth.GID, filters)
|
|
||||||
}
|
|
||||||
|
|
||||||
return adapters.Query(fn, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleMaintenanceEntryUpdate godoc
|
|
||||||
//
|
|
||||||
// @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]
|
|
||||||
// @Security Bearer
|
|
||||||
func (ctrl *V1Controller) HandleMaintenanceEntryUpdate() errchain.HandlerFunc {
|
|
||||||
fn := func(r *http.Request, entryID uuid.UUID, body repo.MaintenanceEntryUpdate) (repo.MaintenanceEntry, error) {
|
|
||||||
auth := services.NewContext(r.Context())
|
|
||||||
return ctrl.repo.MaintEntry.Update(auth, entryID, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
return adapters.ActionID("id", fn, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleMaintenanceEntryDelete godoc
|
|
||||||
//
|
|
||||||
// @Summary Delete Maintenance Entry
|
|
||||||
// @Tags Maintenance
|
|
||||||
// @Produce json
|
|
||||||
// @Param id path string true "Maintenance ID"
|
|
||||||
// @Success 204
|
|
||||||
// @Router /v1/maintenance/{id} [DELETE]
|
|
||||||
// @Security Bearer
|
|
||||||
func (ctrl *V1Controller) HandleMaintenanceEntryDelete() errchain.HandlerFunc {
|
|
||||||
fn := func(r *http.Request, entryID uuid.UUID) (any, error) {
|
|
||||||
auth := services.NewContext(r.Context())
|
|
||||||
err := ctrl.repo.MaintEntry.Delete(auth, entryID)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return adapters.CommandID("id", fn, http.StatusNoContent)
|
|
||||||
}
|
|
||||||
@@ -13,12 +13,12 @@ import (
|
|||||||
|
|
||||||
// HandleGetUserNotifiers godoc
|
// HandleGetUserNotifiers godoc
|
||||||
//
|
//
|
||||||
// @Summary Get Notifiers
|
// @Summary Get Notifiers
|
||||||
// @Tags Notifiers
|
// @Tags Notifiers
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} []repo.NotifierOut
|
// @Success 200 {object} []repo.NotifierOut
|
||||||
// @Router /v1/notifiers [GET]
|
// @Router /v1/notifiers [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleGetUserNotifiers() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleGetUserNotifiers() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request, _ struct{}) ([]repo.NotifierOut, error) {
|
fn := func(r *http.Request, _ struct{}) ([]repo.NotifierOut, error) {
|
||||||
user := services.UseUserCtx(r.Context())
|
user := services.UseUserCtx(r.Context())
|
||||||
@@ -30,13 +30,13 @@ func (ctrl *V1Controller) HandleGetUserNotifiers() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleCreateNotifier godoc
|
// HandleCreateNotifier godoc
|
||||||
//
|
//
|
||||||
// @Summary Create Notifier
|
// @Summary Create Notifier
|
||||||
// @Tags Notifiers
|
// @Tags Notifiers
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param payload body repo.NotifierCreate true "Notifier Data"
|
// @Param payload body repo.NotifierCreate true "Notifier Data"
|
||||||
// @Success 200 {object} repo.NotifierOut
|
// @Success 200 {object} repo.NotifierOut
|
||||||
// @Router /v1/notifiers [POST]
|
// @Router /v1/notifiers [POST]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleCreateNotifier() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleCreateNotifier() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request, in repo.NotifierCreate) (repo.NotifierOut, error) {
|
fn := func(r *http.Request, in repo.NotifierCreate) (repo.NotifierOut, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
@@ -48,12 +48,12 @@ func (ctrl *V1Controller) HandleCreateNotifier() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleDeleteNotifier godocs
|
// HandleDeleteNotifier godocs
|
||||||
//
|
//
|
||||||
// @Summary Delete a Notifier
|
// @Summary Delete a Notifier
|
||||||
// @Tags Notifiers
|
// @Tags Notifiers
|
||||||
// @Param id path string true "Notifier ID"
|
// @Param id path string true "Notifier ID"
|
||||||
// @Success 204
|
// @Success 204
|
||||||
// @Router /v1/notifiers/{id} [DELETE]
|
// @Router /v1/notifiers/{id} [DELETE]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleDeleteNotifier() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleDeleteNotifier() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request, ID uuid.UUID) (any, error) {
|
fn := func(r *http.Request, ID uuid.UUID) (any, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
@@ -65,13 +65,13 @@ func (ctrl *V1Controller) HandleDeleteNotifier() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleUpdateNotifier godocs
|
// HandleUpdateNotifier godocs
|
||||||
//
|
//
|
||||||
// @Summary Update Notifier
|
// @Summary Update Notifier
|
||||||
// @Tags Notifiers
|
// @Tags Notifiers
|
||||||
// @Param id path string true "Notifier ID"
|
// @Param id path string true "Notifier ID"
|
||||||
// @Param payload body repo.NotifierUpdate true "Notifier Data"
|
// @Param payload body repo.NotifierUpdate true "Notifier Data"
|
||||||
// @Success 200 {object} repo.NotifierOut
|
// @Success 200 {object} repo.NotifierOut
|
||||||
// @Router /v1/notifiers/{id} [PUT]
|
// @Router /v1/notifiers/{id} [PUT]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleUpdateNotifier() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleUpdateNotifier() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request, ID uuid.UUID, in repo.NotifierUpdate) (repo.NotifierOut, error) {
|
fn := func(r *http.Request, ID uuid.UUID, in repo.NotifierUpdate) (repo.NotifierOut, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
@@ -83,13 +83,14 @@ func (ctrl *V1Controller) HandleUpdateNotifier() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandlerNotifierTest godoc
|
// HandlerNotifierTest godoc
|
||||||
//
|
//
|
||||||
// @Summary Test Notifier
|
// @Summary Test Notifier
|
||||||
// @Tags Notifiers
|
// @Tags Notifiers
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param url query string true "URL"
|
// @Param id path string true "Notifier ID"
|
||||||
// @Success 204
|
// @Param url query string true "URL"
|
||||||
// @Router /v1/notifiers/test [POST]
|
// @Success 204
|
||||||
// @Security Bearer
|
// @Router /v1/notifiers/test [POST]
|
||||||
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandlerNotifierTest() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandlerNotifierTest() errchain.HandlerFunc {
|
||||||
type body struct {
|
type body struct {
|
||||||
URL string `json:"url" validate:"required"`
|
URL string `json:"url" validate:"required"`
|
||||||
|
|||||||
@@ -1,332 +0,0 @@
|
|||||||
package v1
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/hay-kot/httpkit/errchain"
|
|
||||||
"github.com/hay-kot/httpkit/server"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
|
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
|
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/web/adapters"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UPCITEMDBResponse struct {
|
|
||||||
Code string `json:"code"`
|
|
||||||
Total int `json:"total"`
|
|
||||||
Offset int `json:"offset"`
|
|
||||||
Items []struct {
|
|
||||||
Ean string `json:"ean"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Upc string `json:"upc"`
|
|
||||||
Brand string `json:"brand"`
|
|
||||||
Model string `json:"model"`
|
|
||||||
Color string `json:"color"`
|
|
||||||
Size string `json:"size"`
|
|
||||||
Dimension string `json:"dimension"`
|
|
||||||
Weight string `json:"weight"`
|
|
||||||
Category string `json:"category"`
|
|
||||||
LowestRecordedPrice float64 `json:"lowest_recorded_price"`
|
|
||||||
HighestRecordedPrice float64 `json:"highest_recorded_price"`
|
|
||||||
Images []string `json:"images"`
|
|
||||||
Offers []struct {
|
|
||||||
Merchant string `json:"merchant"`
|
|
||||||
Domain string `json:"domain"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Currency string `json:"currency"`
|
|
||||||
ListPrice string `json:"list_price"`
|
|
||||||
Price float64 `json:"price"`
|
|
||||||
Shipping string `json:"shipping"`
|
|
||||||
Condition string `json:"condition"`
|
|
||||||
Availability string `json:"availability"`
|
|
||||||
Link string `json:"link"`
|
|
||||||
UpdatedT int `json:"updated_t"`
|
|
||||||
} `json:"offers"`
|
|
||||||
Asin string `json:"asin"`
|
|
||||||
Elid string `json:"elid"`
|
|
||||||
} `json:"items"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BARCODESPIDER_COMResponse struct {
|
|
||||||
ItemResponse struct {
|
|
||||||
Code int `json:"code"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
} `json:"item_response"`
|
|
||||||
ItemAttributes struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
Upc string `json:"upc"`
|
|
||||||
Ean string `json:"ean"`
|
|
||||||
ParentCategory string `json:"parent_category"`
|
|
||||||
Category string `json:"category"`
|
|
||||||
Brand string `json:"brand"`
|
|
||||||
Model string `json:"model"`
|
|
||||||
Mpn string `json:"mpn"`
|
|
||||||
Manufacturer string `json:"manufacturer"`
|
|
||||||
Publisher string `json:"publisher"`
|
|
||||||
Asin string `json:"asin"`
|
|
||||||
Color string `json:"color"`
|
|
||||||
Size string `json:"size"`
|
|
||||||
Weight string `json:"weight"`
|
|
||||||
Image string `json:"image"`
|
|
||||||
IsAdult string `json:"is_adult"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
} `json:"item_attributes"`
|
|
||||||
Stores []struct {
|
|
||||||
StoreName string `json:"store_name"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Image string `json:"image"`
|
|
||||||
Price string `json:"price"`
|
|
||||||
Currency string `json:"currency"`
|
|
||||||
Link string `json:"link"`
|
|
||||||
Updated string `json:"updated"`
|
|
||||||
} `json:"Stores"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleGenerateQRCode godoc
|
|
||||||
//
|
|
||||||
// @Summary Search EAN from Barcode
|
|
||||||
// @Tags Items
|
|
||||||
// @Produce json
|
|
||||||
// @Param data query string false "barcode to be searched"
|
|
||||||
// @Success 200 {object} []repo.BarcodeProduct
|
|
||||||
// @Router /v1/products/search-from-barcode [GET]
|
|
||||||
// @Security Bearer
|
|
||||||
func (ctrl *V1Controller) HandleProductSearchFromBarcode(conf config.BarcodeAPIConf) errchain.HandlerFunc {
|
|
||||||
type query struct {
|
|
||||||
// 80 characters is the longest non-2D barcode length (GS1-128)
|
|
||||||
EAN string `schema:"productEAN" validate:"required,max=80"`
|
|
||||||
}
|
|
||||||
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
q, err := adapters.DecodeQuery[query](r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const TIMEOUT_SEC = 10
|
|
||||||
|
|
||||||
log.Info().Msg("Processing barcode lookup request on: " + q.EAN)
|
|
||||||
|
|
||||||
// Search on UPCITEMDB
|
|
||||||
var products []repo.BarcodeProduct
|
|
||||||
|
|
||||||
// www.ean-search.org/: not free
|
|
||||||
|
|
||||||
// Example code: dewalt 5035048748428
|
|
||||||
|
|
||||||
upcitemdb := func(iEan string) ([]repo.BarcodeProduct, error) {
|
|
||||||
client := &http.Client{Timeout: TIMEOUT_SEC * time.Second}
|
|
||||||
resp, err := client.Get("https://api.upcitemdb.com/prod/trial/lookup?upc=" + iEan)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
err = errors.Join(err, resp.Body.Close())
|
|
||||||
}()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("API returned status code: %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We Read the response body on the line below.
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Uncomment the following string for debug
|
|
||||||
// sb := string(body)
|
|
||||||
// log.Debug().Msg("Response: " + sb)
|
|
||||||
|
|
||||||
var result UPCITEMDBResponse
|
|
||||||
if err := json.Unmarshal(body, &result); err != nil { // Parse []byte to go struct pointer
|
|
||||||
log.Error().Msg("Can not unmarshal JSON")
|
|
||||||
}
|
|
||||||
|
|
||||||
var res []repo.BarcodeProduct
|
|
||||||
|
|
||||||
for _, it := range result.Items {
|
|
||||||
var p repo.BarcodeProduct
|
|
||||||
p.SearchEngineName = "upcitemdb.com"
|
|
||||||
p.Barcode = iEan
|
|
||||||
|
|
||||||
p.Item.Description = it.Description
|
|
||||||
p.Item.Name = it.Title
|
|
||||||
p.Manufacturer = it.Brand
|
|
||||||
p.ModelNumber = it.Model
|
|
||||||
if len(it.Images) != 0 {
|
|
||||||
p.ImageURL = it.Images[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
res = append(res, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ps, err := upcitemdb(q.EAN)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Msg("Can not retrieve product from upcitemdb.com" + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Barcode spider implementation
|
|
||||||
barcodespider := func(tokenAPI string, iEan string) ([]repo.BarcodeProduct, error) {
|
|
||||||
if len(tokenAPI) == 0 {
|
|
||||||
return nil, errors.New("no api token configured for barcodespider. " +
|
|
||||||
"Please define the api token in environment variable HBOX_BARCODE_TOKEN_BARCODESPIDER")
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest(
|
|
||||||
"GET", "https://api.barcodespider.com/v1/lookup?upc="+iEan, nil)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Add("token", tokenAPI)
|
|
||||||
|
|
||||||
client := &http.Client{Timeout: TIMEOUT_SEC * time.Second}
|
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// defer the call to Body.Close(). We also check the error code, and merge
|
|
||||||
// it with the other error in this code to avoid error overiding.
|
|
||||||
defer func() {
|
|
||||||
err = errors.Join(err, resp.Body.Close())
|
|
||||||
}()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("barcodespider API returned status code: %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We Read the response body on the line below.
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Uncomment the following string for debug
|
|
||||||
// sb := string(body)
|
|
||||||
// log.Debug().Msg("Response: " + sb)
|
|
||||||
|
|
||||||
var result BARCODESPIDER_COMResponse
|
|
||||||
if err := json.Unmarshal(body, &result); err != nil { // Parse []byte to go struct pointer
|
|
||||||
log.Error().Msg("Can not unmarshal JSON")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: check 200 code on HTTP response.
|
|
||||||
var p repo.BarcodeProduct
|
|
||||||
p.Barcode = iEan
|
|
||||||
p.SearchEngineName = "barcodespider.com"
|
|
||||||
p.Item.Name = result.ItemAttributes.Title
|
|
||||||
p.Item.Description = result.ItemAttributes.Description
|
|
||||||
p.Manufacturer = result.ItemAttributes.Brand
|
|
||||||
p.ModelNumber = result.ItemAttributes.Model
|
|
||||||
p.ImageURL = result.ItemAttributes.Image
|
|
||||||
|
|
||||||
var res []repo.BarcodeProduct
|
|
||||||
res = append(res, p)
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ps2, err := barcodespider(conf.TokenBarcodespider, q.EAN)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Msg("Can not retrieve product from barcodespider.com: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge everything.
|
|
||||||
products = append(products, ps...)
|
|
||||||
|
|
||||||
products = append(products, ps2...)
|
|
||||||
|
|
||||||
// Retrieve images if possible
|
|
||||||
for i := range products {
|
|
||||||
p := &products[i]
|
|
||||||
|
|
||||||
if len(p.ImageURL) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate URL is HTTPS
|
|
||||||
u, err := url.Parse(p.ImageURL)
|
|
||||||
if err != nil || u.Scheme != "https" {
|
|
||||||
log.Warn().Msg("Skipping non-HTTPS image URL: " + p.ImageURL)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &http.Client{Timeout: TIMEOUT_SEC * time.Second}
|
|
||||||
res, err := client.Get(p.ImageURL)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn().Msg("Cannot fetch image for URL: " + p.ImageURL + ": " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
err = errors.Join(err, res.Body.Close())
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Validate response
|
|
||||||
if res.StatusCode != http.StatusOK {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check content type
|
|
||||||
contentType := res.Header.Get("Content-Type")
|
|
||||||
if !strings.HasPrefix(contentType, "image/") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Limit image size to 8MB
|
|
||||||
limitedReader := io.LimitReader(res.Body, 8*1024*1024)
|
|
||||||
|
|
||||||
// Read data of image
|
|
||||||
bytes, err := io.ReadAll(limitedReader)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn().Msg(err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to Base64
|
|
||||||
var base64Encoding string
|
|
||||||
|
|
||||||
// Determine the content type of the image file
|
|
||||||
mimeType := http.DetectContentType(bytes)
|
|
||||||
|
|
||||||
// Prepend the appropriate URI scheme header depending
|
|
||||||
// on the MIME type
|
|
||||||
switch mimeType {
|
|
||||||
case "image/jpeg":
|
|
||||||
base64Encoding += "data:image/jpeg;base64,"
|
|
||||||
case "image/png":
|
|
||||||
base64Encoding += "data:image/png;base64,"
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append the base64 encoded output
|
|
||||||
base64Encoding += base64.StdEncoding.EncodeToString(bytes)
|
|
||||||
|
|
||||||
p.ImageBase64 = base64Encoding
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(products) != 0 {
|
|
||||||
return server.JSON(w, http.StatusOK, products)
|
|
||||||
}
|
|
||||||
|
|
||||||
return server.JSON(w, http.StatusNoContent, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,13 +20,13 @@ var qrcodeLogo []byte
|
|||||||
|
|
||||||
// HandleGenerateQRCode godoc
|
// HandleGenerateQRCode godoc
|
||||||
//
|
//
|
||||||
// @Summary Create QR Code
|
// @Summary Create QR Code
|
||||||
// @Tags Items
|
// @Tags Items
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param data query string false "data to be encoded into qrcode"
|
// @Param data query string false "data to be encoded into qrcode"
|
||||||
// @Success 200 {string} string "image/jpeg"
|
// @Success 200 {string} string "image/jpeg"
|
||||||
// @Router /v1/qrcode [GET]
|
// @Router /v1/qrcode [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleGenerateQRCode() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleGenerateQRCode() errchain.HandlerFunc {
|
||||||
type query struct {
|
type query struct {
|
||||||
// 4,296 characters is the maximum length of a QR code
|
// 4,296 characters is the maximum length of a QR code
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import (
|
|||||||
|
|
||||||
// HandleBillOfMaterialsExport godoc
|
// HandleBillOfMaterialsExport godoc
|
||||||
//
|
//
|
||||||
// @Summary Export Bill of Materials
|
// @Summary Export Bill of Materials
|
||||||
// @Tags Reporting
|
// @Tags Reporting
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {string} string "text/csv"
|
// @Success 200 {string} string "text/csv"
|
||||||
// @Router /v1/reporting/bill-of-materials [GET]
|
// @Router /v1/reporting/bill-of-materials [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleBillOfMaterialsExport() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleBillOfMaterialsExport() errchain.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) error {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
actor := services.UseUserCtx(r.Context())
|
actor := services.UseUserCtx(r.Context())
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ import (
|
|||||||
|
|
||||||
// HandleGroupStatisticsLocations godoc
|
// HandleGroupStatisticsLocations godoc
|
||||||
//
|
//
|
||||||
// @Summary Get Location Statistics
|
// @Summary Get Location Statistics
|
||||||
// @Tags Statistics
|
// @Tags Statistics
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} []repo.TotalsByOrganizer
|
// @Success 200 {object} []repo.TotalsByOrganizer
|
||||||
// @Router /v1/groups/statistics/locations [GET]
|
// @Router /v1/groups/statistics/locations [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleGroupStatisticsLocations() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleGroupStatisticsLocations() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request) ([]repo.TotalsByOrganizer, error) {
|
fn := func(r *http.Request) ([]repo.TotalsByOrganizer, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
@@ -31,12 +31,12 @@ func (ctrl *V1Controller) HandleGroupStatisticsLocations() errchain.HandlerFunc
|
|||||||
|
|
||||||
// HandleGroupStatisticsLabels godoc
|
// HandleGroupStatisticsLabels godoc
|
||||||
//
|
//
|
||||||
// @Summary Get Label Statistics
|
// @Summary Get Label Statistics
|
||||||
// @Tags Statistics
|
// @Tags Statistics
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} []repo.TotalsByOrganizer
|
// @Success 200 {object} []repo.TotalsByOrganizer
|
||||||
// @Router /v1/groups/statistics/labels [GET]
|
// @Router /v1/groups/statistics/labels [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleGroupStatisticsLabels() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleGroupStatisticsLabels() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request) ([]repo.TotalsByOrganizer, error) {
|
fn := func(r *http.Request) ([]repo.TotalsByOrganizer, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
@@ -48,12 +48,12 @@ func (ctrl *V1Controller) HandleGroupStatisticsLabels() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleGroupStatistics godoc
|
// HandleGroupStatistics godoc
|
||||||
//
|
//
|
||||||
// @Summary Get Group Statistics
|
// @Summary Get Group Statistics
|
||||||
// @Tags Statistics
|
// @Tags Statistics
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} repo.GroupStatistics
|
// @Success 200 {object} repo.GroupStatistics
|
||||||
// @Router /v1/groups/statistics [GET]
|
// @Router /v1/groups/statistics [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleGroupStatistics() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleGroupStatistics() errchain.HandlerFunc {
|
||||||
fn := func(r *http.Request) (repo.GroupStatistics, error) {
|
fn := func(r *http.Request) (repo.GroupStatistics, error) {
|
||||||
auth := services.NewContext(r.Context())
|
auth := services.NewContext(r.Context())
|
||||||
@@ -65,14 +65,14 @@ func (ctrl *V1Controller) HandleGroupStatistics() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleGroupStatisticsPriceOverTime godoc
|
// HandleGroupStatisticsPriceOverTime godoc
|
||||||
//
|
//
|
||||||
// @Summary Get Purchase Price Statistics
|
// @Summary Get Purchase Price Statistics
|
||||||
// @Tags Statistics
|
// @Tags Statistics
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} repo.ValueOverTime
|
// @Success 200 {object} repo.ValueOverTime
|
||||||
// @Param start query string false "start date"
|
// @Param start query string false "start date"
|
||||||
// @Param end query string false "end date"
|
// @Param end query string false "end date"
|
||||||
// @Router /v1/groups/statistics/purchase-price [GET]
|
// @Router /v1/groups/statistics/purchase-price [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleGroupStatisticsPriceOverTime() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleGroupStatisticsPriceOverTime() errchain.HandlerFunc {
|
||||||
parseDate := func(datestr string, defaultDate time.Time) (time.Time, error) {
|
parseDate := func(datestr string, defaultDate time.Time) (time.Time, error) {
|
||||||
if datestr == "" {
|
if datestr == "" {
|
||||||
|
|||||||
@@ -15,20 +15,14 @@ import (
|
|||||||
|
|
||||||
// HandleUserRegistration godoc
|
// HandleUserRegistration godoc
|
||||||
//
|
//
|
||||||
// @Summary Register New User
|
// @Summary Register New User
|
||||||
// @Tags User
|
// @Tags User
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param payload body services.UserRegistration true "User Data"
|
// @Param payload body services.UserRegistration true "User Data"
|
||||||
// @Success 204
|
// @Success 204
|
||||||
// @Failure 403 {string} string "Local login is not enabled"
|
// @Router /v1/users/register [Post]
|
||||||
// @Router /v1/users/register [Post]
|
|
||||||
func (ctrl *V1Controller) HandleUserRegistration() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleUserRegistration() errchain.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) error {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
// Forbidden if local login is not enabled
|
|
||||||
if !ctrl.config.Options.AllowLocalLogin {
|
|
||||||
return validate.NewRequestError(fmt.Errorf("local login is not enabled"), http.StatusForbidden)
|
|
||||||
}
|
|
||||||
|
|
||||||
regData := services.UserRegistration{}
|
regData := services.UserRegistration{}
|
||||||
|
|
||||||
if err := server.Decode(r, ®Data); err != nil {
|
if err := server.Decode(r, ®Data); err != nil {
|
||||||
@@ -52,12 +46,12 @@ func (ctrl *V1Controller) HandleUserRegistration() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleUserSelf godoc
|
// HandleUserSelf godoc
|
||||||
//
|
//
|
||||||
// @Summary Get User Self
|
// @Summary Get User Self
|
||||||
// @Tags User
|
// @Tags User
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} Wrapped{item=repo.UserOut}
|
// @Success 200 {object} Wrapped{item=repo.UserOut}
|
||||||
// @Router /v1/users/self [GET]
|
// @Router /v1/users/self [GET]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleUserSelf() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleUserSelf() errchain.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) error {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
token := services.UseTokenCtx(r.Context())
|
token := services.UseTokenCtx(r.Context())
|
||||||
@@ -73,13 +67,13 @@ func (ctrl *V1Controller) HandleUserSelf() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleUserSelfUpdate godoc
|
// HandleUserSelfUpdate godoc
|
||||||
//
|
//
|
||||||
// @Summary Update Account
|
// @Summary Update Account
|
||||||
// @Tags User
|
// @Tags User
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param payload body repo.UserUpdate true "User Data"
|
// @Param payload body repo.UserUpdate true "User Data"
|
||||||
// @Success 200 {object} Wrapped{item=repo.UserUpdate}
|
// @Success 200 {object} Wrapped{item=repo.UserUpdate}
|
||||||
// @Router /v1/users/self [PUT]
|
// @Router /v1/users/self [PUT]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleUserSelfUpdate() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleUserSelfUpdate() errchain.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) error {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
updateData := repo.UserUpdate{}
|
updateData := repo.UserUpdate{}
|
||||||
@@ -100,12 +94,12 @@ func (ctrl *V1Controller) HandleUserSelfUpdate() errchain.HandlerFunc {
|
|||||||
|
|
||||||
// HandleUserSelfDelete godoc
|
// HandleUserSelfDelete godoc
|
||||||
//
|
//
|
||||||
// @Summary Delete Account
|
// @Summary Delete Account
|
||||||
// @Tags User
|
// @Tags User
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 204
|
// @Success 204
|
||||||
// @Router /v1/users/self [DELETE]
|
// @Router /v1/users/self [DELETE]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleUserSelfDelete() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleUserSelfDelete() errchain.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) error {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
if ctrl.isDemo {
|
if ctrl.isDemo {
|
||||||
@@ -130,12 +124,12 @@ type (
|
|||||||
|
|
||||||
// HandleUserSelfChangePassword godoc
|
// HandleUserSelfChangePassword godoc
|
||||||
//
|
//
|
||||||
// @Summary Change Password
|
// @Summary Change Password
|
||||||
// @Tags User
|
// @Tags User
|
||||||
// @Success 204
|
// @Success 204
|
||||||
// @Param payload body ChangePassword true "Password Payload"
|
// @Param payload body ChangePassword true "Password Payload"
|
||||||
// @Router /v1/users/change-password [PUT]
|
// @Router /v1/users/change-password [PUT]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleUserSelfChangePassword() errchain.HandlerFunc {
|
func (ctrl *V1Controller) HandleUserSelfChangePassword() errchain.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) error {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
if ctrl.isDemo {
|
if ctrl.isDemo {
|
||||||
|
|||||||
@@ -18,10 +18,7 @@ func (a *app) setupLogger() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
level, err := zerolog.ParseLevel(a.conf.Log.Level)
|
level, err := zerolog.ParseLevel(a.conf.Log.Level)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
log.Error().Err(err).Str("level", a.conf.Log.Level).Msg("invalid log level, falling back to info")
|
|
||||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
|
||||||
} else {
|
|
||||||
zerolog.SetGlobalLevel(level)
|
zerolog.SetGlobalLevel(level)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
atlas "ariga.io/atlas/sql/migrate"
|
||||||
|
"entgo.io/ent/dialect/sql/schema"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
"github.com/pressly/goose/v3"
|
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/analytics"
|
|
||||||
|
|
||||||
"github.com/hay-kot/httpkit/errchain"
|
"github.com/hay-kot/httpkit/errchain"
|
||||||
"github.com/hay-kot/httpkit/graceful"
|
"github.com/hay-kot/httpkit/graceful"
|
||||||
@@ -26,20 +27,8 @@ import (
|
|||||||
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
|
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
|
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/web/mid"
|
"github.com/sysadminsmedia/homebox/backend/internal/web/mid"
|
||||||
"go.balki.me/anyhttp"
|
|
||||||
|
|
||||||
_ "github.com/lib/pq"
|
|
||||||
_ "github.com/sysadminsmedia/homebox/backend/internal/data/migrations/postgres"
|
|
||||||
_ "github.com/sysadminsmedia/homebox/backend/internal/data/migrations/sqlite3"
|
|
||||||
_ "github.com/sysadminsmedia/homebox/backend/pkgs/cgofreesqlite"
|
_ "github.com/sysadminsmedia/homebox/backend/pkgs/cgofreesqlite"
|
||||||
|
|
||||||
_ "gocloud.dev/pubsub/awssnssqs"
|
|
||||||
_ "gocloud.dev/pubsub/azuresb"
|
|
||||||
_ "gocloud.dev/pubsub/gcppubsub"
|
|
||||||
_ "gocloud.dev/pubsub/kafkapubsub"
|
|
||||||
_ "gocloud.dev/pubsub/mempubsub"
|
|
||||||
_ "gocloud.dev/pubsub/natspubsub"
|
|
||||||
_ "gocloud.dev/pubsub/rabbitpubsub"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -57,33 +46,15 @@ func build() string {
|
|||||||
return fmt.Sprintf("%s, commit %s, built at %s", version, short, buildTime)
|
return fmt.Sprintf("%s, commit %s, built at %s", version, short, buildTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
func validatePostgresSSLMode(sslMode string) bool {
|
// @title Homebox API
|
||||||
validModes := map[string]bool{
|
// @version 1.0
|
||||||
"": true,
|
// @description Track, Manage, and Organize your Things.
|
||||||
"disable": true,
|
// @contact.name Don't
|
||||||
"allow": true,
|
// @BasePath /api
|
||||||
"prefer": true,
|
// @securityDefinitions.apikey Bearer
|
||||||
"require": true,
|
// @in header
|
||||||
"verify-ca": true,
|
// @name Authorization
|
||||||
"verify-full": true,
|
// @description "Type 'Bearer TOKEN' to correctly set the API Key"
|
||||||
}
|
|
||||||
return validModes[strings.ToLower(strings.TrimSpace(sslMode))]
|
|
||||||
}
|
|
||||||
|
|
||||||
// @title Homebox API
|
|
||||||
// @version 1.0
|
|
||||||
// @description Track, Manage, and Organize your Things.
|
|
||||||
// @contact.name Homebox Team
|
|
||||||
// @contact.url https://discord.homebox.software
|
|
||||||
// @host demo.homebox.software
|
|
||||||
// @schemes https http
|
|
||||||
// @BasePath /api
|
|
||||||
// @securityDefinitions.apikey Bearer
|
|
||||||
// @in header
|
|
||||||
// @name Authorization
|
|
||||||
// @description "Type 'Bearer TOKEN' to correctly set the API Key"
|
|
||||||
// @externalDocs.url https://homebox.software/en/api
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
|
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
|
||||||
|
|
||||||
@@ -103,75 +74,90 @@ func run(cfg *config.Config) error {
|
|||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Initialize Database & Repos
|
// Initialize Database & Repos
|
||||||
err := setupStorageDir(cfg)
|
|
||||||
|
err := os.MkdirAll(cfg.Storage.Data, 0o755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
log.Fatal().Err(err).Msg("failed to create data directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.ToLower(cfg.Database.Driver) == "postgres" {
|
c, err := ent.Open("sqlite3", cfg.Storage.SqliteURL)
|
||||||
if !validatePostgresSSLMode(cfg.Database.SslMode) {
|
|
||||||
log.Error().Str("sslmode", cfg.Database.SslMode).Msg("invalid sslmode")
|
|
||||||
return fmt.Errorf("invalid sslmode: %s", cfg.Database.SslMode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
databaseURL, err := setupDatabaseURL(cfg)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
log.Fatal().
|
||||||
}
|
|
||||||
|
|
||||||
c, err := ent.Open(strings.ToLower(cfg.Database.Driver), databaseURL)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
|
||||||
Err(err).
|
Err(err).
|
||||||
Str("driver", strings.ToLower(cfg.Database.Driver)).
|
Str("driver", "sqlite").
|
||||||
Str("host", cfg.Database.Host).
|
Str("url", cfg.Storage.SqliteURL).
|
||||||
Str("port", cfg.Database.Port).
|
Msg("failed opening connection to sqlite")
|
||||||
Str("database", cfg.Database.Database).
|
}
|
||||||
Msg("failed opening connection to {driver} database at {host}:{port}/{database}")
|
defer func(c *ent.Client) {
|
||||||
return fmt.Errorf("failed opening connection to %s database at %s:%s/%s: %w",
|
err := c.Close()
|
||||||
strings.ToLower(cfg.Database.Driver),
|
if err != nil {
|
||||||
cfg.Database.Host,
|
log.Fatal().Err(err).Msg("failed to close database connection")
|
||||||
cfg.Database.Port,
|
}
|
||||||
cfg.Database.Database,
|
}(c)
|
||||||
err,
|
|
||||||
)
|
temp := filepath.Join(os.TempDir(), "migrations")
|
||||||
|
|
||||||
|
err = migrations.Write(temp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
migrationsFs, err := migrations.Migrations(strings.ToLower(cfg.Database.Driver))
|
dir, err := atlas.NewLocalDir(temp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get migrations for %s: %w", strings.ToLower(cfg.Database.Driver), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
goose.SetBaseFS(migrationsFs)
|
|
||||||
err = goose.SetDialect(strings.ToLower(cfg.Database.Driver))
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Str("driver", cfg.Database.Driver).Msg("unsupported database driver")
|
|
||||||
return fmt.Errorf("unsupported database driver: %s", cfg.Database.Driver)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = goose.Up(c.Sql(), strings.ToLower(cfg.Database.Driver))
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("failed to migrate database")
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
collectFuncs, err := loadCurrencies(cfg)
|
options := []schema.MigrateOption{
|
||||||
|
schema.WithDir(dir),
|
||||||
|
schema.WithDropColumn(true),
|
||||||
|
schema.WithDropIndex(true),
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.Schema.Create(context.Background(), options...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Fatal().
|
||||||
|
Err(err).
|
||||||
|
Str("driver", "sqlite").
|
||||||
|
Str("url", cfg.Storage.SqliteURL).
|
||||||
|
Msg("failed creating schema resources")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.RemoveAll(temp)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("failed to remove temporary directory for database migrations")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
collectFuncs := []currencies.CollectorFunc{
|
||||||
|
currencies.CollectDefaults(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Options.CurrencyConfig != "" {
|
||||||
|
log.Info().
|
||||||
|
Str("path", cfg.Options.CurrencyConfig).
|
||||||
|
Msg("loading currency config file")
|
||||||
|
|
||||||
|
content, err := os.ReadFile(cfg.Options.CurrencyConfig)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().
|
||||||
|
Err(err).
|
||||||
|
Str("path", cfg.Options.CurrencyConfig).
|
||||||
|
Msg("failed to read currency config file")
|
||||||
|
}
|
||||||
|
|
||||||
|
collectFuncs = append(collectFuncs, currencies.CollectJSON(bytes.NewReader(content)))
|
||||||
|
}
|
||||||
|
|
||||||
currencies, err := currencies.CollectionCurrencies(collectFuncs...)
|
currencies, err := currencies.CollectionCurrencies(collectFuncs...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Fatal().
|
||||||
Err(err).
|
Err(err).
|
||||||
Msg("failed to collect currencies")
|
Msg("failed to collect currencies")
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app.bus = eventbus.New()
|
app.bus = eventbus.New()
|
||||||
app.db = c
|
app.db = c
|
||||||
app.repos = repo.New(c, app.bus, cfg.Storage, cfg.Database.PubSubConnString, cfg.Thumbnail)
|
app.repos = repo.New(c, app.bus, cfg.Storage.Data)
|
||||||
app.services = services.New(
|
app.services = services.New(
|
||||||
app.repos,
|
app.repos,
|
||||||
services.WithAutoIncrementAssetID(cfg.Options.AutoIncrementAssetID),
|
services.WithAutoIncrementAssetID(cfg.Options.AutoIncrementAssetID),
|
||||||
@@ -212,51 +198,74 @@ func run(cfg *config.Config) error {
|
|||||||
_ = httpserver.Shutdown(context.Background())
|
_ = httpserver.Shutdown(context.Background())
|
||||||
}()
|
}()
|
||||||
|
|
||||||
listener, addrType, addrCfg, err := anyhttp.GetListener(cfg.Web.Host)
|
|
||||||
if err == nil {
|
|
||||||
switch addrType {
|
|
||||||
case anyhttp.SystemdFD:
|
|
||||||
sysdCfg := addrCfg.(*anyhttp.SysdConfig)
|
|
||||||
if sysdCfg.IdleTimeout != nil {
|
|
||||||
log.Error().Msg("idle timeout not yet supported. Please remove and try again")
|
|
||||||
return errors.New("idle timeout not yet supported. Please remove and try again")
|
|
||||||
}
|
|
||||||
fallthrough
|
|
||||||
case anyhttp.UnixSocket:
|
|
||||||
log.Info().Msgf("Server is running on %s", cfg.Web.Host)
|
|
||||||
return httpserver.Serve(listener)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Debug().Msgf("anyhttp error: %v", err)
|
|
||||||
}
|
|
||||||
log.Info().Msgf("Server is running on %s:%s", cfg.Web.Host, cfg.Web.Port)
|
log.Info().Msgf("Server is running on %s:%s", cfg.Web.Host, cfg.Web.Port)
|
||||||
return httpserver.ListenAndServe()
|
return httpserver.ListenAndServe()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
// Start Reoccurring Tasks
|
// Start Reoccurring Tasks
|
||||||
registerRecurringTasks(app, cfg, runner)
|
|
||||||
|
|
||||||
// Send analytics if enabled at around midnight UTC
|
runner.AddFunc("eventbus", app.bus.Run)
|
||||||
if cfg.Options.AllowAnalytics {
|
|
||||||
analyticsTime := time.Second
|
runner.AddFunc("seed_database", func(ctx context.Context) error {
|
||||||
runner.AddPlugin(NewTask("send-analytics", analyticsTime, func(ctx context.Context) {
|
// TODO: Remove through external API that does setup
|
||||||
for {
|
if cfg.Demo {
|
||||||
now := time.Now().UTC()
|
log.Info().Msg("Running in demo mode, creating demo data")
|
||||||
nextMidnight := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, time.UTC)
|
app.SetupDemo()
|
||||||
dur := time.Until(nextMidnight)
|
}
|
||||||
analyticsTime = dur
|
return nil
|
||||||
select {
|
})
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
runner.AddPlugin(NewTask("purge-tokens", time.Duration(24)*time.Hour, func(ctx context.Context) {
|
||||||
case <-time.After(dur):
|
_, err := app.repos.AuthTokens.PurgeExpiredTokens(ctx)
|
||||||
log.Debug().Msg("running send analytics")
|
if err != nil {
|
||||||
err := analytics.Send(version, build())
|
log.Error().
|
||||||
if err != nil {
|
Err(err).
|
||||||
log.Error().Err(err).Msg("failed to send analytics")
|
Msg("failed to purge expired tokens")
|
||||||
}
|
}
|
||||||
}
|
}))
|
||||||
|
|
||||||
|
runner.AddPlugin(NewTask("purge-invitations", time.Duration(24)*time.Hour, func(ctx context.Context) {
|
||||||
|
_, err := app.repos.Groups.InvitationPurge(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Err(err).
|
||||||
|
Msg("failed to purge expired invitations")
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
runner.AddPlugin(NewTask("send-notifications", time.Duration(1)*time.Hour, func(ctx context.Context) {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
if now.Hour() == 8 {
|
||||||
|
fmt.Println("run notifiers")
|
||||||
|
err := app.services.BackgroundService.SendNotifiersToday(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Err(err).
|
||||||
|
Msg("failed to send notifiers")
|
||||||
}
|
}
|
||||||
}))
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
if cfg.Debug.Enabled {
|
||||||
|
runner.AddFunc("debug", func(ctx context.Context) error {
|
||||||
|
debugserver := http.Server{
|
||||||
|
Addr: fmt.Sprintf("%s:%s", cfg.Web.Host, cfg.Debug.Port),
|
||||||
|
Handler: app.debugRouter(),
|
||||||
|
ReadTimeout: cfg.Web.ReadTimeout,
|
||||||
|
WriteTimeout: cfg.Web.WriteTimeout,
|
||||||
|
IdleTimeout: cfg.Web.IdleTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
_ = debugserver.Shutdown(context.Background())
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Info().Msgf("Debug server is running on %s:%s", cfg.Web.Host, cfg.Debug.Port)
|
||||||
|
return debugserver.ListenAndServe()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return runner.Start(context.Background())
|
return runner.Start(context.Background())
|
||||||
|
|||||||
@@ -1,626 +0,0 @@
|
|||||||
package providers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/core/services"
|
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type OIDCProvider struct {
|
|
||||||
service *services.UserService
|
|
||||||
config *config.OIDCConf
|
|
||||||
options *config.Options
|
|
||||||
cookieSecure bool
|
|
||||||
provider *oidc.Provider
|
|
||||||
verifier *oidc.IDTokenVerifier
|
|
||||||
endpoint oauth2.Endpoint
|
|
||||||
}
|
|
||||||
|
|
||||||
type OIDCClaims struct {
|
|
||||||
Email string
|
|
||||||
Groups []string
|
|
||||||
Name string
|
|
||||||
Subject string
|
|
||||||
Issuer string
|
|
||||||
EmailVerified *bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewOIDCProvider(service *services.UserService, config *config.OIDCConf, options *config.Options, cookieSecure bool) (*OIDCProvider, error) {
|
|
||||||
if !config.Enabled {
|
|
||||||
return nil, fmt.Errorf("OIDC is not enabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate required configuration
|
|
||||||
if config.ClientID == "" {
|
|
||||||
return nil, fmt.Errorf("OIDC client ID is required when OIDC is enabled (set HBOX_OIDC_CLIENT_ID)")
|
|
||||||
}
|
|
||||||
if config.ClientSecret == "" {
|
|
||||||
return nil, fmt.Errorf("OIDC client secret is required when OIDC is enabled (set HBOX_OIDC_CLIENT_SECRET)")
|
|
||||||
}
|
|
||||||
if config.IssuerURL == "" {
|
|
||||||
return nil, fmt.Errorf("OIDC issuer URL is required when OIDC is enabled (set HBOX_OIDC_ISSUER_URL)")
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), config.RequestTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
provider, err := oidc.NewProvider(ctx, config.IssuerURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create OIDC provider from issuer URL: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create ID token verifier
|
|
||||||
verifier := provider.Verifier(&oidc.Config{
|
|
||||||
ClientID: config.ClientID,
|
|
||||||
})
|
|
||||||
|
|
||||||
log.Info().
|
|
||||||
Str("issuer", config.IssuerURL).
|
|
||||||
Str("client_id", config.ClientID).
|
|
||||||
Str("scope", config.Scope).
|
|
||||||
Msg("OIDC provider initialized successfully with discovery")
|
|
||||||
|
|
||||||
return &OIDCProvider{
|
|
||||||
service: service,
|
|
||||||
config: config,
|
|
||||||
options: options,
|
|
||||||
cookieSecure: cookieSecure,
|
|
||||||
provider: provider,
|
|
||||||
verifier: verifier,
|
|
||||||
endpoint: provider.Endpoint(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *OIDCProvider) Name() string {
|
|
||||||
return "oidc"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authenticate implements the AuthProvider interface but is not used for OIDC
|
|
||||||
// OIDC uses dedicated endpoints: GET /api/v1/users/login/oidc and GET /api/v1/users/login/oidc/callback
|
|
||||||
func (p *OIDCProvider) Authenticate(w http.ResponseWriter, r *http.Request) (services.UserAuthTokenDetail, error) {
|
|
||||||
_ = w
|
|
||||||
_ = r
|
|
||||||
return services.UserAuthTokenDetail{}, fmt.Errorf("OIDC authentication uses dedicated endpoints: /api/v1/users/login/oidc")
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthenticateWithBaseURL is the main authentication method that requires baseURL
|
|
||||||
// Called from handleCallback after state, nonce, and PKCE verification
|
|
||||||
func (p *OIDCProvider) AuthenticateWithBaseURL(baseURL, expectedNonce, pkceVerifier string, _ http.ResponseWriter, r *http.Request) (services.UserAuthTokenDetail, error) {
|
|
||||||
code := r.URL.Query().Get("code")
|
|
||||||
if code == "" {
|
|
||||||
return services.UserAuthTokenDetail{}, fmt.Errorf("missing authorization code")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get OAuth2 config for this request
|
|
||||||
oauth2Config := p.getOAuth2Config(baseURL)
|
|
||||||
|
|
||||||
// Exchange code for token with timeout and PKCE verifier
|
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), p.config.RequestTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
token, err := oauth2Config.Exchange(ctx, code, oauth2.SetAuthURLParam("code_verifier", pkceVerifier))
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("failed to exchange OIDC code for token")
|
|
||||||
return services.UserAuthTokenDetail{}, fmt.Errorf("failed to exchange code for token")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract ID token
|
|
||||||
idToken, ok := token.Extra("id_token").(string)
|
|
||||||
if !ok {
|
|
||||||
return services.UserAuthTokenDetail{}, fmt.Errorf("no id_token in response")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse and validate the ID token using the library's verifier with timeout
|
|
||||||
verifyCtx, verifyCancel := context.WithTimeout(r.Context(), p.config.RequestTimeout)
|
|
||||||
defer verifyCancel()
|
|
||||||
|
|
||||||
idTokenStruct, err := p.verifier.Verify(verifyCtx, idToken)
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("failed to verify ID token")
|
|
||||||
return services.UserAuthTokenDetail{}, fmt.Errorf("failed to verify ID token")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract claims from the verified token using dynamic parsing
|
|
||||||
var rawClaims map[string]interface{}
|
|
||||||
if err := idTokenStruct.Claims(&rawClaims); err != nil {
|
|
||||||
log.Err(err).Msg("failed to extract claims from ID token")
|
|
||||||
return services.UserAuthTokenDetail{}, fmt.Errorf("failed to extract claims from ID token")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to retrieve UserInfo claims; use them as primary, fallback to ID token claims.
|
|
||||||
finalClaims := rawClaims
|
|
||||||
userInfoCtx, uiCancel := context.WithTimeout(r.Context(), p.config.RequestTimeout)
|
|
||||||
defer uiCancel()
|
|
||||||
|
|
||||||
userInfo, uiErr := p.provider.UserInfo(userInfoCtx, oauth2.StaticTokenSource(token))
|
|
||||||
if uiErr != nil {
|
|
||||||
log.Debug().Err(uiErr).Msg("OIDC UserInfo fetch failed; falling back to ID token claims")
|
|
||||||
} else {
|
|
||||||
var uiClaims map[string]interface{}
|
|
||||||
if err := userInfo.Claims(&uiClaims); err != nil {
|
|
||||||
log.Debug().Err(err).Msg("failed to decode UserInfo claims; falling back to ID token claims")
|
|
||||||
} else {
|
|
||||||
finalClaims = mergeOIDCClaims(uiClaims, rawClaims) // UserInfo first, then fill gaps from ID token
|
|
||||||
log.Debug().Int("userinfo_claims", len(uiClaims)).Int("id_token_claims", len(rawClaims)).Int("merged_claims", len(finalClaims)).Msg("merged UserInfo and ID token claims")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse claims using configurable claim names (after merge)
|
|
||||||
claims, err := p.parseOIDCClaims(finalClaims)
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("failed to parse OIDC claims")
|
|
||||||
return services.UserAuthTokenDetail{}, fmt.Errorf("failed to parse OIDC claims: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify nonce claim matches expected value (nonce only from ID token; ensure preserved in merged map)
|
|
||||||
tokenNonce, exists := finalClaims["nonce"]
|
|
||||||
if !exists {
|
|
||||||
log.Warn().Msg("nonce claim missing from ID token - possible replay attack")
|
|
||||||
return services.UserAuthTokenDetail{}, fmt.Errorf("nonce claim missing from token")
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenNonceStr, ok := tokenNonce.(string)
|
|
||||||
if !ok {
|
|
||||||
log.Warn().Msg("nonce claim is not a string in ID token")
|
|
||||||
return services.UserAuthTokenDetail{}, fmt.Errorf("invalid nonce claim format")
|
|
||||||
}
|
|
||||||
|
|
||||||
if tokenNonceStr != expectedNonce {
|
|
||||||
log.Warn().Str("received", tokenNonceStr).Str("expected", expectedNonce).Msg("OIDC nonce mismatch - possible replay attack")
|
|
||||||
return services.UserAuthTokenDetail{}, fmt.Errorf("nonce parameter mismatch")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if email is verified
|
|
||||||
if p.config.VerifyEmail {
|
|
||||||
if claims.EmailVerified == nil {
|
|
||||||
return services.UserAuthTokenDetail{}, fmt.Errorf("email verification status not found in token claims")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !*claims.EmailVerified {
|
|
||||||
return services.UserAuthTokenDetail{}, fmt.Errorf("email not verified")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check group authorization if configured
|
|
||||||
if p.config.AllowedGroups != "" {
|
|
||||||
allowedGroups := strings.Split(p.config.AllowedGroups, ",")
|
|
||||||
if !p.hasAllowedGroup(claims.Groups, allowedGroups) {
|
|
||||||
log.Warn().
|
|
||||||
Strs("user_groups", claims.Groups).
|
|
||||||
Strs("allowed_groups", allowedGroups).
|
|
||||||
Str("user", claims.Email).
|
|
||||||
Msg("user not in allowed groups")
|
|
||||||
return services.UserAuthTokenDetail{}, fmt.Errorf("user not in allowed groups")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine username from claims
|
|
||||||
email := claims.Email
|
|
||||||
if email == "" {
|
|
||||||
return services.UserAuthTokenDetail{}, fmt.Errorf("no email found in token claims")
|
|
||||||
}
|
|
||||||
if claims.Subject == "" {
|
|
||||||
return services.UserAuthTokenDetail{}, fmt.Errorf("no subject (sub) claim present")
|
|
||||||
}
|
|
||||||
if claims.Issuer == "" {
|
|
||||||
claims.Issuer = p.config.IssuerURL // fallback to configured issuer, though spec requires 'iss'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the dedicated OIDC login method (issuer + subject identity)
|
|
||||||
sessionToken, err := p.service.LoginOIDC(r.Context(), claims.Issuer, claims.Subject, email, claims.Name)
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Str("email", email).Str("issuer", claims.Issuer).Str("subject", claims.Subject).Msg("OIDC login failed")
|
|
||||||
return services.UserAuthTokenDetail{}, fmt.Errorf("OIDC login failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return sessionToken, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *OIDCProvider) parseOIDCClaims(rawClaims map[string]interface{}) (OIDCClaims, error) {
|
|
||||||
var claims OIDCClaims
|
|
||||||
|
|
||||||
// Parse email claim
|
|
||||||
key := p.config.EmailClaim
|
|
||||||
if key == "" {
|
|
||||||
key = "email"
|
|
||||||
}
|
|
||||||
if emailValue, exists := rawClaims[key]; exists {
|
|
||||||
if email, ok := emailValue.(string); ok {
|
|
||||||
claims.Email = email
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse email_verified claim
|
|
||||||
if p.config.VerifyEmail {
|
|
||||||
key = p.config.EmailVerifiedClaim
|
|
||||||
if key == "" {
|
|
||||||
key = "email_verified"
|
|
||||||
}
|
|
||||||
if emailVerifiedValue, exists := rawClaims[key]; exists {
|
|
||||||
switch v := emailVerifiedValue.(type) {
|
|
||||||
case bool:
|
|
||||||
claims.EmailVerified = &v
|
|
||||||
case string:
|
|
||||||
if b, err := strconv.ParseBool(v); err == nil {
|
|
||||||
claims.EmailVerified = &b
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse name claim
|
|
||||||
key = p.config.NameClaim
|
|
||||||
if key == "" {
|
|
||||||
key = "name"
|
|
||||||
}
|
|
||||||
if nameValue, exists := rawClaims[key]; exists {
|
|
||||||
if name, ok := nameValue.(string); ok {
|
|
||||||
claims.Name = name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse groups claim
|
|
||||||
key = p.config.GroupClaim
|
|
||||||
if key == "" {
|
|
||||||
key = "groups"
|
|
||||||
}
|
|
||||||
if groupsValue, exists := rawClaims[key]; exists {
|
|
||||||
switch groups := groupsValue.(type) {
|
|
||||||
case []interface{}:
|
|
||||||
for _, group := range groups {
|
|
||||||
if groupStr, ok := group.(string); ok {
|
|
||||||
claims.Groups = append(claims.Groups, groupStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case []string:
|
|
||||||
claims.Groups = groups
|
|
||||||
case string:
|
|
||||||
// Single group as string
|
|
||||||
claims.Groups = []string{groups}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse subject claim (always "sub")
|
|
||||||
if subValue, exists := rawClaims["sub"]; exists {
|
|
||||||
if subject, ok := subValue.(string); ok {
|
|
||||||
claims.Subject = subject
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Parse issuer claim ("iss")
|
|
||||||
if issValue, exists := rawClaims["iss"]; exists {
|
|
||||||
if iss, ok := issValue.(string); ok {
|
|
||||||
claims.Issuer = iss
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return claims, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *OIDCProvider) hasAllowedGroup(userGroups, allowedGroups []string) bool {
|
|
||||||
if len(allowedGroups) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
allowedGroupsMap := make(map[string]bool)
|
|
||||||
for _, group := range allowedGroups {
|
|
||||||
allowedGroupsMap[strings.TrimSpace(group)] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, userGroup := range userGroups {
|
|
||||||
if allowedGroupsMap[userGroup] {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *OIDCProvider) GetAuthURL(baseURL, state, nonce, pkceVerifier string) string {
|
|
||||||
oauth2Config := p.getOAuth2Config(baseURL)
|
|
||||||
pkceChallenge := generatePKCEChallenge(pkceVerifier)
|
|
||||||
return oauth2Config.AuthCodeURL(state,
|
|
||||||
oidc.Nonce(nonce),
|
|
||||||
oauth2.SetAuthURLParam("code_challenge", pkceChallenge),
|
|
||||||
oauth2.SetAuthURLParam("code_challenge_method", "S256"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *OIDCProvider) getOAuth2Config(baseURL string) oauth2.Config {
|
|
||||||
// Construct full redirect URL with dedicated callback endpoint
|
|
||||||
redirectURL, err := url.JoinPath(baseURL, "/api/v1/users/login/oidc/callback")
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("failed to construct redirect URL")
|
|
||||||
return oauth2.Config{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return oauth2.Config{
|
|
||||||
ClientID: p.config.ClientID,
|
|
||||||
ClientSecret: p.config.ClientSecret,
|
|
||||||
RedirectURL: redirectURL,
|
|
||||||
Endpoint: p.endpoint,
|
|
||||||
Scopes: strings.Fields(p.config.Scope),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// initiateOIDCFlow handles the initial OIDC authentication request by redirecting to the provider
|
|
||||||
func (p *OIDCProvider) initiateOIDCFlow(w http.ResponseWriter, r *http.Request) (services.UserAuthTokenDetail, error) {
|
|
||||||
// Generate state parameter for CSRF protection
|
|
||||||
state, err := generateSecureToken()
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("failed to generate OIDC state parameter")
|
|
||||||
return services.UserAuthTokenDetail{}, fmt.Errorf("internal server error")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate nonce parameter for replay attack protection
|
|
||||||
nonce, err := generateSecureToken()
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("failed to generate OIDC nonce parameter")
|
|
||||||
return services.UserAuthTokenDetail{}, fmt.Errorf("internal server error")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate PKCE verifier for code interception protection
|
|
||||||
pkceVerifier, err := generatePKCEVerifier()
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("failed to generate OIDC PKCE verifier")
|
|
||||||
return services.UserAuthTokenDetail{}, fmt.Errorf("internal server error")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get base URL from request
|
|
||||||
baseURL := p.getBaseURL(r)
|
|
||||||
u, _ := url.Parse(baseURL)
|
|
||||||
domain := u.Hostname()
|
|
||||||
if domain == "" {
|
|
||||||
domain = noPort(r.Host)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store state in session cookie for validation
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: "oidc_state",
|
|
||||||
Value: state,
|
|
||||||
Expires: time.Now().Add(p.config.StateExpiry),
|
|
||||||
Domain: domain,
|
|
||||||
Secure: p.isSecure(r),
|
|
||||||
HttpOnly: true,
|
|
||||||
Path: "/",
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Store nonce in session cookie for validation
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: "oidc_nonce",
|
|
||||||
Value: nonce,
|
|
||||||
Expires: time.Now().Add(p.config.StateExpiry),
|
|
||||||
Domain: domain,
|
|
||||||
Secure: p.isSecure(r),
|
|
||||||
HttpOnly: true,
|
|
||||||
Path: "/",
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Store PKCE verifier in session cookie for token exchange
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: "oidc_pkce_verifier",
|
|
||||||
Value: pkceVerifier,
|
|
||||||
Expires: time.Now().Add(p.config.StateExpiry),
|
|
||||||
Domain: domain,
|
|
||||||
Secure: p.isSecure(r),
|
|
||||||
HttpOnly: true,
|
|
||||||
Path: "/",
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Generate auth URL and redirect
|
|
||||||
authURL := p.GetAuthURL(baseURL, state, nonce, pkceVerifier)
|
|
||||||
http.Redirect(w, r, authURL, http.StatusFound)
|
|
||||||
|
|
||||||
// Return empty token since this is a redirect response
|
|
||||||
return services.UserAuthTokenDetail{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleCallback processes the OAuth2 callback from the OIDC provider
|
|
||||||
func (p *OIDCProvider) handleCallback(w http.ResponseWriter, r *http.Request) (services.UserAuthTokenDetail, error) {
|
|
||||||
// Helper to clear state cookie using computed domain
|
|
||||||
baseURL := p.getBaseURL(r)
|
|
||||||
u, _ := url.Parse(baseURL)
|
|
||||||
domain := u.Hostname()
|
|
||||||
if domain == "" {
|
|
||||||
domain = noPort(r.Host)
|
|
||||||
}
|
|
||||||
clearCookies := func() {
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: "oidc_state",
|
|
||||||
Value: "",
|
|
||||||
Expires: time.Unix(0, 0),
|
|
||||||
Domain: domain,
|
|
||||||
MaxAge: -1,
|
|
||||||
Secure: p.isSecure(r),
|
|
||||||
HttpOnly: true,
|
|
||||||
Path: "/",
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
})
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: "oidc_nonce",
|
|
||||||
Value: "",
|
|
||||||
Expires: time.Unix(0, 0),
|
|
||||||
Domain: domain,
|
|
||||||
MaxAge: -1,
|
|
||||||
Secure: p.isSecure(r),
|
|
||||||
HttpOnly: true,
|
|
||||||
Path: "/",
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
})
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: "oidc_pkce_verifier",
|
|
||||||
Value: "",
|
|
||||||
Expires: time.Unix(0, 0),
|
|
||||||
Domain: domain,
|
|
||||||
MaxAge: -1,
|
|
||||||
Secure: p.isSecure(r),
|
|
||||||
HttpOnly: true,
|
|
||||||
Path: "/",
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for OAuth error responses first
|
|
||||||
if errCode := r.URL.Query().Get("error"); errCode != "" {
|
|
||||||
errDesc := r.URL.Query().Get("error_description")
|
|
||||||
log.Warn().Str("error", errCode).Str("description", errDesc).Msg("OIDC provider returned error")
|
|
||||||
clearCookies()
|
|
||||||
return services.UserAuthTokenDetail{}, fmt.Errorf("OIDC provider error: %s - %s", errCode, errDesc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify state parameter
|
|
||||||
stateCookie, err := r.Cookie("oidc_state")
|
|
||||||
if err != nil {
|
|
||||||
log.Warn().Err(err).Msg("OIDC state cookie not found - possible CSRF attack or expired session")
|
|
||||||
clearCookies()
|
|
||||||
return services.UserAuthTokenDetail{}, fmt.Errorf("state cookie not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
stateParam := r.URL.Query().Get("state")
|
|
||||||
if stateParam == "" {
|
|
||||||
log.Warn().Msg("OIDC state parameter missing from callback")
|
|
||||||
clearCookies()
|
|
||||||
return services.UserAuthTokenDetail{}, fmt.Errorf("state parameter missing")
|
|
||||||
}
|
|
||||||
|
|
||||||
if stateParam != stateCookie.Value {
|
|
||||||
log.Warn().Str("received", stateParam).Str("expected", stateCookie.Value).Msg("OIDC state mismatch - possible CSRF attack")
|
|
||||||
clearCookies()
|
|
||||||
return services.UserAuthTokenDetail{}, fmt.Errorf("state parameter mismatch")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify nonce parameter
|
|
||||||
nonceCookie, err := r.Cookie("oidc_nonce")
|
|
||||||
if err != nil {
|
|
||||||
log.Warn().Err(err).Msg("OIDC nonce cookie not found - possible replay attack or expired session")
|
|
||||||
clearCookies()
|
|
||||||
return services.UserAuthTokenDetail{}, fmt.Errorf("nonce cookie not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify PKCE verifier parameter
|
|
||||||
pkceCookie, err := r.Cookie("oidc_pkce_verifier")
|
|
||||||
if err != nil {
|
|
||||||
log.Warn().Err(err).Msg("OIDC PKCE verifier cookie not found - possible code interception attack or expired session")
|
|
||||||
clearCookies()
|
|
||||||
return services.UserAuthTokenDetail{}, fmt.Errorf("PKCE verifier cookie not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear cookies before proceeding to token verification
|
|
||||||
clearCookies()
|
|
||||||
|
|
||||||
// Use the existing callback logic but return the token instead of redirecting
|
|
||||||
return p.AuthenticateWithBaseURL(baseURL, nonceCookie.Value, pkceCookie.Value, w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper functions
|
|
||||||
func generateSecureToken() (string, error) {
|
|
||||||
// Generate 32 bytes of cryptographically secure random data
|
|
||||||
bytes := make([]byte, 32)
|
|
||||||
_, err := rand.Read(bytes)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to generate secure random token: %w", err)
|
|
||||||
}
|
|
||||||
// Use URL-safe base64 encoding without padding for clean URLs
|
|
||||||
return base64.RawURLEncoding.EncodeToString(bytes), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// generatePKCEVerifier generates a cryptographically secure code verifier for PKCE
|
|
||||||
func generatePKCEVerifier() (string, error) {
|
|
||||||
// PKCE verifier must be 43-128 characters, we'll use 43 for efficiency
|
|
||||||
// 32 bytes = 43 characters when base64url encoded without padding
|
|
||||||
bytes := make([]byte, 32)
|
|
||||||
_, err := rand.Read(bytes)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to generate PKCE verifier: %w", err)
|
|
||||||
}
|
|
||||||
return base64.RawURLEncoding.EncodeToString(bytes), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// generatePKCEChallenge generates a code challenge from a verifier using S256 method
|
|
||||||
func generatePKCEChallenge(verifier string) string {
|
|
||||||
hash := sha256.Sum256([]byte(verifier))
|
|
||||||
return base64.RawURLEncoding.EncodeToString(hash[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
func noPort(host string) string {
|
|
||||||
return strings.Split(host, ":")[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *OIDCProvider) getBaseURL(r *http.Request) string {
|
|
||||||
scheme := "http"
|
|
||||||
if r.TLS != nil {
|
|
||||||
scheme = "https"
|
|
||||||
} else if p.options.TrustProxy && r.Header.Get("X-Forwarded-Proto") == "https" {
|
|
||||||
scheme = "https"
|
|
||||||
}
|
|
||||||
|
|
||||||
host := r.Host
|
|
||||||
if p.options.Hostname != "" {
|
|
||||||
host = p.options.Hostname
|
|
||||||
} else if p.options.TrustProxy {
|
|
||||||
if xfHost := r.Header.Get("X-Forwarded-Host"); xfHost != "" {
|
|
||||||
host = xfHost
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return scheme + "://" + host
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *OIDCProvider) isSecure(r *http.Request) bool {
|
|
||||||
_ = r
|
|
||||||
return p.cookieSecure
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitiateOIDCFlow starts the OIDC authentication flow by redirecting to the provider
|
|
||||||
func (p *OIDCProvider) InitiateOIDCFlow(w http.ResponseWriter, r *http.Request) (services.UserAuthTokenDetail, error) {
|
|
||||||
return p.initiateOIDCFlow(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleCallback processes the OIDC callback and returns the authenticated user token
|
|
||||||
func (p *OIDCProvider) HandleCallback(w http.ResponseWriter, r *http.Request) (services.UserAuthTokenDetail, error) {
|
|
||||||
return p.handleCallback(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeOIDCClaims(primary, secondary map[string]interface{}) map[string]interface{} {
|
|
||||||
// primary has precedence; fill missing/empty values from secondary.
|
|
||||||
merged := make(map[string]interface{}, len(primary)+len(secondary))
|
|
||||||
for k, v := range primary {
|
|
||||||
merged[k] = v
|
|
||||||
}
|
|
||||||
for k, v := range secondary {
|
|
||||||
if existing, ok := merged[k]; !ok || isEmptyClaim(existing) {
|
|
||||||
merged[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return merged
|
|
||||||
}
|
|
||||||
|
|
||||||
func isEmptyClaim(v interface{}) bool {
|
|
||||||
if v == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
switch val := v.(type) {
|
|
||||||
case string:
|
|
||||||
return val == ""
|
|
||||||
case []interface{}:
|
|
||||||
return len(val) == 0
|
|
||||||
case []string:
|
|
||||||
return len(val) == 0
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/hay-kot/httpkit/graceful"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
|
|
||||||
"github.com/sysadminsmedia/homebox/backend/pkgs/utils"
|
|
||||||
"gocloud.dev/pubsub"
|
|
||||||
)
|
|
||||||
|
|
||||||
func registerRecurringTasks(app *app, cfg *config.Config, runner *graceful.Runner) {
|
|
||||||
runner.AddFunc("eventbus", app.bus.Run)
|
|
||||||
|
|
||||||
runner.AddFunc("seed_database", func(ctx context.Context) error {
|
|
||||||
if cfg.Demo {
|
|
||||||
log.Info().Msg("Running in demo mode, creating demo data")
|
|
||||||
err := app.SetupDemo()
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("failed to setup demo data")
|
|
||||||
return fmt.Errorf("failed to setup demo data: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
runner.AddPlugin(NewTask("purge-tokens", 24*time.Hour, func(ctx context.Context) {
|
|
||||||
_, err := app.repos.AuthTokens.PurgeExpiredTokens(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("failed to purge expired tokens")
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
runner.AddPlugin(NewTask("purge-invitations", 24*time.Hour, func(ctx context.Context) {
|
|
||||||
_, err := app.repos.Groups.InvitationPurge(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("failed to purge expired invitations")
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
runner.AddPlugin(NewTask("send-notifications", time.Hour, func(ctx context.Context) {
|
|
||||||
now := time.Now()
|
|
||||||
if now.Hour() == 8 {
|
|
||||||
fmt.Println("run notifiers")
|
|
||||||
err := app.services.BackgroundService.SendNotifiersToday(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("failed to send notifiers")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
if cfg.Thumbnail.Enabled {
|
|
||||||
runner.AddFunc("create-thumbnails-subscription", func(ctx context.Context) error {
|
|
||||||
pubsubString, err := utils.GenerateSubPubConn(cfg.Database.PubSubConnString, "thumbnails")
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("failed to generate pubsub connection string")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
topic, err := pubsub.OpenTopic(ctx, pubsubString)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func(topic *pubsub.Topic, ctx context.Context) {
|
|
||||||
err := topic.Shutdown(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("fail to shutdown pubsub topic")
|
|
||||||
}
|
|
||||||
}(topic, ctx)
|
|
||||||
|
|
||||||
subscription, err := pubsub.OpenSubscription(ctx, pubsubString)
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("failed to open pubsub topic")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func(topic *pubsub.Subscription, ctx context.Context) {
|
|
||||||
err := topic.Shutdown(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("fail to shutdown pubsub topic")
|
|
||||||
}
|
|
||||||
}(subscription, ctx)
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
default:
|
|
||||||
msg, err := subscription.Receive(ctx)
|
|
||||||
log.Debug().Msg("received thumbnail generation request from pubsub topic")
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("failed to receive message from pubsub topic")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if msg == nil {
|
|
||||||
log.Warn().Msg("received nil message from pubsub topic")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
groupId, err := uuid.Parse(msg.Metadata["group_id"])
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Str("group_id", msg.Metadata["group_id"]).Msg("failed to parse group ID from message metadata")
|
|
||||||
}
|
|
||||||
attachmentId, err := uuid.Parse(msg.Metadata["attachment_id"])
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Str("attachment_id", msg.Metadata["attachment_id"]).Msg("failed to parse attachment ID from message metadata")
|
|
||||||
}
|
|
||||||
err = app.repos.Attachments.CreateThumbnail(ctx, groupId, attachmentId, msg.Metadata["title"], msg.Metadata["path"])
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("failed to create thumbnail")
|
|
||||||
}
|
|
||||||
msg.Ack()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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())
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("failed to get latest github release")
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.Debug.Enabled {
|
|
||||||
runner.AddFunc("debug", func(ctx context.Context) error {
|
|
||||||
debugserver := http.Server{
|
|
||||||
Addr: fmt.Sprintf("%s:%s", cfg.Web.Host, cfg.Debug.Port),
|
|
||||||
Handler: app.debugRouter(),
|
|
||||||
ReadTimeout: cfg.Web.ReadTimeout,
|
|
||||||
WriteTimeout: cfg.Web.WriteTimeout,
|
|
||||||
IdleTimeout: cfg.Web.IdleTimeout,
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
<-ctx.Done()
|
|
||||||
_ = debugserver.Shutdown(context.Background())
|
|
||||||
}()
|
|
||||||
|
|
||||||
log.Info().Msgf("Debug server is running on %s:%s", cfg.Web.Host, cfg.Debug.Port)
|
|
||||||
return debugserver.ListenAndServe()
|
|
||||||
})
|
|
||||||
// Print the configuration to the console
|
|
||||||
cfg.Print()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -52,11 +51,9 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
|
|||||||
a.services,
|
a.services,
|
||||||
a.repos,
|
a.repos,
|
||||||
a.bus,
|
a.bus,
|
||||||
a.conf,
|
|
||||||
v1.WithMaxUploadSize(a.conf.Web.MaxUploadSize),
|
v1.WithMaxUploadSize(a.conf.Web.MaxUploadSize),
|
||||||
v1.WithRegistration(a.conf.Options.AllowRegistration),
|
v1.WithRegistration(a.conf.Options.AllowRegistration),
|
||||||
v1.WithDemoStatus(a.conf.Demo), // Disable Password Change in Demo Mode
|
v1.WithDemoStatus(a.conf.Demo), // Disable Password Change in Demo Mode
|
||||||
v1.WithURL(fmt.Sprintf("%s:%s", a.conf.Web.Host, a.conf.Web.Port)),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
r.Route(prefix+"/v1", func(r chi.Router) {
|
r.Route(prefix+"/v1", func(r chi.Router) {
|
||||||
@@ -75,11 +72,6 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
|
|||||||
r.Post("/users/register", chain.ToHandlerFunc(v1Ctrl.HandleUserRegistration()))
|
r.Post("/users/register", chain.ToHandlerFunc(v1Ctrl.HandleUserRegistration()))
|
||||||
r.Post("/users/login", chain.ToHandlerFunc(v1Ctrl.HandleAuthLogin(providers...)))
|
r.Post("/users/login", chain.ToHandlerFunc(v1Ctrl.HandleAuthLogin(providers...)))
|
||||||
|
|
||||||
if a.conf.OIDC.Enabled {
|
|
||||||
r.Get("/users/login/oidc", chain.ToHandlerFunc(v1Ctrl.HandleOIDCLogin()))
|
|
||||||
r.Get("/users/login/oidc/callback", chain.ToHandlerFunc(v1Ctrl.HandleOIDCCallback()))
|
|
||||||
}
|
|
||||||
|
|
||||||
userMW := []errchain.Middleware{
|
userMW := []errchain.Middleware{
|
||||||
a.mwAuthToken,
|
a.mwAuthToken,
|
||||||
a.mwRoles(RoleModeOr, authroles.RoleUser.String()),
|
a.mwRoles(RoleModeOr, authroles.RoleUser.String()),
|
||||||
@@ -107,7 +99,6 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
|
|||||||
r.Post("/actions/zero-item-time-fields", chain.ToHandlerFunc(v1Ctrl.HandleItemDateZeroOut(), userMW...))
|
r.Post("/actions/zero-item-time-fields", chain.ToHandlerFunc(v1Ctrl.HandleItemDateZeroOut(), userMW...))
|
||||||
r.Post("/actions/ensure-import-refs", chain.ToHandlerFunc(v1Ctrl.HandleEnsureImportRefs(), userMW...))
|
r.Post("/actions/ensure-import-refs", chain.ToHandlerFunc(v1Ctrl.HandleEnsureImportRefs(), userMW...))
|
||||||
r.Post("/actions/set-primary-photos", chain.ToHandlerFunc(v1Ctrl.HandleSetPrimaryPhotos(), userMW...))
|
r.Post("/actions/set-primary-photos", chain.ToHandlerFunc(v1Ctrl.HandleSetPrimaryPhotos(), userMW...))
|
||||||
r.Post("/actions/create-missing-thumbnails", chain.ToHandlerFunc(v1Ctrl.HandleCreateMissingThumbnails(), userMW...))
|
|
||||||
|
|
||||||
r.Get("/locations", chain.ToHandlerFunc(v1Ctrl.HandleLocationGetAll(), userMW...))
|
r.Get("/locations", chain.ToHandlerFunc(v1Ctrl.HandleLocationGetAll(), userMW...))
|
||||||
r.Post("/locations", chain.ToHandlerFunc(v1Ctrl.HandleLocationCreate(), userMW...))
|
r.Post("/locations", chain.ToHandlerFunc(v1Ctrl.HandleLocationCreate(), userMW...))
|
||||||
@@ -134,7 +125,6 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
|
|||||||
r.Put("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemUpdate(), userMW...))
|
r.Put("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemUpdate(), userMW...))
|
||||||
r.Patch("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemPatch(), userMW...))
|
r.Patch("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemPatch(), userMW...))
|
||||||
r.Delete("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemDelete(), userMW...))
|
r.Delete("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemDelete(), userMW...))
|
||||||
r.Post("/items/{id}/duplicate", chain.ToHandlerFunc(v1Ctrl.HandleItemDuplicate(), userMW...))
|
|
||||||
|
|
||||||
r.Post("/items/{id}/attachments", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentCreate(), userMW...))
|
r.Post("/items/{id}/attachments", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentCreate(), userMW...))
|
||||||
r.Put("/items/{id}/attachments/{attachment_id}", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentUpdate(), userMW...))
|
r.Put("/items/{id}/attachments/{attachment_id}", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentUpdate(), userMW...))
|
||||||
@@ -142,22 +132,11 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
|
|||||||
|
|
||||||
r.Get("/items/{id}/maintenance", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceLogGet(), userMW...))
|
r.Get("/items/{id}/maintenance", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceLogGet(), userMW...))
|
||||||
r.Post("/items/{id}/maintenance", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryCreate(), userMW...))
|
r.Post("/items/{id}/maintenance", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryCreate(), userMW...))
|
||||||
|
r.Put("/items/{id}/maintenance/{entry_id}", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryUpdate(), userMW...))
|
||||||
|
r.Delete("/items/{id}/maintenance/{entry_id}", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryDelete(), userMW...))
|
||||||
|
|
||||||
r.Get("/assets/{id}", chain.ToHandlerFunc(v1Ctrl.HandleAssetGet(), userMW...))
|
r.Get("/assets/{id}", chain.ToHandlerFunc(v1Ctrl.HandleAssetGet(), userMW...))
|
||||||
|
|
||||||
// Item Templates
|
|
||||||
r.Get("/templates", chain.ToHandlerFunc(v1Ctrl.HandleItemTemplatesGetAll(), userMW...))
|
|
||||||
r.Post("/templates", chain.ToHandlerFunc(v1Ctrl.HandleItemTemplatesCreate(), userMW...))
|
|
||||||
r.Get("/templates/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemTemplatesGet(), userMW...))
|
|
||||||
r.Put("/templates/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemTemplatesUpdate(), userMW...))
|
|
||||||
r.Delete("/templates/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemTemplatesDelete(), userMW...))
|
|
||||||
r.Post("/templates/{id}/create-item", chain.ToHandlerFunc(v1Ctrl.HandleItemTemplatesCreateItem(), userMW...))
|
|
||||||
|
|
||||||
// Maintenance
|
|
||||||
r.Get("/maintenance", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceGetAll(), userMW...))
|
|
||||||
r.Put("/maintenance/{id}", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryUpdate(), userMW...))
|
|
||||||
r.Delete("/maintenance/{id}", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryDelete(), userMW...))
|
|
||||||
|
|
||||||
// Notifiers
|
// Notifiers
|
||||||
r.Get("/notifiers", chain.ToHandlerFunc(v1Ctrl.HandleGetUserNotifiers(), userMW...))
|
r.Get("/notifiers", chain.ToHandlerFunc(v1Ctrl.HandleGetUserNotifiers(), userMW...))
|
||||||
r.Post("/notifiers", chain.ToHandlerFunc(v1Ctrl.HandleCreateNotifier(), userMW...))
|
r.Post("/notifiers", chain.ToHandlerFunc(v1Ctrl.HandleCreateNotifier(), userMW...))
|
||||||
@@ -171,19 +150,12 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
|
|||||||
a.mwRoles(RoleModeOr, authroles.RoleUser.String(), authroles.RoleAttachments.String()),
|
a.mwRoles(RoleModeOr, authroles.RoleUser.String(), authroles.RoleAttachments.String()),
|
||||||
}
|
}
|
||||||
|
|
||||||
r.Get("/products/search-from-barcode", chain.ToHandlerFunc(v1Ctrl.HandleProductSearchFromBarcode(a.conf.Barcode), userMW...))
|
|
||||||
|
|
||||||
r.Get("/qrcode", chain.ToHandlerFunc(v1Ctrl.HandleGenerateQRCode(), assetMW...))
|
r.Get("/qrcode", chain.ToHandlerFunc(v1Ctrl.HandleGenerateQRCode(), assetMW...))
|
||||||
r.Get(
|
r.Get(
|
||||||
"/items/{id}/attachments/{attachment_id}",
|
"/items/{id}/attachments/{attachment_id}",
|
||||||
chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentGet(), assetMW...),
|
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
|
// Reporting Services
|
||||||
r.Get("/reporting/bill-of-materials", chain.ToHandlerFunc(v1Ctrl.HandleBillOfMaterialsExport(), userMW...))
|
r.Get("/reporting/bill-of-materials", chain.ToHandlerFunc(v1Ctrl.HandleBillOfMaterialsExport(), userMW...))
|
||||||
|
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/core/currencies"
|
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
// setupStorageDir handles the creation and validation of the storage directory.
|
|
||||||
func setupStorageDir(cfg *config.Config) error {
|
|
||||||
if strings.HasPrefix(cfg.Storage.ConnString, "file:///./") {
|
|
||||||
raw := strings.TrimPrefix(cfg.Storage.ConnString, "file:///./")
|
|
||||||
clean := filepath.Clean(raw)
|
|
||||||
absBase, err := filepath.Abs(clean)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("failed to get absolute path for storage connection string")
|
|
||||||
return fmt.Errorf("failed to get absolute path for storage connection string: %w", err)
|
|
||||||
}
|
|
||||||
absBase = strings.ReplaceAll(absBase, "\\", "/")
|
|
||||||
storageDir := filepath.Join(absBase, cfg.Storage.PrefixPath)
|
|
||||||
storageDir = strings.ReplaceAll(storageDir, "\\", "/")
|
|
||||||
if !strings.HasPrefix(storageDir, absBase+"/") && storageDir != absBase {
|
|
||||||
log.Error().Str("path", storageDir).Msg("invalid storage path: you tried to use a prefix that is not a subdirectory of the base path")
|
|
||||||
return fmt.Errorf("invalid storage path: you tried to use a prefix that is not a subdirectory of the base path")
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(storageDir, 0o750); err != nil {
|
|
||||||
log.Error().Err(err).Msg("failed to create data directory")
|
|
||||||
return fmt.Errorf("failed to create data directory: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// setupDatabaseURL returns the database URL and ensures any required directories exist.
|
|
||||||
func setupDatabaseURL(cfg *config.Config) (string, error) {
|
|
||||||
databaseURL := ""
|
|
||||||
switch strings.ToLower(cfg.Database.Driver) {
|
|
||||||
case "sqlite3":
|
|
||||||
databaseURL = cfg.Database.SqlitePath
|
|
||||||
dbFilePath := strings.Split(cfg.Database.SqlitePath, "?")[0]
|
|
||||||
dbDir := filepath.Dir(dbFilePath)
|
|
||||||
if err := os.MkdirAll(dbDir, 0o755); err != nil {
|
|
||||||
log.Error().Err(err).Str("path", dbDir).Msg("failed to create SQLite database directory")
|
|
||||||
return "", fmt.Errorf("failed to create SQLite database directory: %w", err)
|
|
||||||
}
|
|
||||||
case "postgres":
|
|
||||||
databaseURL = fmt.Sprintf("host=%s port=%s dbname=%s sslmode=%s", cfg.Database.Host, cfg.Database.Port, cfg.Database.Database, cfg.Database.SslMode)
|
|
||||||
if cfg.Database.Username != "" {
|
|
||||||
databaseURL += fmt.Sprintf(" user=%s", cfg.Database.Username)
|
|
||||||
}
|
|
||||||
if cfg.Database.Password != "" {
|
|
||||||
databaseURL += fmt.Sprintf(" password=%s", cfg.Database.Password)
|
|
||||||
}
|
|
||||||
if cfg.Database.SslRootCert != "" {
|
|
||||||
if _, err := os.Stat(cfg.Database.SslRootCert); err != nil {
|
|
||||||
log.Error().Err(err).Str("path", cfg.Database.SslRootCert).Msg("SSL root certificate file is not accessible")
|
|
||||||
return "", fmt.Errorf("SSL root certificate file is not accessible: %w", err)
|
|
||||||
}
|
|
||||||
databaseURL += fmt.Sprintf(" sslrootcert=%s", cfg.Database.SslRootCert)
|
|
||||||
}
|
|
||||||
if cfg.Database.SslCert != "" {
|
|
||||||
if _, err := os.Stat(cfg.Database.SslCert); err != nil {
|
|
||||||
log.Error().Err(err).Str("path", cfg.Database.SslCert).Msg("SSL certificate file is not accessible")
|
|
||||||
return "", fmt.Errorf("SSL certificate file is not accessible: %w", err)
|
|
||||||
}
|
|
||||||
databaseURL += fmt.Sprintf(" sslcert=%s", cfg.Database.SslCert)
|
|
||||||
}
|
|
||||||
if cfg.Database.SslKey != "" {
|
|
||||||
if _, err := os.Stat(cfg.Database.SslKey); err != nil {
|
|
||||||
log.Error().Err(err).Str("path", cfg.Database.SslKey).Msg("SSL key file is not accessible")
|
|
||||||
return "", fmt.Errorf("SSL key file is not accessible: %w", err)
|
|
||||||
}
|
|
||||||
databaseURL += fmt.Sprintf(" sslkey=%s", cfg.Database.SslKey)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
log.Error().Str("driver", cfg.Database.Driver).Msg("unsupported database driver")
|
|
||||||
return "", fmt.Errorf("unsupported database driver: %s", cfg.Database.Driver)
|
|
||||||
}
|
|
||||||
return databaseURL, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadCurrencies loads currency data from config if provided.
|
|
||||||
func loadCurrencies(cfg *config.Config) ([]currencies.CollectorFunc, error) {
|
|
||||||
collectFuncs := []currencies.CollectorFunc{
|
|
||||||
currencies.CollectDefaults(),
|
|
||||||
}
|
|
||||||
if cfg.Options.CurrencyConfig != "" {
|
|
||||||
log.Info().Str("path", cfg.Options.CurrencyConfig).Msg("loading currency config file")
|
|
||||||
content, err := os.ReadFile(cfg.Options.CurrencyConfig)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Str("path", cfg.Options.CurrencyConfig).Msg("failed to read currency config file")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
collectFuncs = append(collectFuncs, currencies.CollectJSON(bytes.NewReader(content)))
|
|
||||||
}
|
|
||||||
return collectFuncs, nil
|
|
||||||
}
|
|
||||||
45
backend/app/tools/migrations/main.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/migrate"
|
||||||
|
|
||||||
|
atlas "ariga.io/atlas/sql/migrate"
|
||||||
|
_ "ariga.io/atlas/sql/sqlite"
|
||||||
|
"entgo.io/ent/dialect"
|
||||||
|
"entgo.io/ent/dialect/sql/schema"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx := context.Background()
|
||||||
|
// Create a local migration directory able to understand Atlas migration file format for replay.
|
||||||
|
dir, err := atlas.NewLocalDir("internal/data/migrations/migrations")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed creating atlas migration directory: %v", err)
|
||||||
|
}
|
||||||
|
// Migrate diff options.
|
||||||
|
opts := []schema.MigrateOption{
|
||||||
|
schema.WithDir(dir), // provide migration directory
|
||||||
|
schema.WithMigrationMode(schema.ModeReplay), // provide migration mode
|
||||||
|
schema.WithDialect(dialect.SQLite), // Ent dialect to use
|
||||||
|
schema.WithFormatter(atlas.DefaultFormatter),
|
||||||
|
schema.WithDropIndex(true),
|
||||||
|
schema.WithDropColumn(true),
|
||||||
|
}
|
||||||
|
if len(os.Args) != 2 {
|
||||||
|
log.Fatalln("migration name is required. Use: 'go run -mod=mod ent/migrate/main.go <name>'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate migrations using Atlas support for MySQL (note the Ent dialect option passed above).
|
||||||
|
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.")
|
||||||
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
-----BEGIN ENCRYPTED SIGSTORE PRIVATE KEY-----
|
|
||||||
eyJrZGYiOnsibmFtZSI6InNjcnlwdCIsInBhcmFtcyI6eyJOIjo2NTUzNiwiciI6
|
|
||||||
OCwicCI6MX0sInNhbHQiOiJ3bmU3TTd2dndlL2FBS1piUEE2QktsdFNzMkhkSk9v
|
|
||||||
eXlvOTNLMnByRXdJPSJ9LCJjaXBoZXIiOnsibmFtZSI6Im5hY2wvc2VjcmV0Ym94
|
|
||||||
Iiwibm9uY2UiOiJoOWdIMHRsYk9zMnZIbVBTYk5zaGxBQU5TYUlkcVZoQiJ9LCJj
|
|
||||||
aXBoZXJ0ZXh0IjoiTERiQk5ac3ZlVnRMbTlQdkRTa2t6bzRrWGExVGRTTEY5VzVO
|
|
||||||
cGd6M05GNVJLRWlGRmJQRDJDYzhnTWNkRmkrTU8xd2FTUzFGWWdXU3BIdnI3QXZ3
|
|
||||||
K0tUTXVWLzhSZ1pnOE9ieHNJY2xKSlZldHRLTzdzWXY2aWgxM09iZlVBV0lQcGpS
|
|
||||||
ZUQ5UmE3WjJwbWd0SkpBdjl2dlk1RGNNeGRKcFFrOEY1UStLZytSbnhLRUd6Z1ZN
|
|
||||||
MWUxdjF3UGhsOWhVRGRMSFVSTzE5Z0w3aFE9PSJ9
|
|
||||||
-----END ENCRYPTED SIGSTORE PRIVATE KEY-----
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
-----BEGIN PUBLIC KEY-----
|
|
||||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2DXKcerPznDayM+rMJ/25w+ubI8g
|
|
||||||
e3ZTbm07VqLFz6uI2vXqN8X7/72dygtJlUw07FpR0oLXaSia0adaywz1JA==
|
|
||||||
-----END PUBLIC KEY-----
|
|
||||||
231
backend/go.mod
@@ -1,209 +1,78 @@
|
|||||||
module github.com/sysadminsmedia/homebox/backend
|
module github.com/sysadminsmedia/homebox/backend
|
||||||
|
|
||||||
go 1.24.0
|
go 1.22
|
||||||
|
|
||||||
toolchain go1.24.3
|
toolchain go1.22.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
entgo.io/ent v0.14.5
|
ariga.io/atlas v0.19.1
|
||||||
github.com/ardanlabs/conf/v3 v3.9.0
|
entgo.io/ent v0.12.5
|
||||||
|
github.com/ardanlabs/conf/v3 v3.1.7
|
||||||
github.com/containrrr/shoutrrr v0.8.0
|
github.com/containrrr/shoutrrr v0.8.0
|
||||||
github.com/coreos/go-oidc/v3 v3.17.0
|
github.com/go-chi/chi/v5 v5.0.12
|
||||||
github.com/evanoberholster/imagemeta v0.3.1
|
github.com/go-playground/validator/v10 v10.18.0
|
||||||
github.com/gen2brain/avif v0.4.4
|
github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a
|
||||||
github.com/gen2brain/heic v0.4.6
|
|
||||||
github.com/gen2brain/jpegxl v0.4.5
|
|
||||||
github.com/gen2brain/webp v0.5.5
|
|
||||||
github.com/go-chi/chi/v5 v5.2.3
|
|
||||||
github.com/go-playground/validator/v10 v10.28.0
|
|
||||||
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1
|
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/schema v1.4.1
|
github.com/gorilla/schema v1.4.1
|
||||||
github.com/hay-kot/httpkit v0.0.11
|
github.com/hay-kot/httpkit v0.0.9
|
||||||
github.com/lib/pq v1.10.9
|
github.com/mattn/go-sqlite3 v1.14.22
|
||||||
github.com/mattn/go-sqlite3 v1.14.32
|
github.com/olahol/melody v1.1.4
|
||||||
github.com/olahol/melody v1.4.0
|
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/pressly/goose/v3 v3.26.0
|
github.com/rs/zerolog v1.32.0
|
||||||
github.com/rs/zerolog v1.34.0
|
github.com/stretchr/testify v1.8.4
|
||||||
github.com/shirou/gopsutil/v4 v4.25.11
|
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
|
||||||
github.com/stretchr/testify v1.11.1
|
|
||||||
github.com/swaggo/http-swagger/v2 v2.0.2
|
github.com/swaggo/http-swagger/v2 v2.0.2
|
||||||
github.com/swaggo/swag v1.16.6
|
github.com/swaggo/swag v1.16.3
|
||||||
github.com/yeqown/go-qrcode/v2 v2.2.5
|
github.com/yeqown/go-qrcode/v2 v2.2.2
|
||||||
github.com/yeqown/go-qrcode/writer/standard v1.3.0
|
github.com/yeqown/go-qrcode/writer/standard v1.2.2
|
||||||
github.com/zeebo/blake3 v0.2.4
|
golang.org/x/crypto v0.23.0
|
||||||
go.balki.me/anyhttp v0.5.2
|
modernc.org/sqlite v1.29.2
|
||||||
gocloud.dev v0.44.0
|
|
||||||
gocloud.dev/pubsub/kafkapubsub v0.44.0
|
|
||||||
gocloud.dev/pubsub/natspubsub v0.44.0
|
|
||||||
gocloud.dev/pubsub/rabbitpubsub v0.44.0
|
|
||||||
golang.org/x/crypto v0.45.0
|
|
||||||
golang.org/x/image v0.33.0
|
|
||||||
golang.org/x/oauth2 v0.33.0
|
|
||||||
golang.org/x/text v0.31.0
|
|
||||||
modernc.org/sqlite v1.40.1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9 // indirect
|
|
||||||
cel.dev/expr v0.24.0 // indirect
|
|
||||||
cloud.google.com/go v0.121.6 // indirect
|
|
||||||
cloud.google.com/go/auth v0.17.0 // indirect
|
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
|
||||||
cloud.google.com/go/iam v1.5.2 // indirect
|
|
||||||
cloud.google.com/go/monitoring v1.24.2 // indirect
|
|
||||||
cloud.google.com/go/pubsub v1.50.0 // indirect
|
|
||||||
cloud.google.com/go/pubsub/v2 v2.2.1 // indirect
|
|
||||||
cloud.google.com/go/storage v1.56.0 // indirect
|
|
||||||
github.com/Azure/azure-amqp-common-go/v3 v3.2.3 // indirect
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 // indirect
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // indirect
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.9.1 // indirect
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 // indirect
|
|
||||||
github.com/Azure/go-amqp v1.4.0 // indirect
|
|
||||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
|
||||||
github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect
|
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
|
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect
|
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect
|
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect
|
|
||||||
github.com/IBM/sarama v1.46.3 // indirect
|
|
||||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
github.com/agext/levenshtein v1.2.3 // indirect
|
github.com/agext/levenshtein v1.2.3 // indirect
|
||||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2 v1.39.6 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.31.17 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.3 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.89.2 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/sns v1.34.7 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // indirect
|
|
||||||
github.com/aws/smithy-go v1.23.2 // indirect
|
|
||||||
github.com/bmatcuk/doublestar v1.3.4 // indirect
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
|
||||||
github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/eapache/go-resiliency v1.7.0 // indirect
|
github.com/fatih/color v1.16.0 // indirect
|
||||||
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect
|
|
||||||
github.com/eapache/queue v1.1.0 // indirect
|
|
||||||
github.com/ebitengine/purego v0.9.1 // indirect
|
|
||||||
github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect
|
|
||||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
|
|
||||||
github.com/fatih/color v1.18.0 // indirect
|
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
|
||||||
github.com/fogleman/gg v1.3.0 // indirect
|
github.com/fogleman/gg v1.3.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
|
||||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
|
||||||
github.com/go-openapi/inflect v0.19.0 // indirect
|
github.com/go-openapi/inflect v0.19.0 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.22.3 // indirect
|
github.com/go-openapi/jsonpointer v0.20.0 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.21.3 // indirect
|
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||||
github.com/go-openapi/spec v0.22.1 // indirect
|
github.com/go-openapi/spec v0.20.9 // indirect
|
||||||
github.com/go-openapi/swag/conv v0.25.4 // indirect
|
github.com/go-openapi/swag v0.22.4 // indirect
|
||||||
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
|
|
||||||
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
|
|
||||||
github.com/go-openapi/swag/loading v0.25.4 // indirect
|
|
||||||
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
|
|
||||||
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
|
|
||||||
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
|
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.3 // indirect
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
github.com/golang/snappy v1.0.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
github.com/gorilla/websocket v1.5.1 // indirect
|
||||||
github.com/google/s2a-go v0.1.9 // indirect
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
github.com/google/wire v0.7.0 // indirect
|
github.com/hashicorp/hcl/v2 v2.19.1 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
|
||||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
|
||||||
github.com/hashicorp/hcl/v2 v2.18.1 // indirect
|
|
||||||
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
|
|
||||||
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
|
|
||||||
github.com/jcmturner/gofork v1.7.6 // indirect
|
|
||||||
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
|
|
||||||
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
|
|
||||||
github.com/klauspost/compress v1.18.2 // indirect
|
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
|
||||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
|
||||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||||
github.com/nats-io/nats.go v1.47.0 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/nats-io/nkeys v0.4.12 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
|
||||||
github.com/philhofer/fwd v1.2.0 // indirect
|
|
||||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
|
||||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
|
||||||
github.com/rabbitmq/amqp091-go v1.10.0 // indirect
|
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||||
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
|
github.com/swaggo/files/v2 v2.0.0 // indirect
|
||||||
github.com/swaggo/files/v2 v2.0.2 // indirect
|
|
||||||
github.com/tetratelabs/wazero v1.10.1 // indirect
|
|
||||||
github.com/tinylib/msgp v1.6.1 // indirect
|
|
||||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
|
||||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
|
||||||
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/zclconf/go-cty v1.14.1 // indirect
|
||||||
github.com/zclconf/go-cty v1.14.4 // indirect
|
golang.org/x/image v0.18.0 // indirect
|
||||||
github.com/zclconf/go-cty-yaml v1.1.0 // indirect
|
golang.org/x/mod v0.17.0 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
golang.org/x/net v0.25.0 // indirect
|
||||||
go.opentelemetry.io/contrib/detectors/gcp v1.38.0 // indirect
|
golang.org/x/sys v0.20.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 // indirect
|
golang.org/x/text v0.16.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
|
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
|
||||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 // indirect
|
|
||||||
golang.org/x/mod v0.30.0 // indirect
|
|
||||||
golang.org/x/net v0.47.0 // indirect
|
|
||||||
golang.org/x/sync v0.18.0 // indirect
|
|
||||||
golang.org/x/sys v0.38.0 // indirect
|
|
||||||
golang.org/x/time v0.14.0 // indirect
|
|
||||||
golang.org/x/tools v0.39.0 // indirect
|
|
||||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
|
||||||
google.golang.org/api v0.257.0 // indirect
|
|
||||||
google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 // indirect
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
|
||||||
google.golang.org/grpc v1.77.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.67.1 // indirect
|
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/libc v1.41.0 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
|
modernc.org/memory v1.7.2 // indirect
|
||||||
|
modernc.org/strutil v1.2.0 // indirect
|
||||||
|
modernc.org/token v1.1.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
627
backend/go.sum
@@ -1,593 +1,210 @@
|
|||||||
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9 h1:E0wvcUXTkgyN4wy4LGtNzMNGMytJN8afmIWXJVMi4cc=
|
ariga.io/atlas v0.19.1 h1:QzBHkakwzEhmPWOzNhw8Yr/Bbicj6Iq5hwEoNI/Jr9A=
|
||||||
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9/go.mod h1:Oe1xWPuu5q9LzyrWfbZmEZxFYeu4BHTyzfjeW2aZp/w=
|
ariga.io/atlas v0.19.1/go.mod h1:VPlcXdd4w2KqKnH54yEZcry79UAhpaWaxEsmn5JRNoE=
|
||||||
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
|
entgo.io/ent v0.12.5 h1:KREM5E4CSoej4zeGa88Ou/gfturAnpUv0mzAjch1sj4=
|
||||||
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
entgo.io/ent v0.12.5/go.mod h1:Y3JVAjtlIk8xVZYSn3t3mf8xlZIn5SAOXZQxD6kKI+Q=
|
||||||
cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c=
|
|
||||||
cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=
|
|
||||||
cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
|
|
||||||
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
|
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
|
||||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
|
||||||
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
|
|
||||||
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
|
|
||||||
cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc=
|
|
||||||
cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA=
|
|
||||||
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
|
|
||||||
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
|
|
||||||
cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM=
|
|
||||||
cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U=
|
|
||||||
cloud.google.com/go/pubsub v1.50.0 h1:hnYpOIxVlgVD1Z8LN7est4DQZK3K6tvZNurZjIVjUe0=
|
|
||||||
cloud.google.com/go/pubsub v1.50.0/go.mod h1:Di2Y+nqXBpIS+dXUEJPQzLh8PbIQZMLE9IVUFhf2zmM=
|
|
||||||
cloud.google.com/go/pubsub/v2 v2.2.1 h1:3brZcshL3fIiD1qOxAE2QW9wxsfjioy014x4yC9XuYI=
|
|
||||||
cloud.google.com/go/pubsub/v2 v2.2.1/go.mod h1:O5f0KHG9zDheZAd3z5rlCRhxt2JQtB+t/IYLKK3Bpvw=
|
|
||||||
cloud.google.com/go/storage v1.56.0 h1:iixmq2Fse2tqxMbWhLWC9HfBj1qdxqAmiK8/eqtsLxI=
|
|
||||||
cloud.google.com/go/storage v1.56.0/go.mod h1:Tpuj6t4NweCLzlNbw9Z9iwxEkrSem20AetIeH/shgVU=
|
|
||||||
cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=
|
|
||||||
cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=
|
|
||||||
entgo.io/ent v0.14.5 h1:Rj2WOYJtCkWyFo6a+5wB3EfBRP0rnx1fMk6gGA0UUe4=
|
|
||||||
entgo.io/ent v0.14.5/go.mod h1:zTzLmWtPvGpmSwtkaayM2cm5m819NdM7z7tYPq3vN0U=
|
|
||||||
github.com/Azure/azure-amqp-common-go/v3 v3.2.3 h1:uDF62mbd9bypXWi19V1bN5NZEO84JqgmI5G73ibAmrk=
|
|
||||||
github.com/Azure/azure-amqp-common-go/v3 v3.2.3/go.mod h1:7rPmbSfszeovxGfc5fSAXE4ehlXQZHpMja2OtxC2Tas=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 h1:Wc1ml6QlJs2BHQ/9Bqu1jiyggbsSjramq2oUmp5WeIo=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.9.1 h1:CRZwf68N55u7ZZo3Xx2ynuqEA6k5GZfwsEUkU8qsAPk=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.9.1/go.mod h1:NydgUaroiShkgOcb+X6OUdS3RalWBrvDNtOyFHJtsZY=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.0 h1:LR0kAX9ykz8G4YgLCaRDVJ3+n43R8MneB5dTy2konZo=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.0/go.mod h1:DWAciXemNf++PQJLeXUB4HHH5OpsAh12HZnu2wXE1jA=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 h1:lhZdRq7TIx0GJQvSyX2Si406vrYsov2FXGp/RnSEtcs=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1/go.mod h1:8cl44BDmi+effbARHMQjgOKA2AYvcohNm7KEt42mSV8=
|
|
||||||
github.com/Azure/go-amqp v0.17.0/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fwmw9Zlg=
|
|
||||||
github.com/Azure/go-amqp v1.4.0 h1:Xj3caqi4comOF/L1Uc5iuBxR/pB6KumejC01YQOqOR4=
|
|
||||||
github.com/Azure/go-amqp v1.4.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE=
|
|
||||||
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
|
|
||||||
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
|
||||||
github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA=
|
|
||||||
github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=
|
|
||||||
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
|
|
||||||
github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
|
|
||||||
github.com/Azure/go-autorest/autorest/to v0.4.1 h1:CxNHBqdzTr7rLtdrtb5CMjJcDut+WNGCVv7OmS5+lTc=
|
|
||||||
github.com/Azure/go-autorest/autorest/to v0.4.1/go.mod h1:EtaofgU4zmtvn1zT2ARsjRFdq9vXx0YWtmElwL+GZ9M=
|
|
||||||
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
|
|
||||||
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
|
||||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
|
||||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
|
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
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/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c=
|
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
|
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM=
|
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs=
|
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg=
|
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ=
|
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ=
|
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo=
|
|
||||||
github.com/IBM/sarama v1.46.3 h1:njRsX6jNlnR+ClJ8XmkO+CM4unbrNr/2vB5KK6UA+IE=
|
|
||||||
github.com/IBM/sarama v1.46.3/go.mod h1:GTUYiF9DMOZVe3FwyGT+dtSPceGFIgA+sPc5u6CBwko=
|
|
||||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
|
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
|
||||||
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
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 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
|
||||||
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
|
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
|
||||||
github.com/ardanlabs/conf/v3 v3.9.0 h1:aRBYHeD39/OkuaEXYIEoi4wvF3OnS7jUAPxXyLfEu20=
|
github.com/ardanlabs/conf/v3 v3.1.7 h1:p232cF68TafoA5U9ZlbxUIhGJtGNdKHBXF80Fdqb5t0=
|
||||||
github.com/ardanlabs/conf/v3 v3.9.0/go.mod h1:XlL9P0quWP4m1weOVFmlezabinbZLI05niDof/+Ochk=
|
github.com/ardanlabs/conf/v3 v3.1.7/go.mod h1:zclexWKe0NVj6LHQ8NgDDZ7bQ1spE0KeKPFficdtAjU=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk=
|
|
||||||
github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
|
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0=
|
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y=
|
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.31.17 h1:QFl8lL6RgakNK86vusim14P2k8BFSxjvUkcWLDjgz9Y=
|
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.31.17/go.mod h1:V8P7ILjp/Uef/aX8TjGk6OHZN6IKPM5YW6S78QnRD5c=
|
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 h1:56HGpsgnmD+2/KpG0ikvvR8+3v3COCwaF4r+oWwOeNA=
|
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21/go.mod h1:3YELwedmQbw7cXNaII2Wywd+YY58AmLPwX4LzARgmmA=
|
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk=
|
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk=
|
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.3 h1:4GNV1lhyELGjMz5ILMRxDvxvOaeo3Ux9Z69S1EgVMMQ=
|
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.3/go.mod h1:br7KA6edAAqDGUYJ+zVVPAyMrPhnN+zdt17yTUT6FPw=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13 h1:eg/WYAa12vqTphzIdWMzqYRVKKnCboVPRlvaybNCqPA=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13/go.mod h1:/FDdxWhz1486obGrKKC1HONd7krpk38LBt+dutLcN9k=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 h1:NvMjwvv8hpGUILarKw7Z4Q0w1H9anXKsesMxtw++MA4=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4/go.mod h1:455WPHSwaGj2waRSpQp7TsnpOnBfw8iDfPfbwl7KPJE=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 h1:zhBJXdhWIFZ1acfDYIhu4+LCzdUS2Vbcum7D01dXlHQ=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13/go.mod h1:JaaOeCE368qn2Hzi3sEzY6FgAZVCIYcC2nwbro2QCh8=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.89.2 h1:xgBWsgaeUESl8A8k80p6yBdexMWDVeiDmJ/pkjohJ7c=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.89.2/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/sns v1.34.7 h1:OBuZE9Wt8h2imuRktu+WfjiTGrnYdCIJg8IX92aalHE=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/sns v1.34.7/go.mod h1:4WYoZAhHt+dWYpoOQUgkUKfuQbE6Gg/hW4oXE0pKS9U=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8 h1:80dpSqWMwx2dAm30Ib7J6ucz1ZHfiv5OCRwN/EnCOXQ=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8/go.mod h1:IzNt/udsXlETCdvBOL0nmyMe2t9cGmXmZgsdoZGYYhI=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 h1:mLlUgHn02ue8whiR4BmxxGJLR2gwU6s6ZzJ5wDamBUs=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk=
|
|
||||||
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
|
|
||||||
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
|
||||||
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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
|
||||||
github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0=
|
|
||||||
github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4=
|
|
||||||
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
|
||||||
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
|
||||||
github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec=
|
github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec=
|
||||||
github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o=
|
github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o=
|
||||||
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
|
||||||
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
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.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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY=
|
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
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/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA=
|
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||||
github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=
|
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||||
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=
|
|
||||||
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=
|
|
||||||
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
|
|
||||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
|
||||||
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
|
||||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
|
||||||
github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM=
|
|
||||||
github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs=
|
|
||||||
github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo=
|
|
||||||
github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs=
|
|
||||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
|
|
||||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
|
|
||||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
|
|
||||||
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
|
|
||||||
github.com/evanoberholster/imagemeta v0.3.1 h1:E4GUjXcvlVMjP9joN25+bBNf3Al3MTTfMqCrDOCW+LE=
|
|
||||||
github.com/evanoberholster/imagemeta v0.3.1/go.mod h1:V0vtDJmjTqvwAYO8r+u33NRVIMXQb0qSqEfImoKEiXM=
|
|
||||||
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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
|
||||||
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
|
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/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
|
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
|
||||||
github.com/gen2brain/avif v0.4.4 h1:Ga/ss7qcWWQm2bxFpnjYjhJsNfZrWs5RsyklgFjKRSE=
|
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/gen2brain/avif v0.4.4/go.mod h1:/XCaJcjZraQwKVhpu9aEd9aLOssYOawLvhMBtmHVGqk=
|
|
||||||
github.com/gen2brain/heic v0.4.6 h1:sNh3mfaEZLmDJnFc5WoLxCzh/wj5GwfJScPfvF5CNJE=
|
|
||||||
github.com/gen2brain/heic v0.4.6/go.mod h1:ECnpqbqLu0qSje4KSNWUUDK47UPXPzl80T27GWGEL5I=
|
|
||||||
github.com/gen2brain/jpegxl v0.4.5 h1:TWpVEn5xkIfsswzkjHBArd0Cc9AE0tbjBSoa0jDsrbo=
|
|
||||||
github.com/gen2brain/jpegxl v0.4.5/go.mod h1:4kWYJ18xCEuO2vzocYdGpeqNJ990/Gjy3uLMg5TBN6I=
|
|
||||||
github.com/gen2brain/webp v0.5.5 h1:MvQR75yIPU/9nSqYT5h13k4URaJK3gf9tgz/ksRbyEg=
|
|
||||||
github.com/gen2brain/webp v0.5.5/go.mod h1:xOSMzp4aROt2KFW++9qcK/RBTOVC2S9tJG66ip/9Oc0=
|
|
||||||
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
|
||||||
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
|
||||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
|
||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
|
||||||
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
|
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/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
|
||||||
github.com/go-openapi/jsonpointer v0.22.3 h1:dKMwfV4fmt6Ah90zloTbUKWMD+0he+12XYAsPotrkn8=
|
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
github.com/go-openapi/jsonpointer v0.22.3/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo=
|
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
|
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||||
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
|
github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ=
|
||||||
github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k=
|
github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA=
|
||||||
github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA=
|
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
|
||||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||||
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
|
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||||
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
|
github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8=
|
||||||
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
|
github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
|
||||||
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
|
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||||
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
|
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||||
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
|
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
|
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
|
||||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
|
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||||
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
|
|
||||||
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
|
|
||||||
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
|
|
||||||
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
|
|
||||||
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
|
|
||||||
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
|
|
||||||
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
|
|
||||||
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
|
|
||||||
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
|
|
||||||
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
|
|
||||||
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
|
|
||||||
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
|
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
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/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 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
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 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U=
|
||||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
github.com/go-playground/validator/v10 v10.18.0/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 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
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=
|
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||||
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||||
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ=
|
github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a h1:RYfmiM0zluBJOiPDJseKLEN4BapJ42uSi9SZBQ2YyiA=
|
||||||
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
|
github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
|
||||||
github.com/google/go-replayers/grpcreplay v1.3.0 h1:1Keyy0m1sIpqstQmgz307zhiJ1pV4uIlFds5weTmxbo=
|
|
||||||
github.com/google/go-replayers/grpcreplay v1.3.0/go.mod h1:v6NgKtkijC0d3e3RW8il6Sy5sqRVUwoQa4mHOGEy8DI=
|
|
||||||
github.com/google/go-replayers/httpreplay v1.2.0 h1:VM1wEyyjaoU53BwrOnaf9VhAyQQEEioJvFYxYcLRKzk=
|
|
||||||
github.com/google/go-replayers/httpreplay v1.2.0/go.mod h1:WahEFFZZ7a1P4VM1qEeHy+tME4bwyqPcwWbNlUI1Mcg=
|
|
||||||
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
|
|
||||||
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
|
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
|
||||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
|
||||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
|
|
||||||
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
|
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
|
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
|
||||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
|
||||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
|
||||||
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
|
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
|
||||||
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
|
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
|
||||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||||
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/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
|
||||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
|
||||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
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/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/hashicorp/hcl/v2 v2.18.1 h1:6nxnOJFku1EuSawSD81fuviYUV8DxFr3fp2dUi3ZYSo=
|
github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5RPI=
|
||||||
github.com/hashicorp/hcl/v2 v2.18.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
|
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.9 h1:hu2TPY9awmIYWXxWGubaXl2U61pPvaVsm9YwboBRGu0=
|
||||||
github.com/hay-kot/httpkit v0.0.11/go.mod h1:0kZdk5/swzdfqfg2c6pBWimcgeJ9PTyO97EbHnYl2Sw=
|
github.com/hay-kot/httpkit v0.0.9/go.mod h1:AD22YluZrvBDxmtB3Pw2SOyp3A2PZqcmBZa0+COrhoU=
|
||||||
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
|
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
|
||||||
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
|
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
|
||||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
|
|
||||||
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
|
||||||
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
|
||||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
|
||||||
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
|
|
||||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
|
||||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
|
||||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
|
||||||
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
|
||||||
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
|
||||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
|
||||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
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 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
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 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
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/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
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.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.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.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
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-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
|
||||||
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
|
||||||
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
|
||||||
github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
|
|
||||||
github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
|
|
||||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
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/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||||
github.com/nats-io/jwt/v2 v2.5.0 h1:WQQ40AAlqqfx+f6ku+i0pOVm+ASirD4fUh+oQsiE9Ak=
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
github.com/nats-io/jwt/v2 v2.5.0/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI=
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/nats-io/nats-server/v2 v2.9.23 h1:6Wj6H6QpP9FMlpCyWUaNu2yeZ/qGj+mdRkZ1wbikExU=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/nats-io/nats-server/v2 v2.9.23/go.mod h1:wEjrEy9vnqIGE4Pqz4/c75v9Pmaq7My2IgFmnykc4C0=
|
github.com/olahol/melody v1.1.4 h1:RQHfKZkQmDxI0+SLZRNBCn4LiXdqxLKRGSkT8Dyoe/E=
|
||||||
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
github.com/olahol/melody v1.1.4/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4=
|
||||||
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
|
||||||
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
|
||||||
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
|
||||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
|
||||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
|
||||||
github.com/olahol/melody v1.4.0 h1:Pa5SdeZL/zXPi1tJuMAPDbl4n3gQOThSL6G1p4qZ4SI=
|
|
||||||
github.com/olahol/melody v1.4.0/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 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
|
||||||
github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts=
|
github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts=
|
||||||
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
||||||
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
||||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
|
||||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
|
||||||
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
|
||||||
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
|
||||||
github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM=
|
|
||||||
github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY=
|
|
||||||
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
|
||||||
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=
|
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
|
||||||
github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
|
|
||||||
github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
|
||||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
|
||||||
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
|
||||||
github.com/shirou/gopsutil/v4 v4.25.11 h1:X53gB7muL9Gnwwo2evPSE+SfOrltMoR6V3xJAXZILTY=
|
|
||||||
github.com/shirou/gopsutil/v4 v4.25.11/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
|
|
||||||
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/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/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
|
|
||||||
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
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.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.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.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU=
|
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
|
||||||
github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0=
|
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 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg=
|
||||||
github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ=
|
github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ=
|
||||||
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
|
||||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk=
|
||||||
github.com/tetratelabs/wazero v1.10.1 h1:2DugeJf6VVk58KTPszlNfeeN8AhhpwcZqkJj2wwFuH8=
|
github.com/yeqown/go-qrcode/v2 v2.2.2 h1:0comk6jEwi0oWNhKEmzx4JI+Q7XIneAApmFSMKWmSVc=
|
||||||
github.com/tetratelabs/wazero v1.10.1/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU=
|
github.com/yeqown/go-qrcode/v2 v2.2.2/go.mod h1:2Qsk2APUCPne0TsRo40DIkI5MYnbzYKCnKGEFWrxd24=
|
||||||
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
|
github.com/yeqown/go-qrcode/writer/standard v1.2.2 h1:gyzunKXgC0ZUpKqQFUImbAEwewAiwNCkxFEKZV80Kt4=
|
||||||
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
github.com/yeqown/go-qrcode/writer/standard v1.2.2/go.mod h1:bbVRiBJSRPj4UBZP/biLG7JSd9kHqXjErk1eakAMnRA=
|
||||||
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
|
||||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
|
||||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
|
||||||
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
|
||||||
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.3.0 h1:chdyhEfRtUPgQtuPeaWVGQ/TQx4rE1PqeoW3U+53t34=
|
|
||||||
github.com/yeqown/go-qrcode/writer/standard v1.3.0/go.mod h1:O4MbzsotGCvy8upYPCR91j81dr5XLT7heuljcNXW+oQ=
|
|
||||||
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
|
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/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/zclconf/go-cty v1.14.1 h1:t9fyA35fwjjUMcmL5hLER+e/rEPqrbCK1/OSE4SI9KA=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
github.com/zclconf/go-cty v1.14.1/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||||
github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
|
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
||||||
github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0=
|
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||||
github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs=
|
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||||
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||||
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
|
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||||
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
|
||||||
go.balki.me/anyhttp v0.5.2 h1:et4tCDXLeXpWfMNvRKG7ojfrnlr3du7cEaG966MLSpA=
|
|
||||||
go.balki.me/anyhttp v0.5.2/go.mod h1:JhfekOIjgVODoVqUCficjpIgmB3wwlB7jhN0eN2EZ/s=
|
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
|
||||||
go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs=
|
|
||||||
go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts=
|
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw=
|
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ=
|
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
|
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
|
|
||||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
|
||||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0 h1:6VjV6Et+1Hd2iLZEPtdV7vie80Yyqf7oikJLjQ/myi0=
|
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0/go.mod h1:u8hcp8ji5gaM/RfcOo8z9NMnf1pVLfVY7lBY2VOGuUU=
|
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
|
||||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
|
||||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
|
||||||
gocloud.dev v0.44.0 h1:iVyMAqFl2r6xUy7M4mfqwlN+21UpJoEtgHEcfiLMUXs=
|
|
||||||
gocloud.dev v0.44.0/go.mod h1:ZmjROXGdC/eKZLF1N+RujDlFRx3D+4Av2thREKDMVxY=
|
|
||||||
gocloud.dev/pubsub/kafkapubsub v0.44.0 h1:nQvzfnEN6lCh4j2p+1t0OLS4nmC2U/Ji5aWHVwgkifg=
|
|
||||||
gocloud.dev/pubsub/kafkapubsub v0.44.0/go.mod h1:/gcNz6OG4HgcY+w2LXwwY4qaRMgtq+SXoPSQU2jOlcw=
|
|
||||||
gocloud.dev/pubsub/natspubsub v0.44.0 h1:1Us76ckkdgtiE1p1rJZ+38b9TQP051bmjAiQlFQzYrM=
|
|
||||||
gocloud.dev/pubsub/natspubsub v0.44.0/go.mod h1:PvVAGIhL14PWGwWIXX/zAK42ixr2/PKP4Q4yMiAUraQ=
|
|
||||||
gocloud.dev/pubsub/rabbitpubsub v0.44.0 h1:MpRIO6XJ/JTqrlUWt3CxwDe1LvaiXUVu4sS5cv4f/AM=
|
|
||||||
gocloud.dev/pubsub/rabbitpubsub v0.44.0/go.mod h1:BB9+qT3r6g4M5+4asiXaEeqw4QAOzsWusO5krYaqkdA=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
|
||||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
|
||||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
|
||||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
|
||||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
|
|
||||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
|
||||||
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
|
|
||||||
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
|
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
|
||||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
|
||||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|
||||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|
||||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
|
||||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
|
||||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
|
||||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
|
||||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0/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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
|
||||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
|
||||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
|
||||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
|
|
||||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
|
||||||
google.golang.org/api v0.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA=
|
|
||||||
google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4=
|
|
||||||
google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 h1:Nt6z9UHqSlIdIGJdz6KhTIs2VRx/iOsA5iE8bmQNcxs=
|
|
||||||
google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79/go.mod h1:kTmlBHMPqR5uCZPBvwa2B18mvubkjyY3CRLI0c6fj0s=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
|
||||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
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.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-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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
|
||||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||||
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk=
|
||||||
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY=
|
||||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||||
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
modernc.org/sqlite v1.29.2 h1:xgBSyA3gemwgP31PWFfFjtBorQNYpeypGdoSDjXhrgI=
|
||||||
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
modernc.org/sqlite v1.29.2/go.mod h1:hG41jCYxOAOoO6BRK66AdRlmOcDzXf7qnwlwjUIOqa0=
|
||||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||||
modernc.org/libc v1.67.1 h1:bFaqOaa5/zbWYJo8aW0tXPX21hXsngG2M7mckCnFSVk=
|
|
||||||
modernc.org/libc v1.67.1/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
|
|
||||||
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.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
|
||||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
|
||||||
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.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY=
|
|
||||||
modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
|
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -16,24 +15,6 @@ import (
|
|||||||
//go:embed currencies.json
|
//go:embed currencies.json
|
||||||
var defaults []byte
|
var defaults []byte
|
||||||
|
|
||||||
const (
|
|
||||||
MinDecimals = 0
|
|
||||||
MaxDecimals = 18
|
|
||||||
)
|
|
||||||
|
|
||||||
// clampDecimals ensures the decimals value is within a safe range [0, 18]
|
|
||||||
func clampDecimals(decimals int, code string) int {
|
|
||||||
original := decimals
|
|
||||||
if decimals < MinDecimals {
|
|
||||||
decimals = MinDecimals
|
|
||||||
log.Printf("WARNING: Currency %s had negative decimals (%d), normalized to %d", code, original, decimals)
|
|
||||||
} else if decimals > MaxDecimals {
|
|
||||||
decimals = MaxDecimals
|
|
||||||
log.Printf("WARNING: Currency %s had excessive decimals (%d), normalized to %d", code, original, decimals)
|
|
||||||
}
|
|
||||||
return decimals
|
|
||||||
}
|
|
||||||
|
|
||||||
type CollectorFunc func() ([]Currency, error)
|
type CollectorFunc func() ([]Currency, error)
|
||||||
|
|
||||||
func CollectJSON(reader io.Reader) CollectorFunc {
|
func CollectJSON(reader io.Reader) CollectorFunc {
|
||||||
@@ -44,11 +25,6 @@ func CollectJSON(reader io.Reader) CollectorFunc {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clamp decimals during collection to ensure early normalization
|
|
||||||
for i := range currencies {
|
|
||||||
currencies[i].Decimals = clampDecimals(currencies[i].Decimals, currencies[i].Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
return currencies, nil
|
return currencies, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,11 +48,10 @@ func CollectionCurrencies(collectors ...CollectorFunc) ([]Currency, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Currency struct {
|
type Currency struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
Local string `json:"local"`
|
Local string `json:"local"`
|
||||||
Symbol string `json:"symbol"`
|
Symbol string `json:"symbol"`
|
||||||
Decimals int `json:"decimals"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CurrencyRegistry struct {
|
type CurrencyRegistry struct {
|
||||||
@@ -87,10 +62,7 @@ type CurrencyRegistry struct {
|
|||||||
func NewCurrencyService(currencies []Currency) *CurrencyRegistry {
|
func NewCurrencyService(currencies []Currency) *CurrencyRegistry {
|
||||||
registry := make(map[string]Currency, len(currencies))
|
registry := make(map[string]Currency, len(currencies))
|
||||||
for i := range currencies {
|
for i := range currencies {
|
||||||
// Clamp decimals to safe range before adding to registry
|
registry[currencies[i].Code] = currencies[i]
|
||||||
currency := currencies[i]
|
|
||||||
currency.Decimals = clampDecimals(currency.Decimals, currency.Code)
|
|
||||||
registry[currency.Code] = currency
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &CurrencyRegistry{
|
return &CurrencyRegistry{
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ func New(repos *repo.AllRepos, opts ...OptionsFunc) *AllServices {
|
|||||||
repo: repos,
|
repo: repos,
|
||||||
autoIncrementAssetID: options.autoIncrementAssetID,
|
autoIncrementAssetID: options.autoIncrementAssetID,
|
||||||
},
|
},
|
||||||
BackgroundService: &BackgroundService{repos, Latest{}},
|
BackgroundService: &BackgroundService{repos},
|
||||||
Currencies: currencies.NewCurrencyService(options.currencies),
|
Currencies: currencies.NewCurrencyService(options.currencies),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -38,11 +37,10 @@ func bootstrap() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
password := fk.Str(10)
|
|
||||||
tUser, err = tRepos.Users.Create(ctx, repo.UserCreate{
|
tUser, err = tRepos.Users.Create(ctx, repo.UserCreate{
|
||||||
Name: fk.Str(10),
|
Name: fk.Str(10),
|
||||||
Email: fk.Email(),
|
Email: fk.Email(),
|
||||||
Password: &password,
|
Password: fk.Str(10),
|
||||||
IsSuperuser: fk.Bool(),
|
IsSuperuser: fk.Bool(),
|
||||||
GroupID: tGroup.ID,
|
GroupID: tGroup.ID,
|
||||||
})
|
})
|
||||||
@@ -51,8 +49,8 @@ func bootstrap() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func MainNoExit(m *testing.M) int {
|
func TestMain(m *testing.M) {
|
||||||
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 {
|
if err != nil {
|
||||||
log.Fatalf("failed opening connection to sqlite: %v", err)
|
log.Fatalf("failed opening connection to sqlite: %v", err)
|
||||||
}
|
}
|
||||||
@@ -63,19 +61,7 @@ func MainNoExit(m *testing.M) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tClient = client
|
tClient = client
|
||||||
tRepos = repo.New(tClient, tbus, config.Storage{
|
tRepos = repo.New(tClient, tbus, os.TempDir()+"/homebox")
|
||||||
PrefixPath: "/",
|
|
||||||
ConnString: "file://" + os.TempDir(),
|
|
||||||
}, "mem://{{ .Topic }}", config.Thumbnail{
|
|
||||||
Enabled: false,
|
|
||||||
Width: 0,
|
|
||||||
Height: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
err = os.MkdirAll(os.TempDir()+"/homebox", 0o755)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
defaults, _ := currencies.CollectionCurrencies(
|
defaults, _ := currencies.CollectionCurrencies(
|
||||||
currencies.CollectDefaults(),
|
currencies.CollectDefaults(),
|
||||||
@@ -91,9 +77,5 @@ func MainNoExit(m *testing.M) int {
|
|||||||
UID: tUser.ID,
|
UID: tUser.ID,
|
||||||
}
|
}
|
||||||
|
|
||||||
return m.Run()
|
os.Exit(m.Run())
|
||||||
}
|
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
os.Exit(MainNoExit(m))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ type ExportCSVRow struct {
|
|||||||
LabelStr LabelString `csv:"HB.labels"`
|
LabelStr LabelString `csv:"HB.labels"`
|
||||||
AssetID repo.AssetID `csv:"HB.asset_id"`
|
AssetID repo.AssetID `csv:"HB.asset_id"`
|
||||||
Archived bool `csv:"HB.archived"`
|
Archived bool `csv:"HB.archived"`
|
||||||
URL string `csv:"HB.url"`
|
|
||||||
|
|
||||||
Name string `csv:"HB.name"`
|
Name string `csv:"HB.name"`
|
||||||
Quantity int `csv:"HB.quantity"`
|
Quantity int `csv:"HB.quantity"`
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ func (s *IOSheet) Read(data io.Reader) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ReadItems writes the sheet to a writer.
|
// ReadItems writes the sheet to a writer.
|
||||||
func (s *IOSheet) ReadItems(ctx context.Context, items []repo.ItemOut, gid uuid.UUID, repos *repo.AllRepos, hbURL string) error {
|
func (s *IOSheet) ReadItems(ctx context.Context, items []repo.ItemOut, GID uuid.UUID, repos *repo.AllRepos) error {
|
||||||
s.Rows = make([]ExportCSVRow, len(items))
|
s.Rows = make([]ExportCSVRow, len(items))
|
||||||
|
|
||||||
extraHeaders := map[string]struct{}{}
|
extraHeaders := map[string]struct{}{}
|
||||||
@@ -164,7 +164,7 @@ func (s *IOSheet) ReadItems(ctx context.Context, items []repo.ItemOut, gid uuid.
|
|||||||
// TODO: Support fetching nested locations
|
// TODO: Support fetching nested locations
|
||||||
locID := item.Location.ID
|
locID := item.Location.ID
|
||||||
|
|
||||||
locPaths, err := repos.Locations.PathForLoc(context.Background(), gid, locID)
|
locPaths, err := repos.Locations.PathForLoc(context.Background(), GID, locID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("could not get location path")
|
log.Error().Err(err).Msg("could not get location path")
|
||||||
return err
|
return err
|
||||||
@@ -178,8 +178,6 @@ func (s *IOSheet) ReadItems(ctx context.Context, items []repo.ItemOut, gid uuid.
|
|||||||
labelString[i] = l.Name
|
labelString[i] = l.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
url := generateItemURL(item, hbURL)
|
|
||||||
|
|
||||||
customFields := make([]ExportItemFields, len(item.Fields))
|
customFields := make([]ExportItemFields, len(item.Fields))
|
||||||
|
|
||||||
for i, f := range item.Fields {
|
for i, f := range item.Fields {
|
||||||
@@ -203,11 +201,11 @@ func (s *IOSheet) ReadItems(ctx context.Context, items []repo.ItemOut, gid uuid.
|
|||||||
Description: item.Description,
|
Description: item.Description,
|
||||||
Insured: item.Insured,
|
Insured: item.Insured,
|
||||||
Archived: item.Archived,
|
Archived: item.Archived,
|
||||||
URL: url,
|
|
||||||
|
|
||||||
PurchasePrice: item.PurchasePrice,
|
PurchasePrice: item.PurchasePrice,
|
||||||
PurchaseFrom: item.PurchaseFrom,
|
PurchaseMethod: item.PurchaseMethod,
|
||||||
PurchaseTime: item.PurchaseTime,
|
PurchaseFrom: item.PurchaseFrom,
|
||||||
|
PurchaseTime: item.PurchaseTime,
|
||||||
|
|
||||||
Manufacturer: item.Manufacturer,
|
Manufacturer: item.Manufacturer,
|
||||||
ModelNumber: item.ModelNumber,
|
ModelNumber: item.ModelNumber,
|
||||||
@@ -222,7 +220,6 @@ func (s *IOSheet) ReadItems(ctx context.Context, items []repo.ItemOut, gid uuid.
|
|||||||
SoldPrice: item.SoldPrice,
|
SoldPrice: item.SoldPrice,
|
||||||
SoldNotes: item.SoldNotes,
|
SoldNotes: item.SoldNotes,
|
||||||
|
|
||||||
Notes: item.Notes,
|
|
||||||
Fields: customFields,
|
Fields: customFields,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,14 +253,6 @@ func (s *IOSheet) ReadItems(ctx context.Context, items []repo.ItemOut, gid uuid.
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateItemURL(item repo.ItemOut, d string) string {
|
|
||||||
url := ""
|
|
||||||
if item.ID != uuid.Nil {
|
|
||||||
url = fmt.Sprintf("%s/item/%s", d, item.ID.String())
|
|
||||||
}
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
// CSV writes the current sheet to a 2d array, for compatibility with TSV/CSV files.
|
// CSV writes the current sheet to a 2d array, for compatibility with TSV/CSV files.
|
||||||
func (s *IOSheet) CSV() ([][]string, error) {
|
func (s *IOSheet) CSV() ([][]string, error) {
|
||||||
memcsv := make([][]string, len(s.Rows)+1)
|
memcsv := make([][]string, len(s.Rows)+1)
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -14,13 +11,8 @@ import (
|
|||||||
"github.com/sysadminsmedia/homebox/backend/internal/data/types"
|
"github.com/sysadminsmedia/homebox/backend/internal/data/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Latest struct {
|
|
||||||
Version string `json:"version"`
|
|
||||||
Date string `json:"date"`
|
|
||||||
}
|
|
||||||
type BackgroundService struct {
|
type BackgroundService struct {
|
||||||
repos *repo.AllRepos
|
repos *repo.AllRepos
|
||||||
latest Latest
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *BackgroundService) SendNotifiersToday(ctx context.Context) error {
|
func (svc *BackgroundService) SendNotifiersToday(ctx context.Context) error {
|
||||||
@@ -87,52 +79,3 @@ func (svc *BackgroundService) SendNotifiersToday(ctx context.Context) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *BackgroundService) GetLatestGithubRelease(ctx context.Context) error {
|
|
||||||
url := "https://api.github.com/repos/sysadminsmedia/homebox/releases/latest"
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create latest version request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -38,17 +38,13 @@ func (svc *ItemService) Create(ctx Context, item repo.ItemCreate) (repo.ItemOut,
|
|||||||
return svc.repo.Items.Create(ctx, ctx.GID, item)
|
return svc.repo.Items.Create(ctx, ctx.GID, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *ItemService) Duplicate(ctx Context, gid, id uuid.UUID, options repo.DuplicateOptions) (repo.ItemOut, error) {
|
func (svc *ItemService) EnsureAssetID(ctx context.Context, GID uuid.UUID) (int, error) {
|
||||||
return svc.repo.Items.Duplicate(ctx, gid, id, options)
|
items, err := svc.repo.Items.GetAllZeroAssetID(ctx, GID)
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *ItemService) EnsureAssetID(ctx context.Context, gid uuid.UUID) (int, error) {
|
|
||||||
items, err := svc.repo.Items.GetAllZeroAssetID(ctx, gid)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
highest, err := svc.repo.Items.GetHighestAssetID(ctx, gid)
|
highest, err := svc.repo.Items.GetHighestAssetID(ctx, GID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -57,7 +53,7 @@ func (svc *ItemService) EnsureAssetID(ctx context.Context, gid uuid.UUID) (int,
|
|||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
highest++
|
highest++
|
||||||
|
|
||||||
err = svc.repo.Items.SetAssetID(ctx, gid, item.ID, highest)
|
err = svc.repo.Items.SetAssetID(ctx, GID, item.ID, highest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -68,8 +64,8 @@ func (svc *ItemService) EnsureAssetID(ctx context.Context, gid uuid.UUID) (int,
|
|||||||
return finished, nil
|
return finished, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *ItemService) EnsureImportRef(ctx context.Context, gid uuid.UUID) (int, error) {
|
func (svc *ItemService) EnsureImportRef(ctx context.Context, GID uuid.UUID) (int, error) {
|
||||||
ids, err := svc.repo.Items.GetAllZeroImportRef(ctx, gid)
|
ids, err := svc.repo.Items.GetAllZeroImportRef(ctx, GID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -78,7 +74,7 @@ func (svc *ItemService) EnsureImportRef(ctx context.Context, gid uuid.UUID) (int
|
|||||||
for _, itemID := range ids {
|
for _, itemID := range ids {
|
||||||
ref := uuid.New().String()[0:8]
|
ref := uuid.New().String()[0:8]
|
||||||
|
|
||||||
err = svc.repo.Items.Patch(ctx, gid, itemID, repo.ItemPatch{ImportRef: &ref})
|
err = svc.repo.Items.Patch(ctx, GID, itemID, repo.ItemPatch{ImportRef: &ref})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -100,7 +96,7 @@ func serializeLocation[T ~[]string](location T) string {
|
|||||||
// 1. If the item does not exist, it is created.
|
// 1. If the item does not exist, it is created.
|
||||||
// 2. If the item has a ImportRef and it exists it is skipped
|
// 2. If the item has a ImportRef and it exists it is skipped
|
||||||
// 3. Locations and Labels are created if they do not exist.
|
// 3. Locations and Labels are created if they do not exist.
|
||||||
func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data io.Reader) (int, error) {
|
func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data io.Reader) (int, error) {
|
||||||
sheet := reporting.IOSheet{}
|
sheet := reporting.IOSheet{}
|
||||||
|
|
||||||
err := sheet.Read(data)
|
err := sheet.Read(data)
|
||||||
@@ -113,7 +109,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data io.Re
|
|||||||
|
|
||||||
labelMap := make(map[string]uuid.UUID)
|
labelMap := make(map[string]uuid.UUID)
|
||||||
{
|
{
|
||||||
labels, err := svc.repo.Labels.GetAll(ctx, gid)
|
labels, err := svc.repo.Labels.GetAll(ctx, GID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -128,7 +124,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data io.Re
|
|||||||
|
|
||||||
locationMap := make(map[string]uuid.UUID)
|
locationMap := make(map[string]uuid.UUID)
|
||||||
{
|
{
|
||||||
locations, err := svc.repo.Locations.Tree(ctx, gid, repo.TreeQuery{WithItems: false})
|
locations, err := svc.repo.Locations.Tree(ctx, GID, repo.TreeQuery{WithItems: false})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -157,7 +153,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data io.Re
|
|||||||
// Asset ID Pre-Check
|
// Asset ID Pre-Check
|
||||||
highestAID := repo.AssetID(-1)
|
highestAID := repo.AssetID(-1)
|
||||||
if svc.autoIncrementAssetID {
|
if svc.autoIncrementAssetID {
|
||||||
highestAID, err = svc.repo.Items.GetHighestAssetID(ctx, gid)
|
highestAID, err = svc.repo.Items.GetHighestAssetID(ctx, GID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -173,7 +169,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data io.Re
|
|||||||
// ========================================
|
// ========================================
|
||||||
// Preflight check for existing item
|
// Preflight check for existing item
|
||||||
if row.ImportRef != "" {
|
if row.ImportRef != "" {
|
||||||
exists, err := svc.repo.Items.CheckRef(ctx, gid, row.ImportRef)
|
exists, err := svc.repo.Items.CheckRef(ctx, GID, row.ImportRef)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("error checking for existing item with ref %q: %w", row.ImportRef, err)
|
return 0, fmt.Errorf("error checking for existing item with ref %q: %w", row.ImportRef, err)
|
||||||
}
|
}
|
||||||
@@ -192,7 +188,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data io.Re
|
|||||||
|
|
||||||
id, ok := labelMap[label]
|
id, ok := labelMap[label]
|
||||||
if !ok {
|
if !ok {
|
||||||
newLabel, err := svc.repo.Labels.Create(ctx, gid, repo.LabelCreate{Name: label})
|
newLabel, err := svc.repo.Labels.Create(ctx, GID, repo.LabelCreate{Name: label})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -224,7 +220,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data io.Re
|
|||||||
parentID = locationMap[parentPath]
|
parentID = locationMap[parentPath]
|
||||||
}
|
}
|
||||||
|
|
||||||
newLocation, err := svc.repo.Locations.Create(ctx, gid, repo.LocationCreate{
|
newLocation, err := svc.repo.Locations.Create(ctx, GID, repo.LocationCreate{
|
||||||
ParentID: parentID,
|
ParentID: parentID,
|
||||||
Name: pathElement,
|
Name: pathElement,
|
||||||
})
|
})
|
||||||
@@ -265,12 +261,12 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data io.Re
|
|||||||
LabelIDs: labelIds,
|
LabelIDs: labelIds,
|
||||||
}
|
}
|
||||||
|
|
||||||
item, err = svc.repo.Items.Create(ctx, gid, newItem)
|
item, err = svc.repo.Items.Create(ctx, GID, newItem)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
item, err = svc.repo.Items.GetByRef(ctx, gid, row.ImportRef)
|
item, err = svc.repo.Items.GetByRef(ctx, GID, row.ImportRef)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -302,6 +298,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data io.Re
|
|||||||
Archived: row.Archived,
|
Archived: row.Archived,
|
||||||
|
|
||||||
PurchasePrice: row.PurchasePrice,
|
PurchasePrice: row.PurchasePrice,
|
||||||
|
PurchaseFrom: row.PurchaseMethod,
|
||||||
PurchaseFrom: row.PurchaseFrom,
|
PurchaseFrom: row.PurchaseFrom,
|
||||||
PurchaseTime: row.PurchaseTime,
|
PurchaseTime: row.PurchaseTime,
|
||||||
|
|
||||||
@@ -322,7 +319,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data io.Re
|
|||||||
Fields: fields,
|
Fields: fields,
|
||||||
}
|
}
|
||||||
|
|
||||||
item, err = svc.repo.Items.UpdateByGroup(ctx, gid, updateItem)
|
item, err = svc.repo.Items.UpdateByGroup(ctx, GID, updateItem)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -333,15 +330,15 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data io.Re
|
|||||||
return finished, nil
|
return finished, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *ItemService) ExportCSV(ctx context.Context, gid uuid.UUID, hbURL string) ([][]string, error) {
|
func (svc *ItemService) ExportCSV(ctx context.Context, GID uuid.UUID) ([][]string, error) {
|
||||||
items, err := svc.repo.Items.GetAll(ctx, gid)
|
items, err := svc.repo.Items.GetAll(ctx, GID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sheet := reporting.IOSheet{}
|
sheet := reporting.IOSheet{}
|
||||||
|
|
||||||
err = sheet.ReadItems(ctx, items, gid, svc.repo, hbURL)
|
err = sheet.ReadItems(ctx, items, GID, svc.repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -349,8 +346,8 @@ func (svc *ItemService) ExportCSV(ctx context.Context, gid uuid.UUID, hbURL stri
|
|||||||
return sheet.CSV()
|
return sheet.CSV()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *ItemService) ExportBillOfMaterialsCSV(ctx context.Context, gid uuid.UUID) ([]byte, error) {
|
func (svc *ItemService) ExportBillOfMaterialsCSV(ctx context.Context, GID uuid.UUID) ([]byte, error) {
|
||||||
items, err := svc.repo.Items.GetAll(ctx, gid)
|
items, err := svc.repo.Items.GetAll(ctx, GID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,33 +2,35 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent"
|
"github.com/sysadminsmedia/homebox/backend/internal/data/ent"
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/attachment"
|
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/attachment"
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
|
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
|
||||||
"io"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (svc *ItemService) AttachmentPath(ctx context.Context, gid uuid.UUID, attachmentID uuid.UUID) (*ent.Attachment, error) {
|
func (svc *ItemService) AttachmentPath(ctx context.Context, attachmentID uuid.UUID) (*ent.Document, error) {
|
||||||
attachment, err := svc.repo.Attachments.Get(ctx, gid, attachmentID)
|
attachment, err := svc.repo.Attachments.Get(ctx, attachmentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return attachment, nil
|
return attachment.Edges.Document, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *ItemService) AttachmentUpdate(ctx Context, gid uuid.UUID, itemID uuid.UUID, data *repo.ItemAttachmentUpdate) (repo.ItemOut, error) {
|
func (svc *ItemService) AttachmentUpdate(ctx Context, itemID uuid.UUID, data *repo.ItemAttachmentUpdate) (repo.ItemOut, error) {
|
||||||
// Update Attachment
|
// Update Attachment
|
||||||
attachment, err := svc.repo.Attachments.Update(ctx, gid, data.ID, data)
|
attachment, err := svc.repo.Attachments.Update(ctx, data.ID, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return repo.ItemOut{}, err
|
return repo.ItemOut{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update Document
|
// Update Document
|
||||||
attDoc := attachment
|
attDoc := attachment.Edges.Document
|
||||||
_, err = svc.repo.Attachments.Rename(ctx, gid, attDoc.ID, data.Title)
|
_, err = svc.repo.Docs.Rename(ctx, attDoc.ID, data.Title)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return repo.ItemOut{}, err
|
return repo.ItemOut{}, err
|
||||||
}
|
}
|
||||||
@@ -39,15 +41,22 @@ func (svc *ItemService) AttachmentUpdate(ctx Context, gid uuid.UUID, itemID uuid
|
|||||||
// AttachmentAdd adds an attachment to an item by creating an entry in the Documents table and linking it to the Attachment
|
// AttachmentAdd adds an attachment to an item by creating an entry in the Documents table and linking it to the Attachment
|
||||||
// Table and Items table. The file provided via the reader is stored on the file system based on the provided
|
// Table and Items table. The file provided via the reader is stored on the file system based on the provided
|
||||||
// relative path during construction of the service.
|
// relative path during construction of the service.
|
||||||
func (svc *ItemService) AttachmentAdd(ctx Context, itemID uuid.UUID, filename string, attachmentType attachment.Type, primary bool, file io.Reader) (repo.ItemOut, error) {
|
func (svc *ItemService) AttachmentAdd(ctx Context, itemID uuid.UUID, filename string, attachmentType attachment.Type, file io.Reader) (repo.ItemOut, error) {
|
||||||
// Get the Item
|
// Get the Item
|
||||||
_, err := svc.repo.Items.GetOneByGroup(ctx, ctx.GID, itemID)
|
_, err := svc.repo.Items.GetOneByGroup(ctx, ctx.GID, itemID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return repo.ItemOut{}, err
|
return repo.ItemOut{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create the document
|
||||||
|
doc, err := svc.repo.Docs.Create(ctx, ctx.GID, repo.DocumentCreate{Title: filename, Content: file})
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("failed to create document")
|
||||||
|
return repo.ItemOut{}, err
|
||||||
|
}
|
||||||
|
|
||||||
// Create the attachment
|
// Create the attachment
|
||||||
_, err = svc.repo.Attachments.Create(ctx, itemID, repo.ItemCreateAttachment{Title: filename, Content: file}, attachmentType, primary)
|
_, err = svc.repo.Attachments.Create(ctx, itemID, doc.ID, attachmentType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("failed to create attachment")
|
log.Err(err).Msg("failed to create attachment")
|
||||||
return repo.ItemOut{}, err
|
return repo.ItemOut{}, err
|
||||||
@@ -56,12 +65,26 @@ func (svc *ItemService) AttachmentAdd(ctx Context, itemID uuid.UUID, filename st
|
|||||||
return svc.repo.Items.GetOneByGroup(ctx, ctx.GID, itemID)
|
return svc.repo.Items.GetOneByGroup(ctx, ctx.GID, itemID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *ItemService) AttachmentDelete(ctx context.Context, gid uuid.UUID, id uuid.UUID, attachmentID uuid.UUID) error {
|
func (svc *ItemService) AttachmentDelete(ctx context.Context, gid, itemID, attachmentID uuid.UUID) error {
|
||||||
// Delete the attachment
|
// Get the Item
|
||||||
err := svc.repo.Attachments.Delete(ctx, gid, id, attachmentID)
|
_, err := svc.repo.Items.GetOneByGroup(ctx, gid, itemID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attachment, err := svc.repo.Attachments.Get(ctx, attachmentID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the attachment
|
||||||
|
err = svc.repo.Attachments.Delete(ctx, attachmentID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove File
|
||||||
|
err = os.Remove(attachment.Edges.Document.Path)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
|
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestItemService_AddAttachment(t *testing.T) {
|
func TestItemService_AddAttachment(t *testing.T) {
|
||||||
@@ -46,68 +45,18 @@ func TestItemService_AddAttachment(t *testing.T) {
|
|||||||
reader := strings.NewReader(contents)
|
reader := strings.NewReader(contents)
|
||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
afterAttachment, err := svc.AttachmentAdd(tCtx, itm.ID, "testfile.txt", "attachment", false, reader)
|
afterAttachment, err := svc.AttachmentAdd(tCtx, itm.ID, "testfile.txt", "attachment", reader)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotNil(t, afterAttachment)
|
assert.NotNil(t, afterAttachment)
|
||||||
|
|
||||||
// Check that the file exists
|
// Check that the file exists
|
||||||
storedPath := afterAttachment.Attachments[0].Path
|
storedPath := afterAttachment.Attachments[0].Document.Path
|
||||||
|
|
||||||
// path should now be relative: {group}/{documents}
|
// {root}/{group}/{item}/{attachment}
|
||||||
assert.Equal(t, path.Join(tGroup.ID.String(), "documents"), path.Dir(storedPath))
|
assert.Equal(t, path.Join(temp, "homebox", tGroup.ID.String(), "documents"), path.Dir(storedPath))
|
||||||
|
|
||||||
// Check that the file contents are correct
|
// Check that the file contents are correct
|
||||||
bts, err := os.ReadFile(path.Join(os.TempDir(), storedPath))
|
bts, err := os.ReadFile(storedPath)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, contents, string(bts))
|
assert.Equal(t, contents, string(bts))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestItemService_AddAttachment_InvalidStorage(t *testing.T) {
|
|
||||||
// Create a service with an invalid storage path to simulate the issue
|
|
||||||
svc := &ItemService{
|
|
||||||
repo: tRepos,
|
|
||||||
filepath: "/nonexistent/path/that/should/not/exist",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a temporary repo with invalid storage config
|
|
||||||
invalidRepos := repo.New(tClient, tbus, config.Storage{
|
|
||||||
PrefixPath: "/",
|
|
||||||
ConnString: "file:///nonexistent/directory/that/does/not/exist",
|
|
||||||
}, "mem://{{ .Topic }}", config.Thumbnail{
|
|
||||||
Enabled: false,
|
|
||||||
Width: 0,
|
|
||||||
Height: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
svc.repo = invalidRepos
|
|
||||||
|
|
||||||
loc, err := invalidRepos.Locations.Create(context.Background(), tGroup.ID, repo.LocationCreate{
|
|
||||||
Description: "test",
|
|
||||||
Name: "test-invalid",
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotNil(t, loc)
|
|
||||||
|
|
||||||
itmC := repo.ItemCreate{
|
|
||||||
Name: fk.Str(10),
|
|
||||||
Description: fk.Str(10),
|
|
||||||
LocationID: loc.ID,
|
|
||||||
}
|
|
||||||
|
|
||||||
itm, err := invalidRepos.Items.Create(context.Background(), tGroup.ID, itmC)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotNil(t, itm)
|
|
||||||
t.Cleanup(func() {
|
|
||||||
err := invalidRepos.Items.Delete(context.Background(), itm.ID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
contents := fk.Str(1000)
|
|
||||||
reader := strings.NewReader(contents)
|
|
||||||
|
|
||||||
// Attempt to add attachment with invalid storage - should return an error
|
|
||||||
_, err = svc.AttachmentAdd(tCtx, itm.ID, "testfile.txt", "attachment", false, reader)
|
|
||||||
|
|
||||||
// This should return an error now (after the fix)
|
|
||||||
assert.Error(t, err, "AttachmentAdd should return an error when storage is invalid")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,21 +3,20 @@ package services
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent"
|
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/authroles"
|
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/authroles"
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
|
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
|
||||||
"github.com/sysadminsmedia/homebox/backend/pkgs/hasher"
|
"github.com/sysadminsmedia/homebox/backend/pkgs/hasher"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
oneWeek = time.Hour * 24 * 7
|
oneWeek = time.Hour * 24 * 7
|
||||||
ErrorInvalidLogin = errors.New("invalid username or password")
|
ErrorInvalidLogin = errors.New("invalid username or password")
|
||||||
ErrorInvalidToken = errors.New("invalid token")
|
ErrorInvalidToken = errors.New("invalid token")
|
||||||
|
ErrorTokenIDMismatch = errors.New("token id mismatch")
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserService struct {
|
type UserService struct {
|
||||||
@@ -83,7 +82,7 @@ func (svc *UserService) RegisterUser(ctx context.Context, data UserRegistration)
|
|||||||
usrCreate := repo.UserCreate{
|
usrCreate := repo.UserCreate{
|
||||||
Name: data.Name,
|
Name: data.Name,
|
||||||
Email: data.Email,
|
Email: data.Email,
|
||||||
Password: &hashed,
|
Password: hashed,
|
||||||
IsSuperuser: false,
|
IsSuperuser: false,
|
||||||
GroupID: group.ID,
|
GroupID: group.ID,
|
||||||
IsOwner: creatingGroup,
|
IsOwner: creatingGroup,
|
||||||
@@ -133,13 +132,13 @@ func (svc *UserService) GetSelf(ctx context.Context, requestToken string) (repo.
|
|||||||
return svc.repos.AuthTokens.GetUserFromToken(ctx, hash)
|
return svc.repos.AuthTokens.GetUserFromToken(ctx, hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *UserService) UpdateSelf(ctx context.Context, id uuid.UUID, data repo.UserUpdate) (repo.UserOut, error) {
|
func (svc *UserService) UpdateSelf(ctx context.Context, ID uuid.UUID, data repo.UserUpdate) (repo.UserOut, error) {
|
||||||
err := svc.repos.Users.Update(ctx, id, data)
|
err := svc.repos.Users.Update(ctx, ID, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return repo.UserOut{}, err
|
return repo.UserOut{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return svc.repos.Users.GetOneID(ctx, id)
|
return svc.repos.Users.GetOneID(ctx, ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -191,134 +190,13 @@ func (svc *UserService) Login(ctx context.Context, username, password string, ex
|
|||||||
return UserAuthTokenDetail{}, ErrorInvalidLogin
|
return UserAuthTokenDetail{}, ErrorInvalidLogin
|
||||||
}
|
}
|
||||||
|
|
||||||
// SECURITY: Deny login for users with null or empty password (OIDC users)
|
if !hasher.CheckPasswordHash(password, usr.PasswordHash) {
|
||||||
if usr.PasswordHash == "" {
|
|
||||||
log.Warn().Str("email", username).Msg("Login attempt blocked for user with null password (likely OIDC user)")
|
|
||||||
// SECURITY: Perform hash to ensure response times are the same
|
|
||||||
hasher.CheckPasswordHash("not-a-real-password", "not-a-real-password")
|
|
||||||
return UserAuthTokenDetail{}, ErrorInvalidLogin
|
return UserAuthTokenDetail{}, ErrorInvalidLogin
|
||||||
}
|
}
|
||||||
|
|
||||||
check, rehash := hasher.CheckPasswordHash(password, usr.PasswordHash)
|
|
||||||
|
|
||||||
if !check {
|
|
||||||
return UserAuthTokenDetail{}, ErrorInvalidLogin
|
|
||||||
}
|
|
||||||
|
|
||||||
if rehash {
|
|
||||||
hash, err := hasher.HashPassword(password)
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("Failed to hash password")
|
|
||||||
return UserAuthTokenDetail{}, err
|
|
||||||
}
|
|
||||||
err = svc.repos.Users.ChangePassword(ctx, usr.ID, hash)
|
|
||||||
if err != nil {
|
|
||||||
return UserAuthTokenDetail{}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return svc.createSessionToken(ctx, usr.ID, extendedSession)
|
return svc.createSessionToken(ctx, usr.ID, extendedSession)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoginOIDC creates a session token for a user authenticated via OIDC.
|
|
||||||
// It now uses issuer + subject for identity association (OIDC spec compliance).
|
|
||||||
// If the user doesn't exist, it will create one.
|
|
||||||
func (svc *UserService) LoginOIDC(ctx context.Context, issuer, subject, email, name string) (UserAuthTokenDetail, error) {
|
|
||||||
issuer = strings.TrimSpace(issuer)
|
|
||||||
subject = strings.TrimSpace(subject)
|
|
||||||
email = strings.ToLower(strings.TrimSpace(email))
|
|
||||||
name = strings.TrimSpace(name)
|
|
||||||
|
|
||||||
if issuer == "" || subject == "" {
|
|
||||||
log.Warn().Str("issuer", issuer).Str("subject", subject).Msg("OIDC login missing issuer or subject")
|
|
||||||
return UserAuthTokenDetail{}, ErrorInvalidLogin
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to get existing user by OIDC identity
|
|
||||||
usr, err := svc.repos.Users.GetOneOIDC(ctx, issuer, subject)
|
|
||||||
if err != nil {
|
|
||||||
if !ent.IsNotFound(err) {
|
|
||||||
log.Err(err).Str("issuer", issuer).Str("subject", subject).Msg("failed to lookup user by OIDC identity")
|
|
||||||
return UserAuthTokenDetail{}, err
|
|
||||||
}
|
|
||||||
// Not found: attempt migration path by email (legacy) if email provided
|
|
||||||
if email != "" {
|
|
||||||
legacyUsr, lerr := svc.repos.Users.GetOneEmail(ctx, email)
|
|
||||||
if lerr == nil {
|
|
||||||
log.Info().Str("email", email).Str("issuer", issuer).Str("subject", subject).Msg("migrating legacy email-based OIDC user to issuer+subject")
|
|
||||||
// Update user with OIDC identity fields
|
|
||||||
if uerr := svc.repos.Users.SetOIDCIdentity(ctx, legacyUsr.ID, issuer, subject); uerr == nil {
|
|
||||||
usr = legacyUsr
|
|
||||||
} else {
|
|
||||||
log.Err(uerr).Str("email", email).Msg("failed to set OIDC identity on legacy user")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create user if still not resolved
|
|
||||||
if usr.ID == uuid.Nil {
|
|
||||||
log.Debug().Str("issuer", issuer).Str("subject", subject).Msg("OIDC user not found, creating new user")
|
|
||||||
usr, err = svc.registerOIDCUser(ctx, issuer, subject, email, name)
|
|
||||||
if err != nil {
|
|
||||||
if ent.IsConstraintError(err) {
|
|
||||||
if usr2, gerr := svc.repos.Users.GetOneOIDC(ctx, issuer, subject); gerr == nil {
|
|
||||||
log.Info().Str("issuer", issuer).Str("subject", subject).Msg("OIDC user created concurrently; proceeding")
|
|
||||||
usr = usr2
|
|
||||||
} else {
|
|
||||||
log.Err(gerr).Str("issuer", issuer).Str("subject", subject).Msg("failed to fetch user after constraint error")
|
|
||||||
return UserAuthTokenDetail{}, gerr
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Err(err).Str("issuer", issuer).Str("subject", subject).Msg("failed to create OIDC user")
|
|
||||||
return UserAuthTokenDetail{}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return svc.createSessionToken(ctx, usr.ID, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// registerOIDCUser creates a new user for OIDC authentication with issuer+subject identity.
|
|
||||||
func (svc *UserService) registerOIDCUser(ctx context.Context, issuer, subject, email, name string) (repo.UserOut, error) {
|
|
||||||
group, err := svc.repos.Groups.GroupCreate(ctx, "Home")
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("Failed to create group for OIDC user")
|
|
||||||
return repo.UserOut{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
usrCreate := repo.UserCreate{
|
|
||||||
Name: name,
|
|
||||||
Email: email,
|
|
||||||
Password: nil,
|
|
||||||
IsSuperuser: false,
|
|
||||||
GroupID: group.ID,
|
|
||||||
IsOwner: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
entUser, err := svc.repos.Users.CreateWithOIDC(ctx, usrCreate, issuer, subject)
|
|
||||||
if err != nil {
|
|
||||||
return repo.UserOut{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Str("issuer", issuer).Str("subject", subject).Msg("creating default labels for OIDC user")
|
|
||||||
for _, label := range defaultLabels() {
|
|
||||||
_, err := svc.repos.Labels.Create(ctx, group.ID, label)
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("Failed to create default label")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Str("issuer", issuer).Str("subject", subject).Msg("creating default locations for OIDC user")
|
|
||||||
for _, location := range defaultLocations() {
|
|
||||||
_, err := svc.repos.Locations.Create(ctx, group.ID, location)
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("Failed to create default location")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return entUser, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *UserService) Logout(ctx context.Context, token string) error {
|
func (svc *UserService) Logout(ctx context.Context, token string) error {
|
||||||
hash := hasher.HashToken(token)
|
hash := hasher.HashToken(token)
|
||||||
err := svc.repos.AuthTokens.DeleteToken(ctx, hash)
|
err := svc.repos.AuthTokens.DeleteToken(ctx, hash)
|
||||||
@@ -339,8 +217,8 @@ func (svc *UserService) RenewToken(ctx context.Context, token string) (UserAuthT
|
|||||||
// DeleteSelf deletes the user that is currently logged based of the provided UUID
|
// DeleteSelf deletes the user that is currently logged based of the provided UUID
|
||||||
// There is _NO_ protection against deleting the wrong user, as such this should only
|
// There is _NO_ protection against deleting the wrong user, as such this should only
|
||||||
// be used when the identify of the user has been confirmed.
|
// be used when the identify of the user has been confirmed.
|
||||||
func (svc *UserService) DeleteSelf(ctx context.Context, id uuid.UUID) error {
|
func (svc *UserService) DeleteSelf(ctx context.Context, ID uuid.UUID) error {
|
||||||
return svc.repos.Users.Delete(ctx, id)
|
return svc.repos.Users.Delete(ctx, ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *UserService) ChangePassword(ctx Context, current string, new string) (ok bool) {
|
func (svc *UserService) ChangePassword(ctx Context, current string, new string) (ok bool) {
|
||||||
@@ -349,8 +227,7 @@ func (svc *UserService) ChangePassword(ctx Context, current string, new string)
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
match, _ := hasher.CheckPasswordHash(current, usr.PasswordHash)
|
if !hasher.CheckPasswordHash(current, usr.PasswordHash) {
|
||||||
if !match {
|
|
||||||
log.Err(errors.New("current password is incorrect")).Msg("Failed to change password")
|
log.Err(errors.New("current password is incorrect")).Msg("Failed to change password")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"entgo.io/ent/dialect/sql"
|
"entgo.io/ent/dialect/sql"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/attachment"
|
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/attachment"
|
||||||
|
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/document"
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/item"
|
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/item"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,16 +28,10 @@ type Attachment struct {
|
|||||||
Type attachment.Type `json:"type,omitempty"`
|
Type attachment.Type `json:"type,omitempty"`
|
||||||
// Primary holds the value of the "primary" field.
|
// Primary holds the value of the "primary" field.
|
||||||
Primary bool `json:"primary,omitempty"`
|
Primary bool `json:"primary,omitempty"`
|
||||||
// Title holds the value of the "title" field.
|
|
||||||
Title string `json:"title,omitempty"`
|
|
||||||
// Path holds the value of the "path" field.
|
|
||||||
Path string `json:"path,omitempty"`
|
|
||||||
// MimeType holds the value of the "mime_type" field.
|
|
||||||
MimeType string `json:"mime_type,omitempty"`
|
|
||||||
// Edges holds the relations/edges for other nodes in the graph.
|
// Edges holds the relations/edges for other nodes in the graph.
|
||||||
// The values are being populated by the AttachmentQuery when eager-loading is set.
|
// The values are being populated by the AttachmentQuery when eager-loading is set.
|
||||||
Edges AttachmentEdges `json:"edges"`
|
Edges AttachmentEdges `json:"edges"`
|
||||||
attachment_thumbnail *uuid.UUID
|
document_attachments *uuid.UUID
|
||||||
item_attachments *uuid.UUID
|
item_attachments *uuid.UUID
|
||||||
selectValues sql.SelectValues
|
selectValues sql.SelectValues
|
||||||
}
|
}
|
||||||
@@ -45,8 +40,8 @@ type Attachment struct {
|
|||||||
type AttachmentEdges struct {
|
type AttachmentEdges struct {
|
||||||
// Item holds the value of the item edge.
|
// Item holds the value of the item edge.
|
||||||
Item *Item `json:"item,omitempty"`
|
Item *Item `json:"item,omitempty"`
|
||||||
// Thumbnail holds the value of the thumbnail edge.
|
// Document holds the value of the document edge.
|
||||||
Thumbnail *Attachment `json:"thumbnail,omitempty"`
|
Document *Document `json:"document,omitempty"`
|
||||||
// loadedTypes holds the information for reporting if a
|
// loadedTypes holds the information for reporting if a
|
||||||
// type was loaded (or requested) in eager-loading or not.
|
// type was loaded (or requested) in eager-loading or not.
|
||||||
loadedTypes [2]bool
|
loadedTypes [2]bool
|
||||||
@@ -55,23 +50,27 @@ type AttachmentEdges struct {
|
|||||||
// ItemOrErr returns the Item value or an error if the edge
|
// ItemOrErr returns the Item value or an error if the edge
|
||||||
// was not loaded in eager-loading, or loaded but was not found.
|
// was not loaded in eager-loading, or loaded but was not found.
|
||||||
func (e AttachmentEdges) ItemOrErr() (*Item, error) {
|
func (e AttachmentEdges) ItemOrErr() (*Item, error) {
|
||||||
if e.Item != nil {
|
if e.loadedTypes[0] {
|
||||||
|
if e.Item == nil {
|
||||||
|
// Edge was loaded but was not found.
|
||||||
|
return nil, &NotFoundError{label: item.Label}
|
||||||
|
}
|
||||||
return e.Item, nil
|
return e.Item, nil
|
||||||
} else if e.loadedTypes[0] {
|
|
||||||
return nil, &NotFoundError{label: item.Label}
|
|
||||||
}
|
}
|
||||||
return nil, &NotLoadedError{edge: "item"}
|
return nil, &NotLoadedError{edge: "item"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ThumbnailOrErr returns the Thumbnail value or an error if the edge
|
// DocumentOrErr returns the Document value or an error if the edge
|
||||||
// was not loaded in eager-loading, or loaded but was not found.
|
// was not loaded in eager-loading, or loaded but was not found.
|
||||||
func (e AttachmentEdges) ThumbnailOrErr() (*Attachment, error) {
|
func (e AttachmentEdges) DocumentOrErr() (*Document, error) {
|
||||||
if e.Thumbnail != nil {
|
if e.loadedTypes[1] {
|
||||||
return e.Thumbnail, nil
|
if e.Document == nil {
|
||||||
} else if e.loadedTypes[1] {
|
// Edge was loaded but was not found.
|
||||||
return nil, &NotFoundError{label: attachment.Label}
|
return nil, &NotFoundError{label: document.Label}
|
||||||
|
}
|
||||||
|
return e.Document, nil
|
||||||
}
|
}
|
||||||
return nil, &NotLoadedError{edge: "thumbnail"}
|
return nil, &NotLoadedError{edge: "document"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// scanValues returns the types for scanning values from sql.Rows.
|
// scanValues returns the types for scanning values from sql.Rows.
|
||||||
@@ -81,13 +80,13 @@ func (*Attachment) scanValues(columns []string) ([]any, error) {
|
|||||||
switch columns[i] {
|
switch columns[i] {
|
||||||
case attachment.FieldPrimary:
|
case attachment.FieldPrimary:
|
||||||
values[i] = new(sql.NullBool)
|
values[i] = new(sql.NullBool)
|
||||||
case attachment.FieldType, attachment.FieldTitle, attachment.FieldPath, attachment.FieldMimeType:
|
case attachment.FieldType:
|
||||||
values[i] = new(sql.NullString)
|
values[i] = new(sql.NullString)
|
||||||
case attachment.FieldCreatedAt, attachment.FieldUpdatedAt:
|
case attachment.FieldCreatedAt, attachment.FieldUpdatedAt:
|
||||||
values[i] = new(sql.NullTime)
|
values[i] = new(sql.NullTime)
|
||||||
case attachment.FieldID:
|
case attachment.FieldID:
|
||||||
values[i] = new(uuid.UUID)
|
values[i] = new(uuid.UUID)
|
||||||
case attachment.ForeignKeys[0]: // attachment_thumbnail
|
case attachment.ForeignKeys[0]: // document_attachments
|
||||||
values[i] = &sql.NullScanner{S: new(uuid.UUID)}
|
values[i] = &sql.NullScanner{S: new(uuid.UUID)}
|
||||||
case attachment.ForeignKeys[1]: // item_attachments
|
case attachment.ForeignKeys[1]: // item_attachments
|
||||||
values[i] = &sql.NullScanner{S: new(uuid.UUID)}
|
values[i] = &sql.NullScanner{S: new(uuid.UUID)}
|
||||||
@@ -100,7 +99,7 @@ func (*Attachment) scanValues(columns []string) ([]any, error) {
|
|||||||
|
|
||||||
// assignValues assigns the values that were returned from sql.Rows (after scanning)
|
// assignValues assigns the values that were returned from sql.Rows (after scanning)
|
||||||
// to the Attachment fields.
|
// to the Attachment fields.
|
||||||
func (_m *Attachment) assignValues(columns []string, values []any) error {
|
func (a *Attachment) assignValues(columns []string, values []any) error {
|
||||||
if m, n := len(values), len(columns); m < n {
|
if m, n := len(values), len(columns); m < n {
|
||||||
return fmt.Errorf("mismatch number of scan values: %d != %d", m, n)
|
return fmt.Errorf("mismatch number of scan values: %d != %d", m, n)
|
||||||
}
|
}
|
||||||
@@ -110,66 +109,48 @@ func (_m *Attachment) assignValues(columns []string, values []any) error {
|
|||||||
if value, ok := values[i].(*uuid.UUID); !ok {
|
if value, ok := values[i].(*uuid.UUID); !ok {
|
||||||
return fmt.Errorf("unexpected type %T for field id", values[i])
|
return fmt.Errorf("unexpected type %T for field id", values[i])
|
||||||
} else if value != nil {
|
} else if value != nil {
|
||||||
_m.ID = *value
|
a.ID = *value
|
||||||
}
|
}
|
||||||
case attachment.FieldCreatedAt:
|
case attachment.FieldCreatedAt:
|
||||||
if value, ok := values[i].(*sql.NullTime); !ok {
|
if value, ok := values[i].(*sql.NullTime); !ok {
|
||||||
return fmt.Errorf("unexpected type %T for field created_at", values[i])
|
return fmt.Errorf("unexpected type %T for field created_at", values[i])
|
||||||
} else if value.Valid {
|
} else if value.Valid {
|
||||||
_m.CreatedAt = value.Time
|
a.CreatedAt = value.Time
|
||||||
}
|
}
|
||||||
case attachment.FieldUpdatedAt:
|
case attachment.FieldUpdatedAt:
|
||||||
if value, ok := values[i].(*sql.NullTime); !ok {
|
if value, ok := values[i].(*sql.NullTime); !ok {
|
||||||
return fmt.Errorf("unexpected type %T for field updated_at", values[i])
|
return fmt.Errorf("unexpected type %T for field updated_at", values[i])
|
||||||
} else if value.Valid {
|
} else if value.Valid {
|
||||||
_m.UpdatedAt = value.Time
|
a.UpdatedAt = value.Time
|
||||||
}
|
}
|
||||||
case attachment.FieldType:
|
case attachment.FieldType:
|
||||||
if value, ok := values[i].(*sql.NullString); !ok {
|
if value, ok := values[i].(*sql.NullString); !ok {
|
||||||
return fmt.Errorf("unexpected type %T for field type", values[i])
|
return fmt.Errorf("unexpected type %T for field type", values[i])
|
||||||
} else if value.Valid {
|
} else if value.Valid {
|
||||||
_m.Type = attachment.Type(value.String)
|
a.Type = attachment.Type(value.String)
|
||||||
}
|
}
|
||||||
case attachment.FieldPrimary:
|
case attachment.FieldPrimary:
|
||||||
if value, ok := values[i].(*sql.NullBool); !ok {
|
if value, ok := values[i].(*sql.NullBool); !ok {
|
||||||
return fmt.Errorf("unexpected type %T for field primary", values[i])
|
return fmt.Errorf("unexpected type %T for field primary", values[i])
|
||||||
} else if value.Valid {
|
} else if value.Valid {
|
||||||
_m.Primary = value.Bool
|
a.Primary = value.Bool
|
||||||
}
|
|
||||||
case attachment.FieldTitle:
|
|
||||||
if value, ok := values[i].(*sql.NullString); !ok {
|
|
||||||
return fmt.Errorf("unexpected type %T for field title", values[i])
|
|
||||||
} else if value.Valid {
|
|
||||||
_m.Title = value.String
|
|
||||||
}
|
|
||||||
case attachment.FieldPath:
|
|
||||||
if value, ok := values[i].(*sql.NullString); !ok {
|
|
||||||
return fmt.Errorf("unexpected type %T for field path", values[i])
|
|
||||||
} else if value.Valid {
|
|
||||||
_m.Path = value.String
|
|
||||||
}
|
|
||||||
case attachment.FieldMimeType:
|
|
||||||
if value, ok := values[i].(*sql.NullString); !ok {
|
|
||||||
return fmt.Errorf("unexpected type %T for field mime_type", values[i])
|
|
||||||
} else if value.Valid {
|
|
||||||
_m.MimeType = value.String
|
|
||||||
}
|
}
|
||||||
case attachment.ForeignKeys[0]:
|
case attachment.ForeignKeys[0]:
|
||||||
if value, ok := values[i].(*sql.NullScanner); !ok {
|
if value, ok := values[i].(*sql.NullScanner); !ok {
|
||||||
return fmt.Errorf("unexpected type %T for field attachment_thumbnail", values[i])
|
return fmt.Errorf("unexpected type %T for field document_attachments", values[i])
|
||||||
} else if value.Valid {
|
} else if value.Valid {
|
||||||
_m.attachment_thumbnail = new(uuid.UUID)
|
a.document_attachments = new(uuid.UUID)
|
||||||
*_m.attachment_thumbnail = *value.S.(*uuid.UUID)
|
*a.document_attachments = *value.S.(*uuid.UUID)
|
||||||
}
|
}
|
||||||
case attachment.ForeignKeys[1]:
|
case attachment.ForeignKeys[1]:
|
||||||
if value, ok := values[i].(*sql.NullScanner); !ok {
|
if value, ok := values[i].(*sql.NullScanner); !ok {
|
||||||
return fmt.Errorf("unexpected type %T for field item_attachments", values[i])
|
return fmt.Errorf("unexpected type %T for field item_attachments", values[i])
|
||||||
} else if value.Valid {
|
} else if value.Valid {
|
||||||
_m.item_attachments = new(uuid.UUID)
|
a.item_attachments = new(uuid.UUID)
|
||||||
*_m.item_attachments = *value.S.(*uuid.UUID)
|
*a.item_attachments = *value.S.(*uuid.UUID)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
_m.selectValues.Set(columns[i], values[i])
|
a.selectValues.Set(columns[i], values[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -177,63 +158,54 @@ func (_m *Attachment) assignValues(columns []string, values []any) error {
|
|||||||
|
|
||||||
// Value returns the ent.Value that was dynamically selected and assigned to the Attachment.
|
// Value returns the ent.Value that was dynamically selected and assigned to the Attachment.
|
||||||
// This includes values selected through modifiers, order, etc.
|
// This includes values selected through modifiers, order, etc.
|
||||||
func (_m *Attachment) Value(name string) (ent.Value, error) {
|
func (a *Attachment) Value(name string) (ent.Value, error) {
|
||||||
return _m.selectValues.Get(name)
|
return a.selectValues.Get(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// QueryItem queries the "item" edge of the Attachment entity.
|
// QueryItem queries the "item" edge of the Attachment entity.
|
||||||
func (_m *Attachment) QueryItem() *ItemQuery {
|
func (a *Attachment) QueryItem() *ItemQuery {
|
||||||
return NewAttachmentClient(_m.config).QueryItem(_m)
|
return NewAttachmentClient(a.config).QueryItem(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
// QueryThumbnail queries the "thumbnail" edge of the Attachment entity.
|
// QueryDocument queries the "document" edge of the Attachment entity.
|
||||||
func (_m *Attachment) QueryThumbnail() *AttachmentQuery {
|
func (a *Attachment) QueryDocument() *DocumentQuery {
|
||||||
return NewAttachmentClient(_m.config).QueryThumbnail(_m)
|
return NewAttachmentClient(a.config).QueryDocument(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update returns a builder for updating this Attachment.
|
// Update returns a builder for updating this Attachment.
|
||||||
// Note that you need to call Attachment.Unwrap() before calling this method if this Attachment
|
// Note that you need to call Attachment.Unwrap() before calling this method if this Attachment
|
||||||
// was returned from a transaction, and the transaction was committed or rolled back.
|
// was returned from a transaction, and the transaction was committed or rolled back.
|
||||||
func (_m *Attachment) Update() *AttachmentUpdateOne {
|
func (a *Attachment) Update() *AttachmentUpdateOne {
|
||||||
return NewAttachmentClient(_m.config).UpdateOne(_m)
|
return NewAttachmentClient(a.config).UpdateOne(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unwrap unwraps the Attachment entity that was returned from a transaction after it was closed,
|
// Unwrap unwraps the Attachment entity that was returned from a transaction after it was closed,
|
||||||
// so that all future queries will be executed through the driver which created the transaction.
|
// so that all future queries will be executed through the driver which created the transaction.
|
||||||
func (_m *Attachment) Unwrap() *Attachment {
|
func (a *Attachment) Unwrap() *Attachment {
|
||||||
_tx, ok := _m.config.driver.(*txDriver)
|
_tx, ok := a.config.driver.(*txDriver)
|
||||||
if !ok {
|
if !ok {
|
||||||
panic("ent: Attachment is not a transactional entity")
|
panic("ent: Attachment is not a transactional entity")
|
||||||
}
|
}
|
||||||
_m.config.driver = _tx.drv
|
a.config.driver = _tx.drv
|
||||||
return _m
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
// String implements the fmt.Stringer.
|
// String implements the fmt.Stringer.
|
||||||
func (_m *Attachment) String() string {
|
func (a *Attachment) String() string {
|
||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
builder.WriteString("Attachment(")
|
builder.WriteString("Attachment(")
|
||||||
builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
|
builder.WriteString(fmt.Sprintf("id=%v, ", a.ID))
|
||||||
builder.WriteString("created_at=")
|
builder.WriteString("created_at=")
|
||||||
builder.WriteString(_m.CreatedAt.Format(time.ANSIC))
|
builder.WriteString(a.CreatedAt.Format(time.ANSIC))
|
||||||
builder.WriteString(", ")
|
builder.WriteString(", ")
|
||||||
builder.WriteString("updated_at=")
|
builder.WriteString("updated_at=")
|
||||||
builder.WriteString(_m.UpdatedAt.Format(time.ANSIC))
|
builder.WriteString(a.UpdatedAt.Format(time.ANSIC))
|
||||||
builder.WriteString(", ")
|
builder.WriteString(", ")
|
||||||
builder.WriteString("type=")
|
builder.WriteString("type=")
|
||||||
builder.WriteString(fmt.Sprintf("%v", _m.Type))
|
builder.WriteString(fmt.Sprintf("%v", a.Type))
|
||||||
builder.WriteString(", ")
|
builder.WriteString(", ")
|
||||||
builder.WriteString("primary=")
|
builder.WriteString("primary=")
|
||||||
builder.WriteString(fmt.Sprintf("%v", _m.Primary))
|
builder.WriteString(fmt.Sprintf("%v", a.Primary))
|
||||||
builder.WriteString(", ")
|
|
||||||
builder.WriteString("title=")
|
|
||||||
builder.WriteString(_m.Title)
|
|
||||||
builder.WriteString(", ")
|
|
||||||
builder.WriteString("path=")
|
|
||||||
builder.WriteString(_m.Path)
|
|
||||||
builder.WriteString(", ")
|
|
||||||
builder.WriteString("mime_type=")
|
|
||||||
builder.WriteString(_m.MimeType)
|
|
||||||
builder.WriteByte(')')
|
builder.WriteByte(')')
|
||||||
return builder.String()
|
return builder.String()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,16 +24,10 @@ const (
|
|||||||
FieldType = "type"
|
FieldType = "type"
|
||||||
// FieldPrimary holds the string denoting the primary field in the database.
|
// FieldPrimary holds the string denoting the primary field in the database.
|
||||||
FieldPrimary = "primary"
|
FieldPrimary = "primary"
|
||||||
// FieldTitle holds the string denoting the title field in the database.
|
|
||||||
FieldTitle = "title"
|
|
||||||
// FieldPath holds the string denoting the path field in the database.
|
|
||||||
FieldPath = "path"
|
|
||||||
// FieldMimeType holds the string denoting the mime_type field in the database.
|
|
||||||
FieldMimeType = "mime_type"
|
|
||||||
// EdgeItem holds the string denoting the item edge name in mutations.
|
// EdgeItem holds the string denoting the item edge name in mutations.
|
||||||
EdgeItem = "item"
|
EdgeItem = "item"
|
||||||
// EdgeThumbnail holds the string denoting the thumbnail edge name in mutations.
|
// EdgeDocument holds the string denoting the document edge name in mutations.
|
||||||
EdgeThumbnail = "thumbnail"
|
EdgeDocument = "document"
|
||||||
// Table holds the table name of the attachment in the database.
|
// Table holds the table name of the attachment in the database.
|
||||||
Table = "attachments"
|
Table = "attachments"
|
||||||
// ItemTable is the table that holds the item relation/edge.
|
// ItemTable is the table that holds the item relation/edge.
|
||||||
@@ -43,10 +37,13 @@ const (
|
|||||||
ItemInverseTable = "items"
|
ItemInverseTable = "items"
|
||||||
// ItemColumn is the table column denoting the item relation/edge.
|
// ItemColumn is the table column denoting the item relation/edge.
|
||||||
ItemColumn = "item_attachments"
|
ItemColumn = "item_attachments"
|
||||||
// ThumbnailTable is the table that holds the thumbnail relation/edge.
|
// DocumentTable is the table that holds the document relation/edge.
|
||||||
ThumbnailTable = "attachments"
|
DocumentTable = "attachments"
|
||||||
// ThumbnailColumn is the table column denoting the thumbnail relation/edge.
|
// DocumentInverseTable is the table name for the Document entity.
|
||||||
ThumbnailColumn = "attachment_thumbnail"
|
// It exists in this package in order to avoid circular dependency with the "document" package.
|
||||||
|
DocumentInverseTable = "documents"
|
||||||
|
// DocumentColumn is the table column denoting the document relation/edge.
|
||||||
|
DocumentColumn = "document_attachments"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Columns holds all SQL columns for attachment fields.
|
// Columns holds all SQL columns for attachment fields.
|
||||||
@@ -56,15 +53,12 @@ var Columns = []string{
|
|||||||
FieldUpdatedAt,
|
FieldUpdatedAt,
|
||||||
FieldType,
|
FieldType,
|
||||||
FieldPrimary,
|
FieldPrimary,
|
||||||
FieldTitle,
|
|
||||||
FieldPath,
|
|
||||||
FieldMimeType,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ForeignKeys holds the SQL foreign-keys that are owned by the "attachments"
|
// ForeignKeys holds the SQL foreign-keys that are owned by the "attachments"
|
||||||
// table and are not defined as standalone fields in the schema.
|
// table and are not defined as standalone fields in the schema.
|
||||||
var ForeignKeys = []string{
|
var ForeignKeys = []string{
|
||||||
"attachment_thumbnail",
|
"document_attachments",
|
||||||
"item_attachments",
|
"item_attachments",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,12 +86,6 @@ var (
|
|||||||
UpdateDefaultUpdatedAt func() time.Time
|
UpdateDefaultUpdatedAt func() time.Time
|
||||||
// DefaultPrimary holds the default value on creation for the "primary" field.
|
// DefaultPrimary holds the default value on creation for the "primary" field.
|
||||||
DefaultPrimary bool
|
DefaultPrimary bool
|
||||||
// DefaultTitle holds the default value on creation for the "title" field.
|
|
||||||
DefaultTitle string
|
|
||||||
// DefaultPath holds the default value on creation for the "path" field.
|
|
||||||
DefaultPath string
|
|
||||||
// DefaultMimeType holds the default value on creation for the "mime_type" field.
|
|
||||||
DefaultMimeType string
|
|
||||||
// DefaultID holds the default value on creation for the "id" field.
|
// DefaultID holds the default value on creation for the "id" field.
|
||||||
DefaultID func() uuid.UUID
|
DefaultID func() uuid.UUID
|
||||||
)
|
)
|
||||||
@@ -115,7 +103,6 @@ const (
|
|||||||
TypeWarranty Type = "warranty"
|
TypeWarranty Type = "warranty"
|
||||||
TypeAttachment Type = "attachment"
|
TypeAttachment Type = "attachment"
|
||||||
TypeReceipt Type = "receipt"
|
TypeReceipt Type = "receipt"
|
||||||
TypeThumbnail Type = "thumbnail"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (_type Type) String() string {
|
func (_type Type) String() string {
|
||||||
@@ -125,7 +112,7 @@ func (_type Type) String() string {
|
|||||||
// TypeValidator is a validator for the "type" field enum values. It is called by the builders before save.
|
// TypeValidator is a validator for the "type" field enum values. It is called by the builders before save.
|
||||||
func TypeValidator(_type Type) error {
|
func TypeValidator(_type Type) error {
|
||||||
switch _type {
|
switch _type {
|
||||||
case TypePhoto, TypeManual, TypeWarranty, TypeAttachment, TypeReceipt, TypeThumbnail:
|
case TypePhoto, TypeManual, TypeWarranty, TypeAttachment, TypeReceipt:
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("attachment: invalid enum value for type field: %q", _type)
|
return fmt.Errorf("attachment: invalid enum value for type field: %q", _type)
|
||||||
@@ -160,21 +147,6 @@ func ByPrimary(opts ...sql.OrderTermOption) OrderOption {
|
|||||||
return sql.OrderByField(FieldPrimary, opts...).ToFunc()
|
return sql.OrderByField(FieldPrimary, opts...).ToFunc()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ByTitle orders the results by the title field.
|
|
||||||
func ByTitle(opts ...sql.OrderTermOption) OrderOption {
|
|
||||||
return sql.OrderByField(FieldTitle, opts...).ToFunc()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ByPath orders the results by the path field.
|
|
||||||
func ByPath(opts ...sql.OrderTermOption) OrderOption {
|
|
||||||
return sql.OrderByField(FieldPath, opts...).ToFunc()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ByMimeType orders the results by the mime_type field.
|
|
||||||
func ByMimeType(opts ...sql.OrderTermOption) OrderOption {
|
|
||||||
return sql.OrderByField(FieldMimeType, opts...).ToFunc()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ByItemField orders the results by item field.
|
// ByItemField orders the results by item field.
|
||||||
func ByItemField(field string, opts ...sql.OrderTermOption) OrderOption {
|
func ByItemField(field string, opts ...sql.OrderTermOption) OrderOption {
|
||||||
return func(s *sql.Selector) {
|
return func(s *sql.Selector) {
|
||||||
@@ -182,10 +154,10 @@ func ByItemField(field string, opts ...sql.OrderTermOption) OrderOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ByThumbnailField orders the results by thumbnail field.
|
// ByDocumentField orders the results by document field.
|
||||||
func ByThumbnailField(field string, opts ...sql.OrderTermOption) OrderOption {
|
func ByDocumentField(field string, opts ...sql.OrderTermOption) OrderOption {
|
||||||
return func(s *sql.Selector) {
|
return func(s *sql.Selector) {
|
||||||
sqlgraph.OrderByNeighborTerms(s, newThumbnailStep(), sql.OrderByField(field, opts...))
|
sqlgraph.OrderByNeighborTerms(s, newDocumentStep(), sql.OrderByField(field, opts...))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func newItemStep() *sqlgraph.Step {
|
func newItemStep() *sqlgraph.Step {
|
||||||
@@ -195,10 +167,10 @@ func newItemStep() *sqlgraph.Step {
|
|||||||
sqlgraph.Edge(sqlgraph.M2O, true, ItemTable, ItemColumn),
|
sqlgraph.Edge(sqlgraph.M2O, true, ItemTable, ItemColumn),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
func newThumbnailStep() *sqlgraph.Step {
|
func newDocumentStep() *sqlgraph.Step {
|
||||||
return sqlgraph.NewStep(
|
return sqlgraph.NewStep(
|
||||||
sqlgraph.From(Table, FieldID),
|
sqlgraph.From(Table, FieldID),
|
||||||
sqlgraph.To(Table, FieldID),
|
sqlgraph.To(DocumentInverseTable, FieldID),
|
||||||
sqlgraph.Edge(sqlgraph.O2O, false, ThumbnailTable, ThumbnailColumn),
|
sqlgraph.Edge(sqlgraph.M2O, true, DocumentTable, DocumentColumn),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,21 +71,6 @@ func Primary(v bool) predicate.Attachment {
|
|||||||
return predicate.Attachment(sql.FieldEQ(FieldPrimary, v))
|
return predicate.Attachment(sql.FieldEQ(FieldPrimary, v))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Title applies equality check predicate on the "title" field. It's identical to TitleEQ.
|
|
||||||
func Title(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldEQ(FieldTitle, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Path applies equality check predicate on the "path" field. It's identical to PathEQ.
|
|
||||||
func Path(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldEQ(FieldPath, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MimeType applies equality check predicate on the "mime_type" field. It's identical to MimeTypeEQ.
|
|
||||||
func MimeType(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldEQ(FieldMimeType, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreatedAtEQ applies the EQ predicate on the "created_at" field.
|
// CreatedAtEQ applies the EQ predicate on the "created_at" field.
|
||||||
func CreatedAtEQ(v time.Time) predicate.Attachment {
|
func CreatedAtEQ(v time.Time) predicate.Attachment {
|
||||||
return predicate.Attachment(sql.FieldEQ(FieldCreatedAt, v))
|
return predicate.Attachment(sql.FieldEQ(FieldCreatedAt, v))
|
||||||
@@ -196,201 +181,6 @@ func PrimaryNEQ(v bool) predicate.Attachment {
|
|||||||
return predicate.Attachment(sql.FieldNEQ(FieldPrimary, v))
|
return predicate.Attachment(sql.FieldNEQ(FieldPrimary, v))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TitleEQ applies the EQ predicate on the "title" field.
|
|
||||||
func TitleEQ(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldEQ(FieldTitle, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TitleNEQ applies the NEQ predicate on the "title" field.
|
|
||||||
func TitleNEQ(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldNEQ(FieldTitle, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TitleIn applies the In predicate on the "title" field.
|
|
||||||
func TitleIn(vs ...string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldIn(FieldTitle, vs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TitleNotIn applies the NotIn predicate on the "title" field.
|
|
||||||
func TitleNotIn(vs ...string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldNotIn(FieldTitle, vs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TitleGT applies the GT predicate on the "title" field.
|
|
||||||
func TitleGT(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldGT(FieldTitle, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TitleGTE applies the GTE predicate on the "title" field.
|
|
||||||
func TitleGTE(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldGTE(FieldTitle, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TitleLT applies the LT predicate on the "title" field.
|
|
||||||
func TitleLT(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldLT(FieldTitle, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TitleLTE applies the LTE predicate on the "title" field.
|
|
||||||
func TitleLTE(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldLTE(FieldTitle, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TitleContains applies the Contains predicate on the "title" field.
|
|
||||||
func TitleContains(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldContains(FieldTitle, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TitleHasPrefix applies the HasPrefix predicate on the "title" field.
|
|
||||||
func TitleHasPrefix(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldHasPrefix(FieldTitle, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TitleHasSuffix applies the HasSuffix predicate on the "title" field.
|
|
||||||
func TitleHasSuffix(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldHasSuffix(FieldTitle, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TitleEqualFold applies the EqualFold predicate on the "title" field.
|
|
||||||
func TitleEqualFold(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldEqualFold(FieldTitle, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TitleContainsFold applies the ContainsFold predicate on the "title" field.
|
|
||||||
func TitleContainsFold(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldContainsFold(FieldTitle, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// PathEQ applies the EQ predicate on the "path" field.
|
|
||||||
func PathEQ(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldEQ(FieldPath, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// PathNEQ applies the NEQ predicate on the "path" field.
|
|
||||||
func PathNEQ(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldNEQ(FieldPath, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// PathIn applies the In predicate on the "path" field.
|
|
||||||
func PathIn(vs ...string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldIn(FieldPath, vs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
// PathNotIn applies the NotIn predicate on the "path" field.
|
|
||||||
func PathNotIn(vs ...string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldNotIn(FieldPath, vs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
// PathGT applies the GT predicate on the "path" field.
|
|
||||||
func PathGT(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldGT(FieldPath, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// PathGTE applies the GTE predicate on the "path" field.
|
|
||||||
func PathGTE(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldGTE(FieldPath, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// PathLT applies the LT predicate on the "path" field.
|
|
||||||
func PathLT(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldLT(FieldPath, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// PathLTE applies the LTE predicate on the "path" field.
|
|
||||||
func PathLTE(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldLTE(FieldPath, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// PathContains applies the Contains predicate on the "path" field.
|
|
||||||
func PathContains(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldContains(FieldPath, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// PathHasPrefix applies the HasPrefix predicate on the "path" field.
|
|
||||||
func PathHasPrefix(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldHasPrefix(FieldPath, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// PathHasSuffix applies the HasSuffix predicate on the "path" field.
|
|
||||||
func PathHasSuffix(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldHasSuffix(FieldPath, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// PathEqualFold applies the EqualFold predicate on the "path" field.
|
|
||||||
func PathEqualFold(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldEqualFold(FieldPath, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// PathContainsFold applies the ContainsFold predicate on the "path" field.
|
|
||||||
func PathContainsFold(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldContainsFold(FieldPath, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MimeTypeEQ applies the EQ predicate on the "mime_type" field.
|
|
||||||
func MimeTypeEQ(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldEQ(FieldMimeType, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MimeTypeNEQ applies the NEQ predicate on the "mime_type" field.
|
|
||||||
func MimeTypeNEQ(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldNEQ(FieldMimeType, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MimeTypeIn applies the In predicate on the "mime_type" field.
|
|
||||||
func MimeTypeIn(vs ...string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldIn(FieldMimeType, vs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MimeTypeNotIn applies the NotIn predicate on the "mime_type" field.
|
|
||||||
func MimeTypeNotIn(vs ...string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldNotIn(FieldMimeType, vs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MimeTypeGT applies the GT predicate on the "mime_type" field.
|
|
||||||
func MimeTypeGT(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldGT(FieldMimeType, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MimeTypeGTE applies the GTE predicate on the "mime_type" field.
|
|
||||||
func MimeTypeGTE(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldGTE(FieldMimeType, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MimeTypeLT applies the LT predicate on the "mime_type" field.
|
|
||||||
func MimeTypeLT(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldLT(FieldMimeType, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MimeTypeLTE applies the LTE predicate on the "mime_type" field.
|
|
||||||
func MimeTypeLTE(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldLTE(FieldMimeType, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MimeTypeContains applies the Contains predicate on the "mime_type" field.
|
|
||||||
func MimeTypeContains(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldContains(FieldMimeType, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MimeTypeHasPrefix applies the HasPrefix predicate on the "mime_type" field.
|
|
||||||
func MimeTypeHasPrefix(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldHasPrefix(FieldMimeType, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MimeTypeHasSuffix applies the HasSuffix predicate on the "mime_type" field.
|
|
||||||
func MimeTypeHasSuffix(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldHasSuffix(FieldMimeType, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MimeTypeEqualFold applies the EqualFold predicate on the "mime_type" field.
|
|
||||||
func MimeTypeEqualFold(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldEqualFold(FieldMimeType, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MimeTypeContainsFold applies the ContainsFold predicate on the "mime_type" field.
|
|
||||||
func MimeTypeContainsFold(v string) predicate.Attachment {
|
|
||||||
return predicate.Attachment(sql.FieldContainsFold(FieldMimeType, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasItem applies the HasEdge predicate on the "item" edge.
|
// HasItem applies the HasEdge predicate on the "item" edge.
|
||||||
func HasItem() predicate.Attachment {
|
func HasItem() predicate.Attachment {
|
||||||
return predicate.Attachment(func(s *sql.Selector) {
|
return predicate.Attachment(func(s *sql.Selector) {
|
||||||
@@ -414,21 +204,21 @@ func HasItemWith(preds ...predicate.Item) predicate.Attachment {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasThumbnail applies the HasEdge predicate on the "thumbnail" edge.
|
// HasDocument applies the HasEdge predicate on the "document" edge.
|
||||||
func HasThumbnail() predicate.Attachment {
|
func HasDocument() predicate.Attachment {
|
||||||
return predicate.Attachment(func(s *sql.Selector) {
|
return predicate.Attachment(func(s *sql.Selector) {
|
||||||
step := sqlgraph.NewStep(
|
step := sqlgraph.NewStep(
|
||||||
sqlgraph.From(Table, FieldID),
|
sqlgraph.From(Table, FieldID),
|
||||||
sqlgraph.Edge(sqlgraph.O2O, false, ThumbnailTable, ThumbnailColumn),
|
sqlgraph.Edge(sqlgraph.M2O, true, DocumentTable, DocumentColumn),
|
||||||
)
|
)
|
||||||
sqlgraph.HasNeighbors(s, step)
|
sqlgraph.HasNeighbors(s, step)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasThumbnailWith applies the HasEdge predicate on the "thumbnail" edge with a given conditions (other predicates).
|
// HasDocumentWith applies the HasEdge predicate on the "document" edge with a given conditions (other predicates).
|
||||||
func HasThumbnailWith(preds ...predicate.Attachment) predicate.Attachment {
|
func HasDocumentWith(preds ...predicate.Document) predicate.Attachment {
|
||||||
return predicate.Attachment(func(s *sql.Selector) {
|
return predicate.Attachment(func(s *sql.Selector) {
|
||||||
step := newThumbnailStep()
|
step := newDocumentStep()
|
||||||
sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
|
sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
|
||||||
for _, p := range preds {
|
for _, p := range preds {
|
||||||
p(s)
|
p(s)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"entgo.io/ent/schema/field"
|
"entgo.io/ent/schema/field"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/attachment"
|
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/attachment"
|
||||||
|
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/document"
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/item"
|
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/item"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,169 +24,111 @@ type AttachmentCreate struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetCreatedAt sets the "created_at" field.
|
// SetCreatedAt sets the "created_at" field.
|
||||||
func (_c *AttachmentCreate) SetCreatedAt(v time.Time) *AttachmentCreate {
|
func (ac *AttachmentCreate) SetCreatedAt(t time.Time) *AttachmentCreate {
|
||||||
_c.mutation.SetCreatedAt(v)
|
ac.mutation.SetCreatedAt(t)
|
||||||
return _c
|
return ac
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetNillableCreatedAt sets the "created_at" field if the given value is not nil.
|
// SetNillableCreatedAt sets the "created_at" field if the given value is not nil.
|
||||||
func (_c *AttachmentCreate) SetNillableCreatedAt(v *time.Time) *AttachmentCreate {
|
func (ac *AttachmentCreate) SetNillableCreatedAt(t *time.Time) *AttachmentCreate {
|
||||||
if v != nil {
|
if t != nil {
|
||||||
_c.SetCreatedAt(*v)
|
ac.SetCreatedAt(*t)
|
||||||
}
|
}
|
||||||
return _c
|
return ac
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetUpdatedAt sets the "updated_at" field.
|
// SetUpdatedAt sets the "updated_at" field.
|
||||||
func (_c *AttachmentCreate) SetUpdatedAt(v time.Time) *AttachmentCreate {
|
func (ac *AttachmentCreate) SetUpdatedAt(t time.Time) *AttachmentCreate {
|
||||||
_c.mutation.SetUpdatedAt(v)
|
ac.mutation.SetUpdatedAt(t)
|
||||||
return _c
|
return ac
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetNillableUpdatedAt sets the "updated_at" field if the given value is not nil.
|
// SetNillableUpdatedAt sets the "updated_at" field if the given value is not nil.
|
||||||
func (_c *AttachmentCreate) SetNillableUpdatedAt(v *time.Time) *AttachmentCreate {
|
func (ac *AttachmentCreate) SetNillableUpdatedAt(t *time.Time) *AttachmentCreate {
|
||||||
if v != nil {
|
if t != nil {
|
||||||
_c.SetUpdatedAt(*v)
|
ac.SetUpdatedAt(*t)
|
||||||
}
|
}
|
||||||
return _c
|
return ac
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetType sets the "type" field.
|
// SetType sets the "type" field.
|
||||||
func (_c *AttachmentCreate) SetType(v attachment.Type) *AttachmentCreate {
|
func (ac *AttachmentCreate) SetType(a attachment.Type) *AttachmentCreate {
|
||||||
_c.mutation.SetType(v)
|
ac.mutation.SetType(a)
|
||||||
return _c
|
return ac
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetNillableType sets the "type" field if the given value is not nil.
|
// SetNillableType sets the "type" field if the given value is not nil.
|
||||||
func (_c *AttachmentCreate) SetNillableType(v *attachment.Type) *AttachmentCreate {
|
func (ac *AttachmentCreate) SetNillableType(a *attachment.Type) *AttachmentCreate {
|
||||||
if v != nil {
|
if a != nil {
|
||||||
_c.SetType(*v)
|
ac.SetType(*a)
|
||||||
}
|
}
|
||||||
return _c
|
return ac
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPrimary sets the "primary" field.
|
// SetPrimary sets the "primary" field.
|
||||||
func (_c *AttachmentCreate) SetPrimary(v bool) *AttachmentCreate {
|
func (ac *AttachmentCreate) SetPrimary(b bool) *AttachmentCreate {
|
||||||
_c.mutation.SetPrimary(v)
|
ac.mutation.SetPrimary(b)
|
||||||
return _c
|
return ac
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetNillablePrimary sets the "primary" field if the given value is not nil.
|
// SetNillablePrimary sets the "primary" field if the given value is not nil.
|
||||||
func (_c *AttachmentCreate) SetNillablePrimary(v *bool) *AttachmentCreate {
|
func (ac *AttachmentCreate) SetNillablePrimary(b *bool) *AttachmentCreate {
|
||||||
if v != nil {
|
if b != nil {
|
||||||
_c.SetPrimary(*v)
|
ac.SetPrimary(*b)
|
||||||
}
|
}
|
||||||
return _c
|
return ac
|
||||||
}
|
|
||||||
|
|
||||||
// SetTitle sets the "title" field.
|
|
||||||
func (_c *AttachmentCreate) SetTitle(v string) *AttachmentCreate {
|
|
||||||
_c.mutation.SetTitle(v)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetNillableTitle sets the "title" field if the given value is not nil.
|
|
||||||
func (_c *AttachmentCreate) SetNillableTitle(v *string) *AttachmentCreate {
|
|
||||||
if v != nil {
|
|
||||||
_c.SetTitle(*v)
|
|
||||||
}
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetPath sets the "path" field.
|
|
||||||
func (_c *AttachmentCreate) SetPath(v string) *AttachmentCreate {
|
|
||||||
_c.mutation.SetPath(v)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetNillablePath sets the "path" field if the given value is not nil.
|
|
||||||
func (_c *AttachmentCreate) SetNillablePath(v *string) *AttachmentCreate {
|
|
||||||
if v != nil {
|
|
||||||
_c.SetPath(*v)
|
|
||||||
}
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetMimeType sets the "mime_type" field.
|
|
||||||
func (_c *AttachmentCreate) SetMimeType(v string) *AttachmentCreate {
|
|
||||||
_c.mutation.SetMimeType(v)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetNillableMimeType sets the "mime_type" field if the given value is not nil.
|
|
||||||
func (_c *AttachmentCreate) SetNillableMimeType(v *string) *AttachmentCreate {
|
|
||||||
if v != nil {
|
|
||||||
_c.SetMimeType(*v)
|
|
||||||
}
|
|
||||||
return _c
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the "id" field.
|
// SetID sets the "id" field.
|
||||||
func (_c *AttachmentCreate) SetID(v uuid.UUID) *AttachmentCreate {
|
func (ac *AttachmentCreate) SetID(u uuid.UUID) *AttachmentCreate {
|
||||||
_c.mutation.SetID(v)
|
ac.mutation.SetID(u)
|
||||||
return _c
|
return ac
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetNillableID sets the "id" field if the given value is not nil.
|
// SetNillableID sets the "id" field if the given value is not nil.
|
||||||
func (_c *AttachmentCreate) SetNillableID(v *uuid.UUID) *AttachmentCreate {
|
func (ac *AttachmentCreate) SetNillableID(u *uuid.UUID) *AttachmentCreate {
|
||||||
if v != nil {
|
if u != nil {
|
||||||
_c.SetID(*v)
|
ac.SetID(*u)
|
||||||
}
|
}
|
||||||
return _c
|
return ac
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetItemID sets the "item" edge to the Item entity by ID.
|
// SetItemID sets the "item" edge to the Item entity by ID.
|
||||||
func (_c *AttachmentCreate) SetItemID(id uuid.UUID) *AttachmentCreate {
|
func (ac *AttachmentCreate) SetItemID(id uuid.UUID) *AttachmentCreate {
|
||||||
_c.mutation.SetItemID(id)
|
ac.mutation.SetItemID(id)
|
||||||
return _c
|
return ac
|
||||||
}
|
|
||||||
|
|
||||||
// SetNillableItemID sets the "item" edge to the Item entity by ID if the given value is not nil.
|
|
||||||
func (_c *AttachmentCreate) SetNillableItemID(id *uuid.UUID) *AttachmentCreate {
|
|
||||||
if id != nil {
|
|
||||||
_c = _c.SetItemID(*id)
|
|
||||||
}
|
|
||||||
return _c
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetItem sets the "item" edge to the Item entity.
|
// SetItem sets the "item" edge to the Item entity.
|
||||||
func (_c *AttachmentCreate) SetItem(v *Item) *AttachmentCreate {
|
func (ac *AttachmentCreate) SetItem(i *Item) *AttachmentCreate {
|
||||||
return _c.SetItemID(v.ID)
|
return ac.SetItemID(i.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetThumbnailID sets the "thumbnail" edge to the Attachment entity by ID.
|
// SetDocumentID sets the "document" edge to the Document entity by ID.
|
||||||
func (_c *AttachmentCreate) SetThumbnailID(id uuid.UUID) *AttachmentCreate {
|
func (ac *AttachmentCreate) SetDocumentID(id uuid.UUID) *AttachmentCreate {
|
||||||
_c.mutation.SetThumbnailID(id)
|
ac.mutation.SetDocumentID(id)
|
||||||
return _c
|
return ac
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetNillableThumbnailID sets the "thumbnail" edge to the Attachment entity by ID if the given value is not nil.
|
// SetDocument sets the "document" edge to the Document entity.
|
||||||
func (_c *AttachmentCreate) SetNillableThumbnailID(id *uuid.UUID) *AttachmentCreate {
|
func (ac *AttachmentCreate) SetDocument(d *Document) *AttachmentCreate {
|
||||||
if id != nil {
|
return ac.SetDocumentID(d.ID)
|
||||||
_c = _c.SetThumbnailID(*id)
|
|
||||||
}
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetThumbnail sets the "thumbnail" edge to the Attachment entity.
|
|
||||||
func (_c *AttachmentCreate) SetThumbnail(v *Attachment) *AttachmentCreate {
|
|
||||||
return _c.SetThumbnailID(v.ID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mutation returns the AttachmentMutation object of the builder.
|
// Mutation returns the AttachmentMutation object of the builder.
|
||||||
func (_c *AttachmentCreate) Mutation() *AttachmentMutation {
|
func (ac *AttachmentCreate) Mutation() *AttachmentMutation {
|
||||||
return _c.mutation
|
return ac.mutation
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save creates the Attachment in the database.
|
// Save creates the Attachment in the database.
|
||||||
func (_c *AttachmentCreate) Save(ctx context.Context) (*Attachment, error) {
|
func (ac *AttachmentCreate) Save(ctx context.Context) (*Attachment, error) {
|
||||||
_c.defaults()
|
ac.defaults()
|
||||||
return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks)
|
return withHooks(ctx, ac.sqlSave, ac.mutation, ac.hooks)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveX calls Save and panics if Save returns an error.
|
// SaveX calls Save and panics if Save returns an error.
|
||||||
func (_c *AttachmentCreate) SaveX(ctx context.Context) *Attachment {
|
func (ac *AttachmentCreate) SaveX(ctx context.Context) *Attachment {
|
||||||
v, err := _c.Save(ctx)
|
v, err := ac.Save(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -193,91 +136,76 @@ func (_c *AttachmentCreate) SaveX(ctx context.Context) *Attachment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Exec executes the query.
|
// Exec executes the query.
|
||||||
func (_c *AttachmentCreate) Exec(ctx context.Context) error {
|
func (ac *AttachmentCreate) Exec(ctx context.Context) error {
|
||||||
_, err := _c.Save(ctx)
|
_, err := ac.Save(ctx)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecX is like Exec, but panics if an error occurs.
|
// ExecX is like Exec, but panics if an error occurs.
|
||||||
func (_c *AttachmentCreate) ExecX(ctx context.Context) {
|
func (ac *AttachmentCreate) ExecX(ctx context.Context) {
|
||||||
if err := _c.Exec(ctx); err != nil {
|
if err := ac.Exec(ctx); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaults sets the default values of the builder before save.
|
// defaults sets the default values of the builder before save.
|
||||||
func (_c *AttachmentCreate) defaults() {
|
func (ac *AttachmentCreate) defaults() {
|
||||||
if _, ok := _c.mutation.CreatedAt(); !ok {
|
if _, ok := ac.mutation.CreatedAt(); !ok {
|
||||||
v := attachment.DefaultCreatedAt()
|
v := attachment.DefaultCreatedAt()
|
||||||
_c.mutation.SetCreatedAt(v)
|
ac.mutation.SetCreatedAt(v)
|
||||||
}
|
}
|
||||||
if _, ok := _c.mutation.UpdatedAt(); !ok {
|
if _, ok := ac.mutation.UpdatedAt(); !ok {
|
||||||
v := attachment.DefaultUpdatedAt()
|
v := attachment.DefaultUpdatedAt()
|
||||||
_c.mutation.SetUpdatedAt(v)
|
ac.mutation.SetUpdatedAt(v)
|
||||||
}
|
}
|
||||||
if _, ok := _c.mutation.GetType(); !ok {
|
if _, ok := ac.mutation.GetType(); !ok {
|
||||||
v := attachment.DefaultType
|
v := attachment.DefaultType
|
||||||
_c.mutation.SetType(v)
|
ac.mutation.SetType(v)
|
||||||
}
|
}
|
||||||
if _, ok := _c.mutation.Primary(); !ok {
|
if _, ok := ac.mutation.Primary(); !ok {
|
||||||
v := attachment.DefaultPrimary
|
v := attachment.DefaultPrimary
|
||||||
_c.mutation.SetPrimary(v)
|
ac.mutation.SetPrimary(v)
|
||||||
}
|
}
|
||||||
if _, ok := _c.mutation.Title(); !ok {
|
if _, ok := ac.mutation.ID(); !ok {
|
||||||
v := attachment.DefaultTitle
|
|
||||||
_c.mutation.SetTitle(v)
|
|
||||||
}
|
|
||||||
if _, ok := _c.mutation.Path(); !ok {
|
|
||||||
v := attachment.DefaultPath
|
|
||||||
_c.mutation.SetPath(v)
|
|
||||||
}
|
|
||||||
if _, ok := _c.mutation.MimeType(); !ok {
|
|
||||||
v := attachment.DefaultMimeType
|
|
||||||
_c.mutation.SetMimeType(v)
|
|
||||||
}
|
|
||||||
if _, ok := _c.mutation.ID(); !ok {
|
|
||||||
v := attachment.DefaultID()
|
v := attachment.DefaultID()
|
||||||
_c.mutation.SetID(v)
|
ac.mutation.SetID(v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check runs all checks and user-defined validators on the builder.
|
// check runs all checks and user-defined validators on the builder.
|
||||||
func (_c *AttachmentCreate) check() error {
|
func (ac *AttachmentCreate) check() error {
|
||||||
if _, ok := _c.mutation.CreatedAt(); !ok {
|
if _, ok := ac.mutation.CreatedAt(); !ok {
|
||||||
return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "Attachment.created_at"`)}
|
return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "Attachment.created_at"`)}
|
||||||
}
|
}
|
||||||
if _, ok := _c.mutation.UpdatedAt(); !ok {
|
if _, ok := ac.mutation.UpdatedAt(); !ok {
|
||||||
return &ValidationError{Name: "updated_at", err: errors.New(`ent: missing required field "Attachment.updated_at"`)}
|
return &ValidationError{Name: "updated_at", err: errors.New(`ent: missing required field "Attachment.updated_at"`)}
|
||||||
}
|
}
|
||||||
if _, ok := _c.mutation.GetType(); !ok {
|
if _, ok := ac.mutation.GetType(); !ok {
|
||||||
return &ValidationError{Name: "type", err: errors.New(`ent: missing required field "Attachment.type"`)}
|
return &ValidationError{Name: "type", err: errors.New(`ent: missing required field "Attachment.type"`)}
|
||||||
}
|
}
|
||||||
if v, ok := _c.mutation.GetType(); ok {
|
if v, ok := ac.mutation.GetType(); ok {
|
||||||
if err := attachment.TypeValidator(v); err != nil {
|
if err := attachment.TypeValidator(v); err != nil {
|
||||||
return &ValidationError{Name: "type", err: fmt.Errorf(`ent: validator failed for field "Attachment.type": %w`, err)}
|
return &ValidationError{Name: "type", err: fmt.Errorf(`ent: validator failed for field "Attachment.type": %w`, err)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, ok := _c.mutation.Primary(); !ok {
|
if _, ok := ac.mutation.Primary(); !ok {
|
||||||
return &ValidationError{Name: "primary", err: errors.New(`ent: missing required field "Attachment.primary"`)}
|
return &ValidationError{Name: "primary", err: errors.New(`ent: missing required field "Attachment.primary"`)}
|
||||||
}
|
}
|
||||||
if _, ok := _c.mutation.Title(); !ok {
|
if _, ok := ac.mutation.ItemID(); !ok {
|
||||||
return &ValidationError{Name: "title", err: errors.New(`ent: missing required field "Attachment.title"`)}
|
return &ValidationError{Name: "item", err: errors.New(`ent: missing required edge "Attachment.item"`)}
|
||||||
}
|
}
|
||||||
if _, ok := _c.mutation.Path(); !ok {
|
if _, ok := ac.mutation.DocumentID(); !ok {
|
||||||
return &ValidationError{Name: "path", err: errors.New(`ent: missing required field "Attachment.path"`)}
|
return &ValidationError{Name: "document", err: errors.New(`ent: missing required edge "Attachment.document"`)}
|
||||||
}
|
|
||||||
if _, ok := _c.mutation.MimeType(); !ok {
|
|
||||||
return &ValidationError{Name: "mime_type", err: errors.New(`ent: missing required field "Attachment.mime_type"`)}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_c *AttachmentCreate) sqlSave(ctx context.Context) (*Attachment, error) {
|
func (ac *AttachmentCreate) sqlSave(ctx context.Context) (*Attachment, error) {
|
||||||
if err := _c.check(); err != nil {
|
if err := ac.check(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
_node, _spec := _c.createSpec()
|
_node, _spec := ac.createSpec()
|
||||||
if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil {
|
if err := sqlgraph.CreateNode(ctx, ac.driver, _spec); err != nil {
|
||||||
if sqlgraph.IsConstraintError(err) {
|
if sqlgraph.IsConstraintError(err) {
|
||||||
err = &ConstraintError{msg: err.Error(), wrap: err}
|
err = &ConstraintError{msg: err.Error(), wrap: err}
|
||||||
}
|
}
|
||||||
@@ -290,49 +218,37 @@ func (_c *AttachmentCreate) sqlSave(ctx context.Context) (*Attachment, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_c.mutation.id = &_node.ID
|
ac.mutation.id = &_node.ID
|
||||||
_c.mutation.done = true
|
ac.mutation.done = true
|
||||||
return _node, nil
|
return _node, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_c *AttachmentCreate) createSpec() (*Attachment, *sqlgraph.CreateSpec) {
|
func (ac *AttachmentCreate) createSpec() (*Attachment, *sqlgraph.CreateSpec) {
|
||||||
var (
|
var (
|
||||||
_node = &Attachment{config: _c.config}
|
_node = &Attachment{config: ac.config}
|
||||||
_spec = sqlgraph.NewCreateSpec(attachment.Table, sqlgraph.NewFieldSpec(attachment.FieldID, field.TypeUUID))
|
_spec = sqlgraph.NewCreateSpec(attachment.Table, sqlgraph.NewFieldSpec(attachment.FieldID, field.TypeUUID))
|
||||||
)
|
)
|
||||||
if id, ok := _c.mutation.ID(); ok {
|
if id, ok := ac.mutation.ID(); ok {
|
||||||
_node.ID = id
|
_node.ID = id
|
||||||
_spec.ID.Value = &id
|
_spec.ID.Value = &id
|
||||||
}
|
}
|
||||||
if value, ok := _c.mutation.CreatedAt(); ok {
|
if value, ok := ac.mutation.CreatedAt(); ok {
|
||||||
_spec.SetField(attachment.FieldCreatedAt, field.TypeTime, value)
|
_spec.SetField(attachment.FieldCreatedAt, field.TypeTime, value)
|
||||||
_node.CreatedAt = value
|
_node.CreatedAt = value
|
||||||
}
|
}
|
||||||
if value, ok := _c.mutation.UpdatedAt(); ok {
|
if value, ok := ac.mutation.UpdatedAt(); ok {
|
||||||
_spec.SetField(attachment.FieldUpdatedAt, field.TypeTime, value)
|
_spec.SetField(attachment.FieldUpdatedAt, field.TypeTime, value)
|
||||||
_node.UpdatedAt = value
|
_node.UpdatedAt = value
|
||||||
}
|
}
|
||||||
if value, ok := _c.mutation.GetType(); ok {
|
if value, ok := ac.mutation.GetType(); ok {
|
||||||
_spec.SetField(attachment.FieldType, field.TypeEnum, value)
|
_spec.SetField(attachment.FieldType, field.TypeEnum, value)
|
||||||
_node.Type = value
|
_node.Type = value
|
||||||
}
|
}
|
||||||
if value, ok := _c.mutation.Primary(); ok {
|
if value, ok := ac.mutation.Primary(); ok {
|
||||||
_spec.SetField(attachment.FieldPrimary, field.TypeBool, value)
|
_spec.SetField(attachment.FieldPrimary, field.TypeBool, value)
|
||||||
_node.Primary = value
|
_node.Primary = value
|
||||||
}
|
}
|
||||||
if value, ok := _c.mutation.Title(); ok {
|
if nodes := ac.mutation.ItemIDs(); len(nodes) > 0 {
|
||||||
_spec.SetField(attachment.FieldTitle, field.TypeString, value)
|
|
||||||
_node.Title = value
|
|
||||||
}
|
|
||||||
if value, ok := _c.mutation.Path(); ok {
|
|
||||||
_spec.SetField(attachment.FieldPath, field.TypeString, value)
|
|
||||||
_node.Path = value
|
|
||||||
}
|
|
||||||
if value, ok := _c.mutation.MimeType(); ok {
|
|
||||||
_spec.SetField(attachment.FieldMimeType, field.TypeString, value)
|
|
||||||
_node.MimeType = value
|
|
||||||
}
|
|
||||||
if nodes := _c.mutation.ItemIDs(); len(nodes) > 0 {
|
|
||||||
edge := &sqlgraph.EdgeSpec{
|
edge := &sqlgraph.EdgeSpec{
|
||||||
Rel: sqlgraph.M2O,
|
Rel: sqlgraph.M2O,
|
||||||
Inverse: true,
|
Inverse: true,
|
||||||
@@ -349,21 +265,21 @@ func (_c *AttachmentCreate) createSpec() (*Attachment, *sqlgraph.CreateSpec) {
|
|||||||
_node.item_attachments = &nodes[0]
|
_node.item_attachments = &nodes[0]
|
||||||
_spec.Edges = append(_spec.Edges, edge)
|
_spec.Edges = append(_spec.Edges, edge)
|
||||||
}
|
}
|
||||||
if nodes := _c.mutation.ThumbnailIDs(); len(nodes) > 0 {
|
if nodes := ac.mutation.DocumentIDs(); len(nodes) > 0 {
|
||||||
edge := &sqlgraph.EdgeSpec{
|
edge := &sqlgraph.EdgeSpec{
|
||||||
Rel: sqlgraph.O2O,
|
Rel: sqlgraph.M2O,
|
||||||
Inverse: false,
|
Inverse: true,
|
||||||
Table: attachment.ThumbnailTable,
|
Table: attachment.DocumentTable,
|
||||||
Columns: []string{attachment.ThumbnailColumn},
|
Columns: []string{attachment.DocumentColumn},
|
||||||
Bidi: true,
|
Bidi: false,
|
||||||
Target: &sqlgraph.EdgeTarget{
|
Target: &sqlgraph.EdgeTarget{
|
||||||
IDSpec: sqlgraph.NewFieldSpec(attachment.FieldID, field.TypeUUID),
|
IDSpec: sqlgraph.NewFieldSpec(document.FieldID, field.TypeUUID),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, k := range nodes {
|
for _, k := range nodes {
|
||||||
edge.Target.Nodes = append(edge.Target.Nodes, k)
|
edge.Target.Nodes = append(edge.Target.Nodes, k)
|
||||||
}
|
}
|
||||||
_node.attachment_thumbnail = &nodes[0]
|
_node.document_attachments = &nodes[0]
|
||||||
_spec.Edges = append(_spec.Edges, edge)
|
_spec.Edges = append(_spec.Edges, edge)
|
||||||
}
|
}
|
||||||
return _node, _spec
|
return _node, _spec
|
||||||
@@ -377,16 +293,16 @@ type AttachmentCreateBulk struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save creates the Attachment entities in the database.
|
// Save creates the Attachment entities in the database.
|
||||||
func (_c *AttachmentCreateBulk) Save(ctx context.Context) ([]*Attachment, error) {
|
func (acb *AttachmentCreateBulk) Save(ctx context.Context) ([]*Attachment, error) {
|
||||||
if _c.err != nil {
|
if acb.err != nil {
|
||||||
return nil, _c.err
|
return nil, acb.err
|
||||||
}
|
}
|
||||||
specs := make([]*sqlgraph.CreateSpec, len(_c.builders))
|
specs := make([]*sqlgraph.CreateSpec, len(acb.builders))
|
||||||
nodes := make([]*Attachment, len(_c.builders))
|
nodes := make([]*Attachment, len(acb.builders))
|
||||||
mutators := make([]Mutator, len(_c.builders))
|
mutators := make([]Mutator, len(acb.builders))
|
||||||
for i := range _c.builders {
|
for i := range acb.builders {
|
||||||
func(i int, root context.Context) {
|
func(i int, root context.Context) {
|
||||||
builder := _c.builders[i]
|
builder := acb.builders[i]
|
||||||
builder.defaults()
|
builder.defaults()
|
||||||
var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
|
var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
|
||||||
mutation, ok := m.(*AttachmentMutation)
|
mutation, ok := m.(*AttachmentMutation)
|
||||||
@@ -400,11 +316,11 @@ func (_c *AttachmentCreateBulk) Save(ctx context.Context) ([]*Attachment, error)
|
|||||||
var err error
|
var err error
|
||||||
nodes[i], specs[i] = builder.createSpec()
|
nodes[i], specs[i] = builder.createSpec()
|
||||||
if i < len(mutators)-1 {
|
if i < len(mutators)-1 {
|
||||||
_, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation)
|
_, err = mutators[i+1].Mutate(root, acb.builders[i+1].mutation)
|
||||||
} else {
|
} else {
|
||||||
spec := &sqlgraph.BatchCreateSpec{Nodes: specs}
|
spec := &sqlgraph.BatchCreateSpec{Nodes: specs}
|
||||||
// Invoke the actual operation on the latest mutation in the chain.
|
// Invoke the actual operation on the latest mutation in the chain.
|
||||||
if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil {
|
if err = sqlgraph.BatchCreate(ctx, acb.driver, spec); err != nil {
|
||||||
if sqlgraph.IsConstraintError(err) {
|
if sqlgraph.IsConstraintError(err) {
|
||||||
err = &ConstraintError{msg: err.Error(), wrap: err}
|
err = &ConstraintError{msg: err.Error(), wrap: err}
|
||||||
}
|
}
|
||||||
@@ -424,7 +340,7 @@ func (_c *AttachmentCreateBulk) Save(ctx context.Context) ([]*Attachment, error)
|
|||||||
}(i, ctx)
|
}(i, ctx)
|
||||||
}
|
}
|
||||||
if len(mutators) > 0 {
|
if len(mutators) > 0 {
|
||||||
if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil {
|
if _, err := mutators[0].Mutate(ctx, acb.builders[0].mutation); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -432,8 +348,8 @@ func (_c *AttachmentCreateBulk) Save(ctx context.Context) ([]*Attachment, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SaveX is like Save, but panics if an error occurs.
|
// SaveX is like Save, but panics if an error occurs.
|
||||||
func (_c *AttachmentCreateBulk) SaveX(ctx context.Context) []*Attachment {
|
func (acb *AttachmentCreateBulk) SaveX(ctx context.Context) []*Attachment {
|
||||||
v, err := _c.Save(ctx)
|
v, err := acb.Save(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -441,14 +357,14 @@ func (_c *AttachmentCreateBulk) SaveX(ctx context.Context) []*Attachment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Exec executes the query.
|
// Exec executes the query.
|
||||||
func (_c *AttachmentCreateBulk) Exec(ctx context.Context) error {
|
func (acb *AttachmentCreateBulk) Exec(ctx context.Context) error {
|
||||||
_, err := _c.Save(ctx)
|
_, err := acb.Save(ctx)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecX is like Exec, but panics if an error occurs.
|
// ExecX is like Exec, but panics if an error occurs.
|
||||||
func (_c *AttachmentCreateBulk) ExecX(ctx context.Context) {
|
func (acb *AttachmentCreateBulk) ExecX(ctx context.Context) {
|
||||||
if err := _c.Exec(ctx); err != nil {
|
if err := acb.Exec(ctx); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,56 +20,56 @@ type AttachmentDelete struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Where appends a list predicates to the AttachmentDelete builder.
|
// Where appends a list predicates to the AttachmentDelete builder.
|
||||||
func (_d *AttachmentDelete) Where(ps ...predicate.Attachment) *AttachmentDelete {
|
func (ad *AttachmentDelete) Where(ps ...predicate.Attachment) *AttachmentDelete {
|
||||||
_d.mutation.Where(ps...)
|
ad.mutation.Where(ps...)
|
||||||
return _d
|
return ad
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exec executes the deletion query and returns how many vertices were deleted.
|
// Exec executes the deletion query and returns how many vertices were deleted.
|
||||||
func (_d *AttachmentDelete) Exec(ctx context.Context) (int, error) {
|
func (ad *AttachmentDelete) Exec(ctx context.Context) (int, error) {
|
||||||
return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks)
|
return withHooks(ctx, ad.sqlExec, ad.mutation, ad.hooks)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecX is like Exec, but panics if an error occurs.
|
// ExecX is like Exec, but panics if an error occurs.
|
||||||
func (_d *AttachmentDelete) ExecX(ctx context.Context) int {
|
func (ad *AttachmentDelete) ExecX(ctx context.Context) int {
|
||||||
n, err := _d.Exec(ctx)
|
n, err := ad.Exec(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_d *AttachmentDelete) sqlExec(ctx context.Context) (int, error) {
|
func (ad *AttachmentDelete) sqlExec(ctx context.Context) (int, error) {
|
||||||
_spec := sqlgraph.NewDeleteSpec(attachment.Table, sqlgraph.NewFieldSpec(attachment.FieldID, field.TypeUUID))
|
_spec := sqlgraph.NewDeleteSpec(attachment.Table, sqlgraph.NewFieldSpec(attachment.FieldID, field.TypeUUID))
|
||||||
if ps := _d.mutation.predicates; len(ps) > 0 {
|
if ps := ad.mutation.predicates; len(ps) > 0 {
|
||||||
_spec.Predicate = func(selector *sql.Selector) {
|
_spec.Predicate = func(selector *sql.Selector) {
|
||||||
for i := range ps {
|
for i := range ps {
|
||||||
ps[i](selector)
|
ps[i](selector)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec)
|
affected, err := sqlgraph.DeleteNodes(ctx, ad.driver, _spec)
|
||||||
if err != nil && sqlgraph.IsConstraintError(err) {
|
if err != nil && sqlgraph.IsConstraintError(err) {
|
||||||
err = &ConstraintError{msg: err.Error(), wrap: err}
|
err = &ConstraintError{msg: err.Error(), wrap: err}
|
||||||
}
|
}
|
||||||
_d.mutation.done = true
|
ad.mutation.done = true
|
||||||
return affected, err
|
return affected, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// AttachmentDeleteOne is the builder for deleting a single Attachment entity.
|
// AttachmentDeleteOne is the builder for deleting a single Attachment entity.
|
||||||
type AttachmentDeleteOne struct {
|
type AttachmentDeleteOne struct {
|
||||||
_d *AttachmentDelete
|
ad *AttachmentDelete
|
||||||
}
|
}
|
||||||
|
|
||||||
// Where appends a list predicates to the AttachmentDelete builder.
|
// Where appends a list predicates to the AttachmentDelete builder.
|
||||||
func (_d *AttachmentDeleteOne) Where(ps ...predicate.Attachment) *AttachmentDeleteOne {
|
func (ado *AttachmentDeleteOne) Where(ps ...predicate.Attachment) *AttachmentDeleteOne {
|
||||||
_d._d.mutation.Where(ps...)
|
ado.ad.mutation.Where(ps...)
|
||||||
return _d
|
return ado
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exec executes the deletion query.
|
// Exec executes the deletion query.
|
||||||
func (_d *AttachmentDeleteOne) Exec(ctx context.Context) error {
|
func (ado *AttachmentDeleteOne) Exec(ctx context.Context) error {
|
||||||
n, err := _d._d.Exec(ctx)
|
n, err := ado.ad.Exec(ctx)
|
||||||
switch {
|
switch {
|
||||||
case err != nil:
|
case err != nil:
|
||||||
return err
|
return err
|
||||||
@@ -81,8 +81,8 @@ func (_d *AttachmentDeleteOne) Exec(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ExecX is like Exec, but panics if an error occurs.
|
// ExecX is like Exec, but panics if an error occurs.
|
||||||
func (_d *AttachmentDeleteOne) ExecX(ctx context.Context) {
|
func (ado *AttachmentDeleteOne) ExecX(ctx context.Context) {
|
||||||
if err := _d.Exec(ctx); err != nil {
|
if err := ado.Exec(ctx); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
"entgo.io/ent"
|
|
||||||
"entgo.io/ent/dialect/sql"
|
"entgo.io/ent/dialect/sql"
|
||||||
"entgo.io/ent/dialect/sql/sqlgraph"
|
"entgo.io/ent/dialect/sql/sqlgraph"
|
||||||
"entgo.io/ent/schema/field"
|
"entgo.io/ent/schema/field"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/attachment"
|
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/attachment"
|
||||||
|
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/document"
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/item"
|
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/item"
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/predicate"
|
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/predicate"
|
||||||
)
|
)
|
||||||
@@ -20,57 +20,57 @@ import (
|
|||||||
// AttachmentQuery is the builder for querying Attachment entities.
|
// AttachmentQuery is the builder for querying Attachment entities.
|
||||||
type AttachmentQuery struct {
|
type AttachmentQuery struct {
|
||||||
config
|
config
|
||||||
ctx *QueryContext
|
ctx *QueryContext
|
||||||
order []attachment.OrderOption
|
order []attachment.OrderOption
|
||||||
inters []Interceptor
|
inters []Interceptor
|
||||||
predicates []predicate.Attachment
|
predicates []predicate.Attachment
|
||||||
withItem *ItemQuery
|
withItem *ItemQuery
|
||||||
withThumbnail *AttachmentQuery
|
withDocument *DocumentQuery
|
||||||
withFKs bool
|
withFKs bool
|
||||||
// intermediate query (i.e. traversal path).
|
// intermediate query (i.e. traversal path).
|
||||||
sql *sql.Selector
|
sql *sql.Selector
|
||||||
path func(context.Context) (*sql.Selector, error)
|
path func(context.Context) (*sql.Selector, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Where adds a new predicate for the AttachmentQuery builder.
|
// Where adds a new predicate for the AttachmentQuery builder.
|
||||||
func (_q *AttachmentQuery) Where(ps ...predicate.Attachment) *AttachmentQuery {
|
func (aq *AttachmentQuery) Where(ps ...predicate.Attachment) *AttachmentQuery {
|
||||||
_q.predicates = append(_q.predicates, ps...)
|
aq.predicates = append(aq.predicates, ps...)
|
||||||
return _q
|
return aq
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limit the number of records to be returned by this query.
|
// Limit the number of records to be returned by this query.
|
||||||
func (_q *AttachmentQuery) Limit(limit int) *AttachmentQuery {
|
func (aq *AttachmentQuery) Limit(limit int) *AttachmentQuery {
|
||||||
_q.ctx.Limit = &limit
|
aq.ctx.Limit = &limit
|
||||||
return _q
|
return aq
|
||||||
}
|
}
|
||||||
|
|
||||||
// Offset to start from.
|
// Offset to start from.
|
||||||
func (_q *AttachmentQuery) Offset(offset int) *AttachmentQuery {
|
func (aq *AttachmentQuery) Offset(offset int) *AttachmentQuery {
|
||||||
_q.ctx.Offset = &offset
|
aq.ctx.Offset = &offset
|
||||||
return _q
|
return aq
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unique configures the query builder to filter duplicate records on query.
|
// Unique configures the query builder to filter duplicate records on query.
|
||||||
// By default, unique is set to true, and can be disabled using this method.
|
// By default, unique is set to true, and can be disabled using this method.
|
||||||
func (_q *AttachmentQuery) Unique(unique bool) *AttachmentQuery {
|
func (aq *AttachmentQuery) Unique(unique bool) *AttachmentQuery {
|
||||||
_q.ctx.Unique = &unique
|
aq.ctx.Unique = &unique
|
||||||
return _q
|
return aq
|
||||||
}
|
}
|
||||||
|
|
||||||
// Order specifies how the records should be ordered.
|
// Order specifies how the records should be ordered.
|
||||||
func (_q *AttachmentQuery) Order(o ...attachment.OrderOption) *AttachmentQuery {
|
func (aq *AttachmentQuery) Order(o ...attachment.OrderOption) *AttachmentQuery {
|
||||||
_q.order = append(_q.order, o...)
|
aq.order = append(aq.order, o...)
|
||||||
return _q
|
return aq
|
||||||
}
|
}
|
||||||
|
|
||||||
// QueryItem chains the current query on the "item" edge.
|
// QueryItem chains the current query on the "item" edge.
|
||||||
func (_q *AttachmentQuery) QueryItem() *ItemQuery {
|
func (aq *AttachmentQuery) QueryItem() *ItemQuery {
|
||||||
query := (&ItemClient{config: _q.config}).Query()
|
query := (&ItemClient{config: aq.config}).Query()
|
||||||
query.path = func(ctx context.Context) (fromU *sql.Selector, err error) {
|
query.path = func(ctx context.Context) (fromU *sql.Selector, err error) {
|
||||||
if err := _q.prepareQuery(ctx); err != nil {
|
if err := aq.prepareQuery(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
selector := _q.sqlQuery(ctx)
|
selector := aq.sqlQuery(ctx)
|
||||||
if err := selector.Err(); err != nil {
|
if err := selector.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -79,29 +79,29 @@ func (_q *AttachmentQuery) QueryItem() *ItemQuery {
|
|||||||
sqlgraph.To(item.Table, item.FieldID),
|
sqlgraph.To(item.Table, item.FieldID),
|
||||||
sqlgraph.Edge(sqlgraph.M2O, true, attachment.ItemTable, attachment.ItemColumn),
|
sqlgraph.Edge(sqlgraph.M2O, true, attachment.ItemTable, attachment.ItemColumn),
|
||||||
)
|
)
|
||||||
fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step)
|
fromU = sqlgraph.SetNeighbors(aq.driver.Dialect(), step)
|
||||||
return fromU, nil
|
return fromU, nil
|
||||||
}
|
}
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
// QueryThumbnail chains the current query on the "thumbnail" edge.
|
// QueryDocument chains the current query on the "document" edge.
|
||||||
func (_q *AttachmentQuery) QueryThumbnail() *AttachmentQuery {
|
func (aq *AttachmentQuery) QueryDocument() *DocumentQuery {
|
||||||
query := (&AttachmentClient{config: _q.config}).Query()
|
query := (&DocumentClient{config: aq.config}).Query()
|
||||||
query.path = func(ctx context.Context) (fromU *sql.Selector, err error) {
|
query.path = func(ctx context.Context) (fromU *sql.Selector, err error) {
|
||||||
if err := _q.prepareQuery(ctx); err != nil {
|
if err := aq.prepareQuery(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
selector := _q.sqlQuery(ctx)
|
selector := aq.sqlQuery(ctx)
|
||||||
if err := selector.Err(); err != nil {
|
if err := selector.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
step := sqlgraph.NewStep(
|
step := sqlgraph.NewStep(
|
||||||
sqlgraph.From(attachment.Table, attachment.FieldID, selector),
|
sqlgraph.From(attachment.Table, attachment.FieldID, selector),
|
||||||
sqlgraph.To(attachment.Table, attachment.FieldID),
|
sqlgraph.To(document.Table, document.FieldID),
|
||||||
sqlgraph.Edge(sqlgraph.O2O, false, attachment.ThumbnailTable, attachment.ThumbnailColumn),
|
sqlgraph.Edge(sqlgraph.M2O, true, attachment.DocumentTable, attachment.DocumentColumn),
|
||||||
)
|
)
|
||||||
fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step)
|
fromU = sqlgraph.SetNeighbors(aq.driver.Dialect(), step)
|
||||||
return fromU, nil
|
return fromU, nil
|
||||||
}
|
}
|
||||||
return query
|
return query
|
||||||
@@ -109,8 +109,8 @@ func (_q *AttachmentQuery) QueryThumbnail() *AttachmentQuery {
|
|||||||
|
|
||||||
// First returns the first Attachment entity from the query.
|
// First returns the first Attachment entity from the query.
|
||||||
// Returns a *NotFoundError when no Attachment was found.
|
// Returns a *NotFoundError when no Attachment was found.
|
||||||
func (_q *AttachmentQuery) First(ctx context.Context) (*Attachment, error) {
|
func (aq *AttachmentQuery) First(ctx context.Context) (*Attachment, error) {
|
||||||
nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst))
|
nodes, err := aq.Limit(1).All(setContextOp(ctx, aq.ctx, "First"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -121,8 +121,8 @@ func (_q *AttachmentQuery) First(ctx context.Context) (*Attachment, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FirstX is like First, but panics if an error occurs.
|
// FirstX is like First, but panics if an error occurs.
|
||||||
func (_q *AttachmentQuery) FirstX(ctx context.Context) *Attachment {
|
func (aq *AttachmentQuery) FirstX(ctx context.Context) *Attachment {
|
||||||
node, err := _q.First(ctx)
|
node, err := aq.First(ctx)
|
||||||
if err != nil && !IsNotFound(err) {
|
if err != nil && !IsNotFound(err) {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -131,9 +131,9 @@ func (_q *AttachmentQuery) FirstX(ctx context.Context) *Attachment {
|
|||||||
|
|
||||||
// FirstID returns the first Attachment ID from the query.
|
// FirstID returns the first Attachment ID from the query.
|
||||||
// Returns a *NotFoundError when no Attachment ID was found.
|
// Returns a *NotFoundError when no Attachment ID was found.
|
||||||
func (_q *AttachmentQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) {
|
func (aq *AttachmentQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) {
|
||||||
var ids []uuid.UUID
|
var ids []uuid.UUID
|
||||||
if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil {
|
if ids, err = aq.Limit(1).IDs(setContextOp(ctx, aq.ctx, "FirstID")); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(ids) == 0 {
|
if len(ids) == 0 {
|
||||||
@@ -144,8 +144,8 @@ func (_q *AttachmentQuery) FirstID(ctx context.Context) (id uuid.UUID, err error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FirstIDX is like FirstID, but panics if an error occurs.
|
// FirstIDX is like FirstID, but panics if an error occurs.
|
||||||
func (_q *AttachmentQuery) FirstIDX(ctx context.Context) uuid.UUID {
|
func (aq *AttachmentQuery) FirstIDX(ctx context.Context) uuid.UUID {
|
||||||
id, err := _q.FirstID(ctx)
|
id, err := aq.FirstID(ctx)
|
||||||
if err != nil && !IsNotFound(err) {
|
if err != nil && !IsNotFound(err) {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -155,8 +155,8 @@ func (_q *AttachmentQuery) FirstIDX(ctx context.Context) uuid.UUID {
|
|||||||
// Only returns a single Attachment entity found by the query, ensuring it only returns one.
|
// Only returns a single Attachment entity found by the query, ensuring it only returns one.
|
||||||
// Returns a *NotSingularError when more than one Attachment entity is found.
|
// Returns a *NotSingularError when more than one Attachment entity is found.
|
||||||
// Returns a *NotFoundError when no Attachment entities are found.
|
// Returns a *NotFoundError when no Attachment entities are found.
|
||||||
func (_q *AttachmentQuery) Only(ctx context.Context) (*Attachment, error) {
|
func (aq *AttachmentQuery) Only(ctx context.Context) (*Attachment, error) {
|
||||||
nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly))
|
nodes, err := aq.Limit(2).All(setContextOp(ctx, aq.ctx, "Only"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -171,8 +171,8 @@ func (_q *AttachmentQuery) Only(ctx context.Context) (*Attachment, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OnlyX is like Only, but panics if an error occurs.
|
// OnlyX is like Only, but panics if an error occurs.
|
||||||
func (_q *AttachmentQuery) OnlyX(ctx context.Context) *Attachment {
|
func (aq *AttachmentQuery) OnlyX(ctx context.Context) *Attachment {
|
||||||
node, err := _q.Only(ctx)
|
node, err := aq.Only(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -182,9 +182,9 @@ func (_q *AttachmentQuery) OnlyX(ctx context.Context) *Attachment {
|
|||||||
// OnlyID is like Only, but returns the only Attachment ID in the query.
|
// OnlyID is like Only, but returns the only Attachment ID in the query.
|
||||||
// Returns a *NotSingularError when more than one Attachment ID is found.
|
// Returns a *NotSingularError when more than one Attachment ID is found.
|
||||||
// Returns a *NotFoundError when no entities are found.
|
// Returns a *NotFoundError when no entities are found.
|
||||||
func (_q *AttachmentQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) {
|
func (aq *AttachmentQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) {
|
||||||
var ids []uuid.UUID
|
var ids []uuid.UUID
|
||||||
if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil {
|
if ids, err = aq.Limit(2).IDs(setContextOp(ctx, aq.ctx, "OnlyID")); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch len(ids) {
|
switch len(ids) {
|
||||||
@@ -199,8 +199,8 @@ func (_q *AttachmentQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OnlyIDX is like OnlyID, but panics if an error occurs.
|
// OnlyIDX is like OnlyID, but panics if an error occurs.
|
||||||
func (_q *AttachmentQuery) OnlyIDX(ctx context.Context) uuid.UUID {
|
func (aq *AttachmentQuery) OnlyIDX(ctx context.Context) uuid.UUID {
|
||||||
id, err := _q.OnlyID(ctx)
|
id, err := aq.OnlyID(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -208,18 +208,18 @@ func (_q *AttachmentQuery) OnlyIDX(ctx context.Context) uuid.UUID {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// All executes the query and returns a list of Attachments.
|
// All executes the query and returns a list of Attachments.
|
||||||
func (_q *AttachmentQuery) All(ctx context.Context) ([]*Attachment, error) {
|
func (aq *AttachmentQuery) All(ctx context.Context) ([]*Attachment, error) {
|
||||||
ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll)
|
ctx = setContextOp(ctx, aq.ctx, "All")
|
||||||
if err := _q.prepareQuery(ctx); err != nil {
|
if err := aq.prepareQuery(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
qr := querierAll[[]*Attachment, *AttachmentQuery]()
|
qr := querierAll[[]*Attachment, *AttachmentQuery]()
|
||||||
return withInterceptors[[]*Attachment](ctx, _q, qr, _q.inters)
|
return withInterceptors[[]*Attachment](ctx, aq, qr, aq.inters)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AllX is like All, but panics if an error occurs.
|
// AllX is like All, but panics if an error occurs.
|
||||||
func (_q *AttachmentQuery) AllX(ctx context.Context) []*Attachment {
|
func (aq *AttachmentQuery) AllX(ctx context.Context) []*Attachment {
|
||||||
nodes, err := _q.All(ctx)
|
nodes, err := aq.All(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -227,20 +227,20 @@ func (_q *AttachmentQuery) AllX(ctx context.Context) []*Attachment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IDs executes the query and returns a list of Attachment IDs.
|
// IDs executes the query and returns a list of Attachment IDs.
|
||||||
func (_q *AttachmentQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) {
|
func (aq *AttachmentQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) {
|
||||||
if _q.ctx.Unique == nil && _q.path != nil {
|
if aq.ctx.Unique == nil && aq.path != nil {
|
||||||
_q.Unique(true)
|
aq.Unique(true)
|
||||||
}
|
}
|
||||||
ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs)
|
ctx = setContextOp(ctx, aq.ctx, "IDs")
|
||||||
if err = _q.Select(attachment.FieldID).Scan(ctx, &ids); err != nil {
|
if err = aq.Select(attachment.FieldID).Scan(ctx, &ids); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return ids, nil
|
return ids, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IDsX is like IDs, but panics if an error occurs.
|
// IDsX is like IDs, but panics if an error occurs.
|
||||||
func (_q *AttachmentQuery) IDsX(ctx context.Context) []uuid.UUID {
|
func (aq *AttachmentQuery) IDsX(ctx context.Context) []uuid.UUID {
|
||||||
ids, err := _q.IDs(ctx)
|
ids, err := aq.IDs(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -248,17 +248,17 @@ func (_q *AttachmentQuery) IDsX(ctx context.Context) []uuid.UUID {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Count returns the count of the given query.
|
// Count returns the count of the given query.
|
||||||
func (_q *AttachmentQuery) Count(ctx context.Context) (int, error) {
|
func (aq *AttachmentQuery) Count(ctx context.Context) (int, error) {
|
||||||
ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount)
|
ctx = setContextOp(ctx, aq.ctx, "Count")
|
||||||
if err := _q.prepareQuery(ctx); err != nil {
|
if err := aq.prepareQuery(ctx); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
return withInterceptors[int](ctx, _q, querierCount[*AttachmentQuery](), _q.inters)
|
return withInterceptors[int](ctx, aq, querierCount[*AttachmentQuery](), aq.inters)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CountX is like Count, but panics if an error occurs.
|
// CountX is like Count, but panics if an error occurs.
|
||||||
func (_q *AttachmentQuery) CountX(ctx context.Context) int {
|
func (aq *AttachmentQuery) CountX(ctx context.Context) int {
|
||||||
count, err := _q.Count(ctx)
|
count, err := aq.Count(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -266,9 +266,9 @@ func (_q *AttachmentQuery) CountX(ctx context.Context) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Exist returns true if the query has elements in the graph.
|
// Exist returns true if the query has elements in the graph.
|
||||||
func (_q *AttachmentQuery) Exist(ctx context.Context) (bool, error) {
|
func (aq *AttachmentQuery) Exist(ctx context.Context) (bool, error) {
|
||||||
ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist)
|
ctx = setContextOp(ctx, aq.ctx, "Exist")
|
||||||
switch _, err := _q.FirstID(ctx); {
|
switch _, err := aq.FirstID(ctx); {
|
||||||
case IsNotFound(err):
|
case IsNotFound(err):
|
||||||
return false, nil
|
return false, nil
|
||||||
case err != nil:
|
case err != nil:
|
||||||
@@ -279,8 +279,8 @@ func (_q *AttachmentQuery) Exist(ctx context.Context) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ExistX is like Exist, but panics if an error occurs.
|
// ExistX is like Exist, but panics if an error occurs.
|
||||||
func (_q *AttachmentQuery) ExistX(ctx context.Context) bool {
|
func (aq *AttachmentQuery) ExistX(ctx context.Context) bool {
|
||||||
exist, err := _q.Exist(ctx)
|
exist, err := aq.Exist(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -289,44 +289,44 @@ func (_q *AttachmentQuery) ExistX(ctx context.Context) bool {
|
|||||||
|
|
||||||
// Clone returns a duplicate of the AttachmentQuery builder, including all associated steps. It can be
|
// Clone returns a duplicate of the AttachmentQuery builder, including all associated steps. It can be
|
||||||
// used to prepare common query builders and use them differently after the clone is made.
|
// used to prepare common query builders and use them differently after the clone is made.
|
||||||
func (_q *AttachmentQuery) Clone() *AttachmentQuery {
|
func (aq *AttachmentQuery) Clone() *AttachmentQuery {
|
||||||
if _q == nil {
|
if aq == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &AttachmentQuery{
|
return &AttachmentQuery{
|
||||||
config: _q.config,
|
config: aq.config,
|
||||||
ctx: _q.ctx.Clone(),
|
ctx: aq.ctx.Clone(),
|
||||||
order: append([]attachment.OrderOption{}, _q.order...),
|
order: append([]attachment.OrderOption{}, aq.order...),
|
||||||
inters: append([]Interceptor{}, _q.inters...),
|
inters: append([]Interceptor{}, aq.inters...),
|
||||||
predicates: append([]predicate.Attachment{}, _q.predicates...),
|
predicates: append([]predicate.Attachment{}, aq.predicates...),
|
||||||
withItem: _q.withItem.Clone(),
|
withItem: aq.withItem.Clone(),
|
||||||
withThumbnail: _q.withThumbnail.Clone(),
|
withDocument: aq.withDocument.Clone(),
|
||||||
// clone intermediate query.
|
// clone intermediate query.
|
||||||
sql: _q.sql.Clone(),
|
sql: aq.sql.Clone(),
|
||||||
path: _q.path,
|
path: aq.path,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithItem tells the query-builder to eager-load the nodes that are connected to
|
// WithItem tells the query-builder to eager-load the nodes that are connected to
|
||||||
// the "item" edge. The optional arguments are used to configure the query builder of the edge.
|
// the "item" edge. The optional arguments are used to configure the query builder of the edge.
|
||||||
func (_q *AttachmentQuery) WithItem(opts ...func(*ItemQuery)) *AttachmentQuery {
|
func (aq *AttachmentQuery) WithItem(opts ...func(*ItemQuery)) *AttachmentQuery {
|
||||||
query := (&ItemClient{config: _q.config}).Query()
|
query := (&ItemClient{config: aq.config}).Query()
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(query)
|
opt(query)
|
||||||
}
|
}
|
||||||
_q.withItem = query
|
aq.withItem = query
|
||||||
return _q
|
return aq
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithThumbnail tells the query-builder to eager-load the nodes that are connected to
|
// WithDocument tells the query-builder to eager-load the nodes that are connected to
|
||||||
// the "thumbnail" edge. The optional arguments are used to configure the query builder of the edge.
|
// the "document" edge. The optional arguments are used to configure the query builder of the edge.
|
||||||
func (_q *AttachmentQuery) WithThumbnail(opts ...func(*AttachmentQuery)) *AttachmentQuery {
|
func (aq *AttachmentQuery) WithDocument(opts ...func(*DocumentQuery)) *AttachmentQuery {
|
||||||
query := (&AttachmentClient{config: _q.config}).Query()
|
query := (&DocumentClient{config: aq.config}).Query()
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(query)
|
opt(query)
|
||||||
}
|
}
|
||||||
_q.withThumbnail = query
|
aq.withDocument = query
|
||||||
return _q
|
return aq
|
||||||
}
|
}
|
||||||
|
|
||||||
// GroupBy is used to group vertices by one or more fields/columns.
|
// GroupBy is used to group vertices by one or more fields/columns.
|
||||||
@@ -343,10 +343,10 @@ func (_q *AttachmentQuery) WithThumbnail(opts ...func(*AttachmentQuery)) *Attach
|
|||||||
// GroupBy(attachment.FieldCreatedAt).
|
// GroupBy(attachment.FieldCreatedAt).
|
||||||
// Aggregate(ent.Count()).
|
// Aggregate(ent.Count()).
|
||||||
// Scan(ctx, &v)
|
// Scan(ctx, &v)
|
||||||
func (_q *AttachmentQuery) GroupBy(field string, fields ...string) *AttachmentGroupBy {
|
func (aq *AttachmentQuery) GroupBy(field string, fields ...string) *AttachmentGroupBy {
|
||||||
_q.ctx.Fields = append([]string{field}, fields...)
|
aq.ctx.Fields = append([]string{field}, fields...)
|
||||||
grbuild := &AttachmentGroupBy{build: _q}
|
grbuild := &AttachmentGroupBy{build: aq}
|
||||||
grbuild.flds = &_q.ctx.Fields
|
grbuild.flds = &aq.ctx.Fields
|
||||||
grbuild.label = attachment.Label
|
grbuild.label = attachment.Label
|
||||||
grbuild.scan = grbuild.Scan
|
grbuild.scan = grbuild.Scan
|
||||||
return grbuild
|
return grbuild
|
||||||
@@ -364,56 +364,56 @@ func (_q *AttachmentQuery) GroupBy(field string, fields ...string) *AttachmentGr
|
|||||||
// client.Attachment.Query().
|
// client.Attachment.Query().
|
||||||
// Select(attachment.FieldCreatedAt).
|
// Select(attachment.FieldCreatedAt).
|
||||||
// Scan(ctx, &v)
|
// Scan(ctx, &v)
|
||||||
func (_q *AttachmentQuery) Select(fields ...string) *AttachmentSelect {
|
func (aq *AttachmentQuery) Select(fields ...string) *AttachmentSelect {
|
||||||
_q.ctx.Fields = append(_q.ctx.Fields, fields...)
|
aq.ctx.Fields = append(aq.ctx.Fields, fields...)
|
||||||
sbuild := &AttachmentSelect{AttachmentQuery: _q}
|
sbuild := &AttachmentSelect{AttachmentQuery: aq}
|
||||||
sbuild.label = attachment.Label
|
sbuild.label = attachment.Label
|
||||||
sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan
|
sbuild.flds, sbuild.scan = &aq.ctx.Fields, sbuild.Scan
|
||||||
return sbuild
|
return sbuild
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aggregate returns a AttachmentSelect configured with the given aggregations.
|
// Aggregate returns a AttachmentSelect configured with the given aggregations.
|
||||||
func (_q *AttachmentQuery) Aggregate(fns ...AggregateFunc) *AttachmentSelect {
|
func (aq *AttachmentQuery) Aggregate(fns ...AggregateFunc) *AttachmentSelect {
|
||||||
return _q.Select().Aggregate(fns...)
|
return aq.Select().Aggregate(fns...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_q *AttachmentQuery) prepareQuery(ctx context.Context) error {
|
func (aq *AttachmentQuery) prepareQuery(ctx context.Context) error {
|
||||||
for _, inter := range _q.inters {
|
for _, inter := range aq.inters {
|
||||||
if inter == nil {
|
if inter == nil {
|
||||||
return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)")
|
return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)")
|
||||||
}
|
}
|
||||||
if trv, ok := inter.(Traverser); ok {
|
if trv, ok := inter.(Traverser); ok {
|
||||||
if err := trv.Traverse(ctx, _q); err != nil {
|
if err := trv.Traverse(ctx, aq); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, f := range _q.ctx.Fields {
|
for _, f := range aq.ctx.Fields {
|
||||||
if !attachment.ValidColumn(f) {
|
if !attachment.ValidColumn(f) {
|
||||||
return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)}
|
return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _q.path != nil {
|
if aq.path != nil {
|
||||||
prev, err := _q.path(ctx)
|
prev, err := aq.path(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_q.sql = prev
|
aq.sql = prev
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_q *AttachmentQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Attachment, error) {
|
func (aq *AttachmentQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Attachment, error) {
|
||||||
var (
|
var (
|
||||||
nodes = []*Attachment{}
|
nodes = []*Attachment{}
|
||||||
withFKs = _q.withFKs
|
withFKs = aq.withFKs
|
||||||
_spec = _q.querySpec()
|
_spec = aq.querySpec()
|
||||||
loadedTypes = [2]bool{
|
loadedTypes = [2]bool{
|
||||||
_q.withItem != nil,
|
aq.withItem != nil,
|
||||||
_q.withThumbnail != nil,
|
aq.withDocument != nil,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if _q.withItem != nil || _q.withThumbnail != nil {
|
if aq.withItem != nil || aq.withDocument != nil {
|
||||||
withFKs = true
|
withFKs = true
|
||||||
}
|
}
|
||||||
if withFKs {
|
if withFKs {
|
||||||
@@ -423,7 +423,7 @@ func (_q *AttachmentQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*A
|
|||||||
return (*Attachment).scanValues(nil, columns)
|
return (*Attachment).scanValues(nil, columns)
|
||||||
}
|
}
|
||||||
_spec.Assign = func(columns []string, values []any) error {
|
_spec.Assign = func(columns []string, values []any) error {
|
||||||
node := &Attachment{config: _q.config}
|
node := &Attachment{config: aq.config}
|
||||||
nodes = append(nodes, node)
|
nodes = append(nodes, node)
|
||||||
node.Edges.loadedTypes = loadedTypes
|
node.Edges.loadedTypes = loadedTypes
|
||||||
return node.assignValues(columns, values)
|
return node.assignValues(columns, values)
|
||||||
@@ -431,28 +431,28 @@ func (_q *AttachmentQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*A
|
|||||||
for i := range hooks {
|
for i := range hooks {
|
||||||
hooks[i](ctx, _spec)
|
hooks[i](ctx, _spec)
|
||||||
}
|
}
|
||||||
if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil {
|
if err := sqlgraph.QueryNodes(ctx, aq.driver, _spec); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(nodes) == 0 {
|
if len(nodes) == 0 {
|
||||||
return nodes, nil
|
return nodes, nil
|
||||||
}
|
}
|
||||||
if query := _q.withItem; query != nil {
|
if query := aq.withItem; query != nil {
|
||||||
if err := _q.loadItem(ctx, query, nodes, nil,
|
if err := aq.loadItem(ctx, query, nodes, nil,
|
||||||
func(n *Attachment, e *Item) { n.Edges.Item = e }); err != nil {
|
func(n *Attachment, e *Item) { n.Edges.Item = e }); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if query := _q.withThumbnail; query != nil {
|
if query := aq.withDocument; query != nil {
|
||||||
if err := _q.loadThumbnail(ctx, query, nodes, nil,
|
if err := aq.loadDocument(ctx, query, nodes, nil,
|
||||||
func(n *Attachment, e *Attachment) { n.Edges.Thumbnail = e }); err != nil {
|
func(n *Attachment, e *Document) { n.Edges.Document = e }); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nodes, nil
|
return nodes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_q *AttachmentQuery) loadItem(ctx context.Context, query *ItemQuery, nodes []*Attachment, init func(*Attachment), assign func(*Attachment, *Item)) error {
|
func (aq *AttachmentQuery) loadItem(ctx context.Context, query *ItemQuery, nodes []*Attachment, init func(*Attachment), assign func(*Attachment, *Item)) error {
|
||||||
ids := make([]uuid.UUID, 0, len(nodes))
|
ids := make([]uuid.UUID, 0, len(nodes))
|
||||||
nodeids := make(map[uuid.UUID][]*Attachment)
|
nodeids := make(map[uuid.UUID][]*Attachment)
|
||||||
for i := range nodes {
|
for i := range nodes {
|
||||||
@@ -484,14 +484,14 @@ func (_q *AttachmentQuery) loadItem(ctx context.Context, query *ItemQuery, nodes
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (_q *AttachmentQuery) loadThumbnail(ctx context.Context, query *AttachmentQuery, nodes []*Attachment, init func(*Attachment), assign func(*Attachment, *Attachment)) error {
|
func (aq *AttachmentQuery) loadDocument(ctx context.Context, query *DocumentQuery, nodes []*Attachment, init func(*Attachment), assign func(*Attachment, *Document)) error {
|
||||||
ids := make([]uuid.UUID, 0, len(nodes))
|
ids := make([]uuid.UUID, 0, len(nodes))
|
||||||
nodeids := make(map[uuid.UUID][]*Attachment)
|
nodeids := make(map[uuid.UUID][]*Attachment)
|
||||||
for i := range nodes {
|
for i := range nodes {
|
||||||
if nodes[i].attachment_thumbnail == nil {
|
if nodes[i].document_attachments == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fk := *nodes[i].attachment_thumbnail
|
fk := *nodes[i].document_attachments
|
||||||
if _, ok := nodeids[fk]; !ok {
|
if _, ok := nodeids[fk]; !ok {
|
||||||
ids = append(ids, fk)
|
ids = append(ids, fk)
|
||||||
}
|
}
|
||||||
@@ -500,7 +500,7 @@ func (_q *AttachmentQuery) loadThumbnail(ctx context.Context, query *AttachmentQ
|
|||||||
if len(ids) == 0 {
|
if len(ids) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
query.Where(attachment.IDIn(ids...))
|
query.Where(document.IDIn(ids...))
|
||||||
neighbors, err := query.All(ctx)
|
neighbors, err := query.All(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -508,7 +508,7 @@ func (_q *AttachmentQuery) loadThumbnail(ctx context.Context, query *AttachmentQ
|
|||||||
for _, n := range neighbors {
|
for _, n := range neighbors {
|
||||||
nodes, ok := nodeids[n.ID]
|
nodes, ok := nodeids[n.ID]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf(`unexpected foreign-key "attachment_thumbnail" returned %v`, n.ID)
|
return fmt.Errorf(`unexpected foreign-key "document_attachments" returned %v`, n.ID)
|
||||||
}
|
}
|
||||||
for i := range nodes {
|
for i := range nodes {
|
||||||
assign(nodes[i], n)
|
assign(nodes[i], n)
|
||||||
@@ -517,24 +517,24 @@ func (_q *AttachmentQuery) loadThumbnail(ctx context.Context, query *AttachmentQ
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_q *AttachmentQuery) sqlCount(ctx context.Context) (int, error) {
|
func (aq *AttachmentQuery) sqlCount(ctx context.Context) (int, error) {
|
||||||
_spec := _q.querySpec()
|
_spec := aq.querySpec()
|
||||||
_spec.Node.Columns = _q.ctx.Fields
|
_spec.Node.Columns = aq.ctx.Fields
|
||||||
if len(_q.ctx.Fields) > 0 {
|
if len(aq.ctx.Fields) > 0 {
|
||||||
_spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique
|
_spec.Unique = aq.ctx.Unique != nil && *aq.ctx.Unique
|
||||||
}
|
}
|
||||||
return sqlgraph.CountNodes(ctx, _q.driver, _spec)
|
return sqlgraph.CountNodes(ctx, aq.driver, _spec)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_q *AttachmentQuery) querySpec() *sqlgraph.QuerySpec {
|
func (aq *AttachmentQuery) querySpec() *sqlgraph.QuerySpec {
|
||||||
_spec := sqlgraph.NewQuerySpec(attachment.Table, attachment.Columns, sqlgraph.NewFieldSpec(attachment.FieldID, field.TypeUUID))
|
_spec := sqlgraph.NewQuerySpec(attachment.Table, attachment.Columns, sqlgraph.NewFieldSpec(attachment.FieldID, field.TypeUUID))
|
||||||
_spec.From = _q.sql
|
_spec.From = aq.sql
|
||||||
if unique := _q.ctx.Unique; unique != nil {
|
if unique := aq.ctx.Unique; unique != nil {
|
||||||
_spec.Unique = *unique
|
_spec.Unique = *unique
|
||||||
} else if _q.path != nil {
|
} else if aq.path != nil {
|
||||||
_spec.Unique = true
|
_spec.Unique = true
|
||||||
}
|
}
|
||||||
if fields := _q.ctx.Fields; len(fields) > 0 {
|
if fields := aq.ctx.Fields; len(fields) > 0 {
|
||||||
_spec.Node.Columns = make([]string, 0, len(fields))
|
_spec.Node.Columns = make([]string, 0, len(fields))
|
||||||
_spec.Node.Columns = append(_spec.Node.Columns, attachment.FieldID)
|
_spec.Node.Columns = append(_spec.Node.Columns, attachment.FieldID)
|
||||||
for i := range fields {
|
for i := range fields {
|
||||||
@@ -543,20 +543,20 @@ func (_q *AttachmentQuery) querySpec() *sqlgraph.QuerySpec {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ps := _q.predicates; len(ps) > 0 {
|
if ps := aq.predicates; len(ps) > 0 {
|
||||||
_spec.Predicate = func(selector *sql.Selector) {
|
_spec.Predicate = func(selector *sql.Selector) {
|
||||||
for i := range ps {
|
for i := range ps {
|
||||||
ps[i](selector)
|
ps[i](selector)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if limit := _q.ctx.Limit; limit != nil {
|
if limit := aq.ctx.Limit; limit != nil {
|
||||||
_spec.Limit = *limit
|
_spec.Limit = *limit
|
||||||
}
|
}
|
||||||
if offset := _q.ctx.Offset; offset != nil {
|
if offset := aq.ctx.Offset; offset != nil {
|
||||||
_spec.Offset = *offset
|
_spec.Offset = *offset
|
||||||
}
|
}
|
||||||
if ps := _q.order; len(ps) > 0 {
|
if ps := aq.order; len(ps) > 0 {
|
||||||
_spec.Order = func(selector *sql.Selector) {
|
_spec.Order = func(selector *sql.Selector) {
|
||||||
for i := range ps {
|
for i := range ps {
|
||||||
ps[i](selector)
|
ps[i](selector)
|
||||||
@@ -566,33 +566,33 @@ func (_q *AttachmentQuery) querySpec() *sqlgraph.QuerySpec {
|
|||||||
return _spec
|
return _spec
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_q *AttachmentQuery) sqlQuery(ctx context.Context) *sql.Selector {
|
func (aq *AttachmentQuery) sqlQuery(ctx context.Context) *sql.Selector {
|
||||||
builder := sql.Dialect(_q.driver.Dialect())
|
builder := sql.Dialect(aq.driver.Dialect())
|
||||||
t1 := builder.Table(attachment.Table)
|
t1 := builder.Table(attachment.Table)
|
||||||
columns := _q.ctx.Fields
|
columns := aq.ctx.Fields
|
||||||
if len(columns) == 0 {
|
if len(columns) == 0 {
|
||||||
columns = attachment.Columns
|
columns = attachment.Columns
|
||||||
}
|
}
|
||||||
selector := builder.Select(t1.Columns(columns...)...).From(t1)
|
selector := builder.Select(t1.Columns(columns...)...).From(t1)
|
||||||
if _q.sql != nil {
|
if aq.sql != nil {
|
||||||
selector = _q.sql
|
selector = aq.sql
|
||||||
selector.Select(selector.Columns(columns...)...)
|
selector.Select(selector.Columns(columns...)...)
|
||||||
}
|
}
|
||||||
if _q.ctx.Unique != nil && *_q.ctx.Unique {
|
if aq.ctx.Unique != nil && *aq.ctx.Unique {
|
||||||
selector.Distinct()
|
selector.Distinct()
|
||||||
}
|
}
|
||||||
for _, p := range _q.predicates {
|
for _, p := range aq.predicates {
|
||||||
p(selector)
|
p(selector)
|
||||||
}
|
}
|
||||||
for _, p := range _q.order {
|
for _, p := range aq.order {
|
||||||
p(selector)
|
p(selector)
|
||||||
}
|
}
|
||||||
if offset := _q.ctx.Offset; offset != nil {
|
if offset := aq.ctx.Offset; offset != nil {
|
||||||
// limit is mandatory for offset clause. We start
|
// limit is mandatory for offset clause. We start
|
||||||
// with default value, and override it below if needed.
|
// with default value, and override it below if needed.
|
||||||
selector.Offset(*offset).Limit(math.MaxInt32)
|
selector.Offset(*offset).Limit(math.MaxInt32)
|
||||||
}
|
}
|
||||||
if limit := _q.ctx.Limit; limit != nil {
|
if limit := aq.ctx.Limit; limit != nil {
|
||||||
selector.Limit(*limit)
|
selector.Limit(*limit)
|
||||||
}
|
}
|
||||||
return selector
|
return selector
|
||||||
@@ -605,41 +605,41 @@ type AttachmentGroupBy struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Aggregate adds the given aggregation functions to the group-by query.
|
// Aggregate adds the given aggregation functions to the group-by query.
|
||||||
func (_g *AttachmentGroupBy) Aggregate(fns ...AggregateFunc) *AttachmentGroupBy {
|
func (agb *AttachmentGroupBy) Aggregate(fns ...AggregateFunc) *AttachmentGroupBy {
|
||||||
_g.fns = append(_g.fns, fns...)
|
agb.fns = append(agb.fns, fns...)
|
||||||
return _g
|
return agb
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan applies the selector query and scans the result into the given value.
|
// Scan applies the selector query and scans the result into the given value.
|
||||||
func (_g *AttachmentGroupBy) Scan(ctx context.Context, v any) error {
|
func (agb *AttachmentGroupBy) Scan(ctx context.Context, v any) error {
|
||||||
ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy)
|
ctx = setContextOp(ctx, agb.build.ctx, "GroupBy")
|
||||||
if err := _g.build.prepareQuery(ctx); err != nil {
|
if err := agb.build.prepareQuery(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return scanWithInterceptors[*AttachmentQuery, *AttachmentGroupBy](ctx, _g.build, _g, _g.build.inters, v)
|
return scanWithInterceptors[*AttachmentQuery, *AttachmentGroupBy](ctx, agb.build, agb, agb.build.inters, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_g *AttachmentGroupBy) sqlScan(ctx context.Context, root *AttachmentQuery, v any) error {
|
func (agb *AttachmentGroupBy) sqlScan(ctx context.Context, root *AttachmentQuery, v any) error {
|
||||||
selector := root.sqlQuery(ctx).Select()
|
selector := root.sqlQuery(ctx).Select()
|
||||||
aggregation := make([]string, 0, len(_g.fns))
|
aggregation := make([]string, 0, len(agb.fns))
|
||||||
for _, fn := range _g.fns {
|
for _, fn := range agb.fns {
|
||||||
aggregation = append(aggregation, fn(selector))
|
aggregation = append(aggregation, fn(selector))
|
||||||
}
|
}
|
||||||
if len(selector.SelectedColumns()) == 0 {
|
if len(selector.SelectedColumns()) == 0 {
|
||||||
columns := make([]string, 0, len(*_g.flds)+len(_g.fns))
|
columns := make([]string, 0, len(*agb.flds)+len(agb.fns))
|
||||||
for _, f := range *_g.flds {
|
for _, f := range *agb.flds {
|
||||||
columns = append(columns, selector.C(f))
|
columns = append(columns, selector.C(f))
|
||||||
}
|
}
|
||||||
columns = append(columns, aggregation...)
|
columns = append(columns, aggregation...)
|
||||||
selector.Select(columns...)
|
selector.Select(columns...)
|
||||||
}
|
}
|
||||||
selector.GroupBy(selector.Columns(*_g.flds...)...)
|
selector.GroupBy(selector.Columns(*agb.flds...)...)
|
||||||
if err := selector.Err(); err != nil {
|
if err := selector.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rows := &sql.Rows{}
|
rows := &sql.Rows{}
|
||||||
query, args := selector.Query()
|
query, args := selector.Query()
|
||||||
if err := _g.build.driver.Query(ctx, query, args, rows); err != nil {
|
if err := agb.build.driver.Query(ctx, query, args, rows); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
@@ -653,27 +653,27 @@ type AttachmentSelect struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Aggregate adds the given aggregation functions to the selector query.
|
// Aggregate adds the given aggregation functions to the selector query.
|
||||||
func (_s *AttachmentSelect) Aggregate(fns ...AggregateFunc) *AttachmentSelect {
|
func (as *AttachmentSelect) Aggregate(fns ...AggregateFunc) *AttachmentSelect {
|
||||||
_s.fns = append(_s.fns, fns...)
|
as.fns = append(as.fns, fns...)
|
||||||
return _s
|
return as
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan applies the selector query and scans the result into the given value.
|
// Scan applies the selector query and scans the result into the given value.
|
||||||
func (_s *AttachmentSelect) Scan(ctx context.Context, v any) error {
|
func (as *AttachmentSelect) Scan(ctx context.Context, v any) error {
|
||||||
ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect)
|
ctx = setContextOp(ctx, as.ctx, "Select")
|
||||||
if err := _s.prepareQuery(ctx); err != nil {
|
if err := as.prepareQuery(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return scanWithInterceptors[*AttachmentQuery, *AttachmentSelect](ctx, _s.AttachmentQuery, _s, _s.inters, v)
|
return scanWithInterceptors[*AttachmentQuery, *AttachmentSelect](ctx, as.AttachmentQuery, as, as.inters, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_s *AttachmentSelect) sqlScan(ctx context.Context, root *AttachmentQuery, v any) error {
|
func (as *AttachmentSelect) sqlScan(ctx context.Context, root *AttachmentQuery, v any) error {
|
||||||
selector := root.sqlQuery(ctx)
|
selector := root.sqlQuery(ctx)
|
||||||
aggregation := make([]string, 0, len(_s.fns))
|
aggregation := make([]string, 0, len(as.fns))
|
||||||
for _, fn := range _s.fns {
|
for _, fn := range as.fns {
|
||||||
aggregation = append(aggregation, fn(selector))
|
aggregation = append(aggregation, fn(selector))
|
||||||
}
|
}
|
||||||
switch n := len(*_s.selector.flds); {
|
switch n := len(*as.selector.flds); {
|
||||||
case n == 0 && len(aggregation) > 0:
|
case n == 0 && len(aggregation) > 0:
|
||||||
selector.Select(aggregation...)
|
selector.Select(aggregation...)
|
||||||
case n != 0 && len(aggregation) > 0:
|
case n != 0 && len(aggregation) > 0:
|
||||||
@@ -681,7 +681,7 @@ func (_s *AttachmentSelect) sqlScan(ctx context.Context, root *AttachmentQuery,
|
|||||||
}
|
}
|
||||||
rows := &sql.Rows{}
|
rows := &sql.Rows{}
|
||||||
query, args := selector.Query()
|
query, args := selector.Query()
|
||||||
if err := _s.driver.Query(ctx, query, args, rows); err != nil {
|
if err := as.driver.Query(ctx, query, args, rows); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|||||||