mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-21 21:33:02 +01:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d34557f69 | ||
|
|
97a34475c8 | ||
|
|
18488f5b15 | ||
|
|
434f1fa411 | ||
|
|
57f9372e49 | ||
|
|
dbaaf4ad0a | ||
|
|
5596740cd2 | ||
|
|
bb86a51b05 | ||
|
|
72bdf524c2 | ||
|
|
461be2afca | ||
|
|
1cc38d6a5c | ||
|
|
50bd2ab86e | ||
|
|
5adb8fbad7 | ||
|
|
dea2dcfde8 | ||
|
|
ba8367f637 | ||
|
|
b87cdc8164 | ||
|
|
0f51e51f63 | ||
|
|
1279028d07 | ||
|
|
84bf67079b | ||
|
|
889197994b | ||
|
|
ae73b194c4 | ||
|
|
30014a77ca | ||
|
|
1b20a69c5e | ||
|
|
a8e1d2c447 | ||
|
|
14f1b93d38 | ||
|
|
92368dabf8 | ||
|
|
eca071f974 | ||
|
|
48a719d385 | ||
|
|
92c29a37e1 | ||
|
|
ea4e2f6da4 | ||
|
|
28a7adbffe | ||
|
|
9e19e89de4 | ||
|
|
e48109e530 | ||
|
|
e62529b314 | ||
|
|
b0f184a1b1 | ||
|
|
6960b235ea | ||
|
|
3198800329 | ||
|
|
1a69265601 | ||
|
|
2dc765efdf | ||
|
|
6a6e9e842e | ||
|
|
cc78b19d6e | ||
|
|
6b1110b3c5 | ||
|
|
3aae71ee18 | ||
|
|
340b9ac43d | ||
|
|
00c3e1bfc9 |
5
.devcontainer/Dockerfile
Normal file
5
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
||||
# [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
|
||||
40
.devcontainer/devcontainer.json
Normal file
40
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,40 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/typescript-node
|
||||
{
|
||||
"name": "Node.js & TypeScript",
|
||||
"build": {
|
||||
"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.
|
||||
"customizations": {
|
||||
// Configure properties specific to VS Code.
|
||||
"vscode": {
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"forwardPorts": [
|
||||
7745,
|
||||
3000
|
||||
],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "go install github.com/go-task/task/v3/cmd/task@latest && npm install -g pnpm && task setup",
|
||||
|
||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "node",
|
||||
"features": {
|
||||
"golang": "1.19"
|
||||
}
|
||||
}
|
||||
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
github: [hay-kot]
|
||||
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,38 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
61
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
61
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
name: "Bug Report"
|
||||
description: "Submit a bug report for the current release"
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: First Check
|
||||
description: Please confirm and check all the following options.
|
||||
options:
|
||||
- label: This is not a feature request
|
||||
required: true
|
||||
- label: I added a very descriptive title to this issue.
|
||||
required: true
|
||||
- label: I used the GitHub search to find a similar issue and didn't find it.
|
||||
required: true
|
||||
- label: I searched the documentation, with the integrated search.
|
||||
required: true
|
||||
- label: I already read the docs and didn't find an answer.
|
||||
required: true
|
||||
- type: input
|
||||
id: homebox-version
|
||||
attributes:
|
||||
label: Homebox Version
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: What is the issue you are experiencing?
|
||||
placeholder: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
description: If you do not provide a way to reproduce the issue, your issue will likely be closed.
|
||||
label: How can the maintainer reproduce the issue?
|
||||
placeholder: A clear step-by-step guide on how to reproduce the issue.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: Deployment
|
||||
description: What Deployment system are you using?
|
||||
multiple: true
|
||||
options:
|
||||
- Docker (Linux)
|
||||
- Docker (Windows)
|
||||
- Docker (Synology)
|
||||
- Unraid
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: os-details
|
||||
attributes:
|
||||
label: Deployment Details
|
||||
description: You can add more details about your operating system here, in particular if you chose "Other". If you are experiencing issues with deployment, please provide your docker-compose or docker commands
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
37
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
37
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: "Feature Request"
|
||||
description: "Submit a feature request for the current release"
|
||||
labels: ["feature-request"]
|
||||
body:
|
||||
- type: textarea
|
||||
id: problem-statement
|
||||
attributes:
|
||||
label: What is the problem you are trying to solve with this feature?
|
||||
placeholder: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
- type: textarea
|
||||
id: feature-solution
|
||||
attributes:
|
||||
label: What is the solution you are proposing?
|
||||
placeholder: A clear and concise description of what you want to happen.
|
||||
- type: textarea
|
||||
id: feature-alternatives
|
||||
attributes:
|
||||
label: What alternatives have you considered?
|
||||
placeholder: A clear and concise description of any alternative solutions or features you've considered.
|
||||
- type: textarea
|
||||
id: feature-details
|
||||
attributes:
|
||||
label: Additional context
|
||||
placeholder: Add any other context or screenshots about the feature request here.
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: Contributions
|
||||
description: Please confirm the following
|
||||
options:
|
||||
- label: I have searched through existing issues and feature requests to see if my idea has already been proposed.
|
||||
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
|
||||
21
.github/workflows/partial-publish.yaml
vendored
21
.github/workflows/partial-publish.yaml
vendored
@@ -44,28 +44,23 @@ jobs:
|
||||
env:
|
||||
CR_PAT: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
push: true
|
||||
tags: user/app:latest
|
||||
|
||||
- name: build nightly the image
|
||||
if: ${{ inputs.release == false }}
|
||||
run: |
|
||||
docker build --push \
|
||||
--tag ghcr.io/hay-kot/homebox:{{ inputs.tag }} \
|
||||
--build-arg COMMIT=$(git rev-parse HEAD) \
|
||||
--build-arg BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
|
||||
--platform linux/amd64,linux/arm64,linux/arm/v7 .
|
||||
docker build --push --no-cache \
|
||||
--tag=ghcr.io/hay-kot/homebox:${{ inputs.tag }} \
|
||||
--build-arg=COMMIT=$(git rev-parse HEAD) \
|
||||
--build-arg=BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
|
||||
--platform=linux/amd64,linux/arm64,linux/arm/v7 .
|
||||
|
||||
- name: build release tagged the image
|
||||
if: ${{ inputs.release == true }}
|
||||
run: |
|
||||
docker build --push \
|
||||
docker build --push --no-cache \
|
||||
--tag ghcr.io/hay-kot/homebox:nightly \
|
||||
--tag ghcr.io/hay-kot/homebox:latest \
|
||||
--tag ghcr.io/hay-kot/homebox:{{ inputs.tag }} \
|
||||
--tag ghcr.io/hay-kot/homebox:${{ inputs.tag }} \
|
||||
--build-arg VERSION=${{ inputs.tag }} \
|
||||
--build-arg COMMIT=$(git rev-parse HEAD) \
|
||||
--build-arg BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
|
||||
--platform linux/amd64,linux/arm64,linux/arm/v7 .
|
||||
|
||||
25
.github/workflows/publish.yaml
vendored
25
.github/workflows/publish.yaml
vendored
@@ -31,9 +31,21 @@ jobs:
|
||||
- uses: superfly/flyctl-actions/setup-flyctl@master
|
||||
- run: flyctl deploy --remote-only
|
||||
|
||||
publish-nightly:
|
||||
name: "Publish Nightly"
|
||||
if: github.event_name != 'release'
|
||||
needs:
|
||||
- backend-tests
|
||||
- frontend-tests
|
||||
uses: hay-kot/homebox/.github/workflows/partial-publish.yaml@main
|
||||
with:
|
||||
tag: nightly
|
||||
secrets:
|
||||
GH_TOKEN: ${{ secrets.CR_PAT }}
|
||||
|
||||
publish-tag:
|
||||
if: github.event_name == 'release'
|
||||
name: "Publish Tag"
|
||||
if: github.event_name == 'release'
|
||||
needs:
|
||||
- backend-tests
|
||||
- frontend-tests
|
||||
@@ -44,17 +56,6 @@ jobs:
|
||||
secrets:
|
||||
GH_TOKEN: ${{ secrets.CR_PAT }}
|
||||
|
||||
publish-nightly:
|
||||
name: "Publish Nightly"
|
||||
needs:
|
||||
- backend-tests
|
||||
- frontend-tests
|
||||
uses: hay-kot/homebox/.github/workflows/partial-publish.yaml@main
|
||||
with:
|
||||
tag: nightly
|
||||
secrets:
|
||||
GH_TOKEN: ${{ secrets.CR_PAT }}
|
||||
|
||||
deploy-docs:
|
||||
name: Deploy docs
|
||||
needs:
|
||||
|
||||
@@ -12,6 +12,7 @@ RUN pnpm build
|
||||
FROM golang:alpine AS builder
|
||||
ARG BUILD_TIME
|
||||
ARG COMMIT
|
||||
ARG VERSION
|
||||
RUN apk update && \
|
||||
apk upgrade && \
|
||||
apk add --update git build-base gcc g++
|
||||
@@ -22,7 +23,7 @@ RUN go get -d -v ./...
|
||||
RUN rm -rf ./app/api/public
|
||||
COPY --from=frontend-builder /app/.output/public ./app/api/public
|
||||
RUN CGO_ENABLED=1 GOOS=linux go build \
|
||||
-ldflags "-s -w -X main.Commit=$COMMIT -X main.BuildTime=$BUILD_TIME" \
|
||||
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
|
||||
-o /go/bin/api \
|
||||
-v ./app/api/*.go
|
||||
|
||||
|
||||
@@ -2,8 +2,14 @@ version: "3"
|
||||
|
||||
env:
|
||||
HBOX_STORAGE_SQLITE_URL: .data/homebox.db?_fk=1
|
||||
|
||||
UNSAFE_DISABLE_PASSWORD_PROJECTION: "yes_i_am_sure"
|
||||
tasks:
|
||||
setup:
|
||||
desc: Install dependencies
|
||||
cmds:
|
||||
- go install github.com/swaggo/swag/cmd/swag@latest
|
||||
- cd backend && go mod tidy
|
||||
- cd frontend && pnpm install --shamefully-hoist
|
||||
generate:
|
||||
desc: |
|
||||
Generates collateral files from the backend project
|
||||
@@ -27,6 +33,7 @@ tasks:
|
||||
- "./scripts/process-types.py"
|
||||
generates:
|
||||
- "./frontend/lib/api/types/data-contracts.ts"
|
||||
- "./backend/ent/schema"
|
||||
- "./backend/app/api/docs/swagger.json"
|
||||
- "./backend/app/api/docs/swagger.yaml"
|
||||
|
||||
|
||||
62
backend/app/api/demo.go
Normal file
62
backend/app/api/demo.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"strings"
|
||||
|
||||
"github.com/hay-kot/homebox/backend/internal/services"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (a *app) SetupDemo() {
|
||||
csvText := `Import Ref,Location,Labels,Quantity,Name,Description,Insured,Serial Number,Model Number,Manufacturer,Notes,Purchase From,Purchased Price,Purchased Time,Lifetime Warranty,Warranty Expires,Warranty Details,Sold To,Sold Price,Sold Time,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,,,,,,,
|
||||
,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,,,,,,,
|
||||
,Office,IOT;Home Assistant; Z-Wave,1,Zooz 110v Power Switch,"Zooz Z-Wave Plus Power Switch ZEN15 for 110V AC Units, Sump Pumps, Humidifiers, and More",,,ZEN15,Zooz,,Amazon,39.95,10/13/2021,,,,,,,
|
||||
,Downstairs,IOT;Home Assistant; Z-Wave,1,Ecolink Z-Wave PIR Motion Sensor,"Ecolink Z-Wave PIR Motion Detector Pet Immune, White (PIRZWAVE2.5-ECO)",,,PIRZWAVE2.5-ECO,Ecolink,,Amazon,35.58,10/21/2020,,,,,,,
|
||||
,Entry,IOT;Home Assistant; Z-Wave,1,Yale Security Touchscreen Deadbolt,"Yale Security YRD226-ZW2-619 YRD226ZW2619 Touchscreen Deadbolt, Satin Nickel",,,YRD226ZW2619,Yale,,Amazon,120.39,10/14/2020,,,,,,,
|
||||
,Kitchen,IOT;Home Assistant; Z-Wave,1,Smart Rocker Light Dimmer,"UltraPro Z-Wave Smart Rocker Light Dimmer with QuickFit and SimpleWire, 3-Way Ready, Compatible with Alexa, Google Assistant, ZWave Hub Required, Repeater/Range Extender, White Paddle Only, 39351",,,39351,Honeywell,,Amazon,65.98,09/30/0202,,,,,,,
|
||||
`
|
||||
|
||||
var (
|
||||
registration = services.UserRegistration{
|
||||
Email: "demo@example.com",
|
||||
Name: "Demo",
|
||||
Password: "demo",
|
||||
}
|
||||
)
|
||||
|
||||
// First check if we've already setup a demo user and skip if so
|
||||
_, err := a.services.User.Login(context.Background(), registration.Email, registration.Password)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = a.services.User.RegisterUser(context.Background(), registration)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to register demo user")
|
||||
log.Fatal().Msg("Failed to setup demo")
|
||||
}
|
||||
|
||||
token, _ := a.services.User.Login(context.Background(), registration.Email, registration.Password)
|
||||
self, _ := a.services.User.GetSelf(context.Background(), token.Raw)
|
||||
|
||||
// Read CSV Text
|
||||
reader := csv.NewReader(strings.NewReader(csvText))
|
||||
reader.Comma = ','
|
||||
|
||||
records, err := reader.ReadAll()
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to read CSV")
|
||||
log.Fatal().Msg("Failed to setup demo")
|
||||
}
|
||||
|
||||
_, err = a.services.Items.CsvImport(context.Background(), self.GroupID, records)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to import CSV")
|
||||
log.Fatal().Msg("Failed to setup demo")
|
||||
}
|
||||
|
||||
log.Info().Msg("Demo setup complete")
|
||||
}
|
||||
@@ -21,6 +21,63 @@ const docTemplate = `{
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {
|
||||
"/v1/groups": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Group"
|
||||
],
|
||||
"summary": "Get the current user's group",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.Group"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Group"
|
||||
],
|
||||
"summary": "Updates some fields of the current users group",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "User Data",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.GroupUpdate"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.Group"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/groups/invitations": {
|
||||
"post": {
|
||||
"security": [
|
||||
@@ -32,7 +89,7 @@ const docTemplate = `{
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
"Group"
|
||||
],
|
||||
"summary": "Get the current user",
|
||||
"parameters": [
|
||||
@@ -70,26 +127,51 @@ const docTemplate = `{
|
||||
"Items"
|
||||
],
|
||||
"summary": "Get All Items",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "search string",
|
||||
"name": "q",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "page number",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "items per page",
|
||||
"name": "pageSize",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "multi",
|
||||
"description": "label Ids",
|
||||
"name": "labels",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "multi",
|
||||
"description": "location Ids",
|
||||
"name": "locations",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Results"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.ItemSummary"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
"$ref": "#/definitions/repo.PaginationResult-repo_ItemSummary"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,7 +235,7 @@ const docTemplate = `{
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,7 +336,7 @@ const docTemplate = `{
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -270,7 +352,7 @@ const docTemplate = `{
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Items"
|
||||
"Items Attachments"
|
||||
],
|
||||
"summary": "imports items into the database",
|
||||
"parameters": [
|
||||
@@ -333,7 +415,7 @@ const docTemplate = `{
|
||||
"application/octet-stream"
|
||||
],
|
||||
"tags": [
|
||||
"Items"
|
||||
"Items Attachments"
|
||||
],
|
||||
"summary": "retrieves an attachment for an item",
|
||||
"parameters": [
|
||||
@@ -354,7 +436,7 @@ const docTemplate = `{
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -370,7 +452,7 @@ const docTemplate = `{
|
||||
"application/octet-stream"
|
||||
],
|
||||
"tags": [
|
||||
"Items"
|
||||
"Items Attachments"
|
||||
],
|
||||
"summary": "retrieves an attachment for an item",
|
||||
"parameters": [
|
||||
@@ -405,7 +487,7 @@ const docTemplate = `{
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Items"
|
||||
"Items Attachments"
|
||||
],
|
||||
"summary": "retrieves an attachment for an item",
|
||||
"parameters": [
|
||||
@@ -449,7 +531,7 @@ const docTemplate = `{
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Items"
|
||||
"Items Attachments"
|
||||
],
|
||||
"summary": "retrieves an attachment for an item",
|
||||
"parameters": [
|
||||
@@ -470,7 +552,7 @@ const docTemplate = `{
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -634,7 +716,7 @@ const docTemplate = `{
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -798,7 +880,7 @@ const docTemplate = `{
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -846,7 +928,7 @@ const docTemplate = `{
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -903,7 +985,7 @@ const docTemplate = `{
|
||||
"summary": "User Logout",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -922,7 +1004,7 @@ const docTemplate = `{
|
||||
"summary": "User Token Refresh",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -949,7 +1031,7 @@ const docTemplate = `{
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1049,28 +1131,7 @@ const docTemplate = `{
|
||||
"summary": "Deletes the user account",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/users/self/password": {
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
],
|
||||
"summary": "Update the current user's password // TODO:",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1091,6 +1152,37 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.Group": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"currency": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.GroupUpdate": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"currency": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.ItemAttachment": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1143,6 +1235,32 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.ItemField": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"booleanValue": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"numberValue": {
|
||||
"type": "integer"
|
||||
},
|
||||
"textValue": {
|
||||
"type": "string"
|
||||
},
|
||||
"timeValue": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.ItemOut": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1158,6 +1276,13 @@ const docTemplate = `{
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"fields": {
|
||||
"description": "Future",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.ItemField"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1275,6 +1400,12 @@ const docTemplate = `{
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"fields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.ItemField"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1488,6 +1619,26 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.PaginationResult-repo_ItemSummary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.ItemSummary"
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
"type": "integer"
|
||||
},
|
||||
"pageSize": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.UserOut": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1541,9 +1692,7 @@ const docTemplate = `{
|
||||
"server.Results": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "any"
|
||||
}
|
||||
"items": {}
|
||||
}
|
||||
},
|
||||
"server.ValidationError": {
|
||||
@@ -1580,6 +1729,9 @@ const docTemplate = `{
|
||||
"build": {
|
||||
"$ref": "#/definitions/v1.Build"
|
||||
},
|
||||
"demo": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"health": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -13,6 +13,63 @@
|
||||
},
|
||||
"basePath": "/api",
|
||||
"paths": {
|
||||
"/v1/groups": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Group"
|
||||
],
|
||||
"summary": "Get the current user's group",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.Group"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Group"
|
||||
],
|
||||
"summary": "Updates some fields of the current users group",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "User Data",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.GroupUpdate"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.Group"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/groups/invitations": {
|
||||
"post": {
|
||||
"security": [
|
||||
@@ -24,7 +81,7 @@
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
"Group"
|
||||
],
|
||||
"summary": "Get the current user",
|
||||
"parameters": [
|
||||
@@ -62,26 +119,51 @@
|
||||
"Items"
|
||||
],
|
||||
"summary": "Get All Items",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "search string",
|
||||
"name": "q",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "page number",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "items per page",
|
||||
"name": "pageSize",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "multi",
|
||||
"description": "label Ids",
|
||||
"name": "labels",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "multi",
|
||||
"description": "location Ids",
|
||||
"name": "locations",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Results"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.ItemSummary"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
"$ref": "#/definitions/repo.PaginationResult-repo_ItemSummary"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,7 +227,7 @@
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,7 +328,7 @@
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -262,7 +344,7 @@
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Items"
|
||||
"Items Attachments"
|
||||
],
|
||||
"summary": "imports items into the database",
|
||||
"parameters": [
|
||||
@@ -325,7 +407,7 @@
|
||||
"application/octet-stream"
|
||||
],
|
||||
"tags": [
|
||||
"Items"
|
||||
"Items Attachments"
|
||||
],
|
||||
"summary": "retrieves an attachment for an item",
|
||||
"parameters": [
|
||||
@@ -346,7 +428,7 @@
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -362,7 +444,7 @@
|
||||
"application/octet-stream"
|
||||
],
|
||||
"tags": [
|
||||
"Items"
|
||||
"Items Attachments"
|
||||
],
|
||||
"summary": "retrieves an attachment for an item",
|
||||
"parameters": [
|
||||
@@ -397,7 +479,7 @@
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Items"
|
||||
"Items Attachments"
|
||||
],
|
||||
"summary": "retrieves an attachment for an item",
|
||||
"parameters": [
|
||||
@@ -441,7 +523,7 @@
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Items"
|
||||
"Items Attachments"
|
||||
],
|
||||
"summary": "retrieves an attachment for an item",
|
||||
"parameters": [
|
||||
@@ -462,7 +544,7 @@
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -626,7 +708,7 @@
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -790,7 +872,7 @@
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -838,7 +920,7 @@
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -895,7 +977,7 @@
|
||||
"summary": "User Logout",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -914,7 +996,7 @@
|
||||
"summary": "User Token Refresh",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -941,7 +1023,7 @@
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1041,28 +1123,7 @@
|
||||
"summary": "Deletes the user account",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/users/self/password": {
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
],
|
||||
"summary": "Update the current user's password // TODO:",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1083,6 +1144,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.Group": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"currency": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.GroupUpdate": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"currency": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.ItemAttachment": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1135,6 +1227,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.ItemField": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"booleanValue": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"numberValue": {
|
||||
"type": "integer"
|
||||
},
|
||||
"textValue": {
|
||||
"type": "string"
|
||||
},
|
||||
"timeValue": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.ItemOut": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1150,6 +1268,13 @@
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"fields": {
|
||||
"description": "Future",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.ItemField"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1267,6 +1392,12 @@
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"fields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.ItemField"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1480,6 +1611,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.PaginationResult-repo_ItemSummary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.ItemSummary"
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
"type": "integer"
|
||||
},
|
||||
"pageSize": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.UserOut": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1533,9 +1684,7 @@
|
||||
"server.Results": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "any"
|
||||
}
|
||||
"items": {}
|
||||
}
|
||||
},
|
||||
"server.ValidationError": {
|
||||
@@ -1572,6 +1721,9 @@
|
||||
"build": {
|
||||
"$ref": "#/definitions/v1.Build"
|
||||
},
|
||||
"demo": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"health": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -9,6 +9,26 @@ definitions:
|
||||
title:
|
||||
type: string
|
||||
type: object
|
||||
repo.Group:
|
||||
properties:
|
||||
createdAt:
|
||||
type: string
|
||||
currency:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
updatedAt:
|
||||
type: string
|
||||
type: object
|
||||
repo.GroupUpdate:
|
||||
properties:
|
||||
currency:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
type: object
|
||||
repo.ItemAttachment:
|
||||
properties:
|
||||
createdAt:
|
||||
@@ -43,6 +63,23 @@ definitions:
|
||||
name:
|
||||
type: string
|
||||
type: object
|
||||
repo.ItemField:
|
||||
properties:
|
||||
booleanValue:
|
||||
type: boolean
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
numberValue:
|
||||
type: integer
|
||||
textValue:
|
||||
type: string
|
||||
timeValue:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
repo.ItemOut:
|
||||
properties:
|
||||
attachments:
|
||||
@@ -53,6 +90,11 @@ definitions:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
fields:
|
||||
description: Future
|
||||
items:
|
||||
$ref: '#/definitions/repo.ItemField'
|
||||
type: array
|
||||
id:
|
||||
type: string
|
||||
insured:
|
||||
@@ -133,6 +175,10 @@ definitions:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
fields:
|
||||
items:
|
||||
$ref: '#/definitions/repo.ItemField'
|
||||
type: array
|
||||
id:
|
||||
type: string
|
||||
insured:
|
||||
@@ -275,6 +321,19 @@ definitions:
|
||||
updatedAt:
|
||||
type: string
|
||||
type: object
|
||||
repo.PaginationResult-repo_ItemSummary:
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
$ref: '#/definitions/repo.ItemSummary'
|
||||
type: array
|
||||
page:
|
||||
type: integer
|
||||
pageSize:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
type: object
|
||||
repo.UserOut:
|
||||
properties:
|
||||
email:
|
||||
@@ -310,8 +369,7 @@ definitions:
|
||||
type: object
|
||||
server.Results:
|
||||
properties:
|
||||
items:
|
||||
type: any
|
||||
items: {}
|
||||
type: object
|
||||
server.ValidationError:
|
||||
properties:
|
||||
@@ -335,6 +393,8 @@ definitions:
|
||||
properties:
|
||||
build:
|
||||
$ref: '#/definitions/v1.Build'
|
||||
demo:
|
||||
type: boolean
|
||||
health:
|
||||
type: boolean
|
||||
message:
|
||||
@@ -401,6 +461,40 @@ info:
|
||||
title: Go API Templates
|
||||
version: "1.0"
|
||||
paths:
|
||||
/v1/groups:
|
||||
get:
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/repo.Group'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Get the current user's group
|
||||
tags:
|
||||
- Group
|
||||
put:
|
||||
parameters:
|
||||
- description: User Data
|
||||
in: body
|
||||
name: payload
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/repo.GroupUpdate'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/repo.Group'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Updates some fields of the current users group
|
||||
tags:
|
||||
- Group
|
||||
/v1/groups/invitations:
|
||||
post:
|
||||
parameters:
|
||||
@@ -421,23 +515,43 @@ paths:
|
||||
- Bearer: []
|
||||
summary: Get the current user
|
||||
tags:
|
||||
- User
|
||||
- Group
|
||||
/v1/items:
|
||||
get:
|
||||
parameters:
|
||||
- description: search string
|
||||
in: query
|
||||
name: q
|
||||
type: string
|
||||
- description: page number
|
||||
in: query
|
||||
name: page
|
||||
type: integer
|
||||
- description: items per page
|
||||
in: query
|
||||
name: pageSize
|
||||
type: integer
|
||||
- collectionFormat: multi
|
||||
description: label Ids
|
||||
in: query
|
||||
items:
|
||||
type: string
|
||||
name: labels
|
||||
type: array
|
||||
- collectionFormat: multi
|
||||
description: location Ids
|
||||
in: query
|
||||
items:
|
||||
type: string
|
||||
name: locations
|
||||
type: array
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/server.Results'
|
||||
- properties:
|
||||
items:
|
||||
items:
|
||||
$ref: '#/definitions/repo.ItemSummary'
|
||||
type: array
|
||||
type: object
|
||||
$ref: '#/definitions/repo.PaginationResult-repo_ItemSummary'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Get All Items
|
||||
@@ -475,7 +589,7 @@ paths:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: ""
|
||||
description: No Content
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: deletes a item
|
||||
@@ -565,7 +679,7 @@ paths:
|
||||
- Bearer: []
|
||||
summary: imports items into the database
|
||||
tags:
|
||||
- Items
|
||||
- Items Attachments
|
||||
/v1/items/{id}/attachments/{attachment_id}:
|
||||
delete:
|
||||
parameters:
|
||||
@@ -581,12 +695,12 @@ paths:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
description: ""
|
||||
description: No Content
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: retrieves an attachment for an item
|
||||
tags:
|
||||
- Items
|
||||
- Items Attachments
|
||||
get:
|
||||
parameters:
|
||||
- description: Item ID
|
||||
@@ -610,7 +724,7 @@ paths:
|
||||
- Bearer: []
|
||||
summary: retrieves an attachment for an item
|
||||
tags:
|
||||
- Items
|
||||
- Items Attachments
|
||||
put:
|
||||
parameters:
|
||||
- description: Item ID
|
||||
@@ -638,7 +752,7 @@ paths:
|
||||
- Bearer: []
|
||||
summary: retrieves an attachment for an item
|
||||
tags:
|
||||
- Items
|
||||
- Items Attachments
|
||||
/v1/items/{id}/attachments/download:
|
||||
get:
|
||||
parameters:
|
||||
@@ -656,12 +770,12 @@ paths:
|
||||
- application/octet-stream
|
||||
responses:
|
||||
"200":
|
||||
description: ""
|
||||
description: OK
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: retrieves an attachment for an item
|
||||
tags:
|
||||
- Items
|
||||
- Items Attachments
|
||||
/v1/items/import:
|
||||
post:
|
||||
parameters:
|
||||
@@ -674,7 +788,7 @@ paths:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: ""
|
||||
description: No Content
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: imports items into the database
|
||||
@@ -733,7 +847,7 @@ paths:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: ""
|
||||
description: No Content
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: deletes a label
|
||||
@@ -830,7 +944,7 @@ paths:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: ""
|
||||
description: No Content
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: deletes a location
|
||||
@@ -897,7 +1011,7 @@ paths:
|
||||
$ref: '#/definitions/v1.ChangePassword'
|
||||
responses:
|
||||
"204":
|
||||
description: ""
|
||||
description: No Content
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Updates the users password
|
||||
@@ -933,7 +1047,7 @@ paths:
|
||||
post:
|
||||
responses:
|
||||
"204":
|
||||
description: ""
|
||||
description: No Content
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: User Logout
|
||||
@@ -946,7 +1060,7 @@ paths:
|
||||
This does not validate that the user still exists within the database.
|
||||
responses:
|
||||
"200":
|
||||
description: ""
|
||||
description: OK
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: User Token Refresh
|
||||
@@ -965,7 +1079,7 @@ paths:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: ""
|
||||
description: No Content
|
||||
summary: Get the current user
|
||||
tags:
|
||||
- User
|
||||
@@ -975,7 +1089,7 @@ paths:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: ""
|
||||
description: No Content
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Deletes the user account
|
||||
@@ -1024,18 +1138,6 @@ paths:
|
||||
summary: Update the current user
|
||||
tags:
|
||||
- User
|
||||
/v1/users/self/password:
|
||||
put:
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: ""
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: 'Update the current user''s password // TODO:'
|
||||
tags:
|
||||
- User
|
||||
securityDefinitions:
|
||||
Bearer:
|
||||
description: '"Type ''Bearer TOKEN'' to correctly set the API Key"'
|
||||
|
||||
@@ -20,28 +20,23 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
Version = "0.1.0"
|
||||
Commit = "HEAD"
|
||||
BuildTime = "now"
|
||||
version = "nightly"
|
||||
commit = "HEAD"
|
||||
buildTime = "now"
|
||||
)
|
||||
|
||||
// @title Go API Templates
|
||||
// @version 1.0
|
||||
// @description This is a simple Rest API Server Template that implements some basic User and Authentication patterns to help you get started and bootstrap your next project!.
|
||||
// @contact.name Don't
|
||||
// @license.name MIT
|
||||
// @BasePath /api
|
||||
// @securityDefinitions.apikey Bearer
|
||||
// @in header
|
||||
// @name Authorization
|
||||
// @description "Type 'Bearer TOKEN' to correctly set the API Key"
|
||||
// @title Go API Templates
|
||||
// @version 1.0
|
||||
// @description This is a simple Rest API Server Template that implements some basic User and Authentication patterns to help you get started and bootstrap your next project!.
|
||||
// @contact.name Don't
|
||||
// @license.name MIT
|
||||
// @BasePath /api
|
||||
// @securityDefinitions.apikey Bearer
|
||||
// @in header
|
||||
// @name Authorization
|
||||
// @description "Type 'Bearer TOKEN' to correctly set the API Key"
|
||||
func main() {
|
||||
path := ""
|
||||
if len(os.Args) > 1 {
|
||||
path = os.Args[1]
|
||||
}
|
||||
|
||||
cfg, err := config.NewConfig(path)
|
||||
cfg, err := config.New()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -115,7 +110,7 @@ func run(cfg *config.Config) error {
|
||||
|
||||
app.db = c
|
||||
app.repos = repo.New(c, cfg.Storage.Data)
|
||||
app.services = services.NewServices(app.repos)
|
||||
app.services = services.New(app.repos)
|
||||
|
||||
// =========================================================================
|
||||
// Start Server
|
||||
@@ -143,6 +138,20 @@ func run(cfg *config.Config) error {
|
||||
Msg("failed to purge expired tokens")
|
||||
}
|
||||
})
|
||||
go app.startBgTask(time.Duration(24)*time.Hour, func() {
|
||||
_, err := app.repos.Groups.InvitationPurge(context.Background())
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Msg("failed to purge expired invitations")
|
||||
}
|
||||
})
|
||||
|
||||
// TODO: Remove through external API that does setup
|
||||
if cfg.Demo {
|
||||
log.Info().Msg("Running in demo mode, creating demo data")
|
||||
app.SetupDemo()
|
||||
}
|
||||
|
||||
return app.server.Start(routes)
|
||||
}
|
||||
|
||||
@@ -63,22 +63,6 @@ func (a *app) mwAuthToken(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
// mwAdminOnly is a middleware that extends the mwAuthToken middleware to only allow
|
||||
// requests from superusers.
|
||||
// func (a *app) mwAdminOnly(next http.Handler) http.Handler {
|
||||
// mw := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// usr := services.UseUserCtx(r.Context())
|
||||
|
||||
// if !usr.IsSuperuser {
|
||||
// server.RespondUnauthorized(w)
|
||||
// return
|
||||
// }
|
||||
|
||||
// next.ServeHTTP(w, r)
|
||||
// })
|
||||
// return a.mwAuthToken(mw)
|
||||
// }
|
||||
|
||||
// mqStripTrailingSlash is a middleware that will strip trailing slashes from the request path.
|
||||
func mwStripTrailingSlash(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -111,7 +95,6 @@ func (a *app) mwStructLogger(next http.Handler) http.Handler {
|
||||
|
||||
log.Info().
|
||||
Str("id", middleware.GetReqID(r.Context())).
|
||||
Str("url", url).
|
||||
Str("method", r.Method).
|
||||
Str("remote_addr", r.RemoteAddr).
|
||||
Int("status", record.Status).
|
||||
|
||||
@@ -42,58 +42,63 @@ func (a *app) newRouter(repos *repo.AllRepos) *chi.Mux {
|
||||
// API Version 1
|
||||
|
||||
v1Base := v1.BaseUrlFunc(prefix)
|
||||
v1Ctrl := v1.NewControllerV1(a.services, v1.WithMaxUploadSize(a.conf.Web.MaxUploadSize))
|
||||
{
|
||||
r.Get(v1Base("/status"), v1Ctrl.HandleBase(func() bool { return true }, v1.Build{
|
||||
Version: Version,
|
||||
Commit: Commit,
|
||||
BuildTime: BuildTime,
|
||||
}))
|
||||
v1Ctrl := v1.NewControllerV1(a.services,
|
||||
v1.WithMaxUploadSize(a.conf.Web.MaxUploadSize),
|
||||
v1.WithRegistration(a.conf.AllowRegistration),
|
||||
v1.WithDemoStatus(a.conf.Demo), // Disable Password Change in Demo Mode
|
||||
)
|
||||
r.Get(v1Base("/status"), v1Ctrl.HandleBase(func() bool { return true }, v1.Build{
|
||||
Version: version,
|
||||
Commit: commit,
|
||||
BuildTime: buildTime,
|
||||
}))
|
||||
|
||||
r.Post(v1Base("/users/register"), v1Ctrl.HandleUserRegistration())
|
||||
r.Post(v1Base("/users/login"), v1Ctrl.HandleAuthLogin())
|
||||
r.Post(v1Base("/users/register"), v1Ctrl.HandleUserRegistration())
|
||||
r.Post(v1Base("/users/login"), v1Ctrl.HandleAuthLogin())
|
||||
|
||||
// Attachment download URl needs a `token` query param to be passed in the request.
|
||||
// and also needs to be outside of the `auth` middleware.
|
||||
r.Get(v1Base("/items/{id}/attachments/download"), v1Ctrl.HandleItemAttachmentDownload())
|
||||
// Attachment download URl needs a `token` query param to be passed in the request.
|
||||
// and also needs to be outside of the `auth` middleware.
|
||||
r.Get(v1Base("/items/{id}/attachments/download"), v1Ctrl.HandleItemAttachmentDownload())
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(a.mwAuthToken)
|
||||
r.Get(v1Base("/users/self"), v1Ctrl.HandleUserSelf())
|
||||
r.Put(v1Base("/users/self"), v1Ctrl.HandleUserSelfUpdate())
|
||||
r.Delete(v1Base("/users/self"), v1Ctrl.HandleUserSelfDelete())
|
||||
r.Put(v1Base("/users/self/password"), v1Ctrl.HandleUserUpdatePassword())
|
||||
r.Post(v1Base("/users/logout"), v1Ctrl.HandleAuthLogout())
|
||||
r.Get(v1Base("/users/refresh"), v1Ctrl.HandleAuthRefresh())
|
||||
r.Put(v1Base("/users/self/change-password"), v1Ctrl.HandleUserSelfChangePassword())
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(a.mwAuthToken)
|
||||
r.Get(v1Base("/users/self"), v1Ctrl.HandleUserSelf())
|
||||
r.Put(v1Base("/users/self"), v1Ctrl.HandleUserSelfUpdate())
|
||||
r.Delete(v1Base("/users/self"), v1Ctrl.HandleUserSelfDelete())
|
||||
r.Post(v1Base("/users/logout"), v1Ctrl.HandleAuthLogout())
|
||||
r.Get(v1Base("/users/refresh"), v1Ctrl.HandleAuthRefresh())
|
||||
r.Put(v1Base("/users/self/change-password"), v1Ctrl.HandleUserSelfChangePassword())
|
||||
|
||||
r.Post(v1Base("/groups/invitations"), v1Ctrl.HandleGroupInvitationsCreate())
|
||||
r.Post(v1Base("/groups/invitations"), v1Ctrl.HandleGroupInvitationsCreate())
|
||||
|
||||
r.Get(v1Base("/locations"), v1Ctrl.HandleLocationGetAll())
|
||||
r.Post(v1Base("/locations"), v1Ctrl.HandleLocationCreate())
|
||||
r.Get(v1Base("/locations/{id}"), v1Ctrl.HandleLocationGet())
|
||||
r.Put(v1Base("/locations/{id}"), v1Ctrl.HandleLocationUpdate())
|
||||
r.Delete(v1Base("/locations/{id}"), v1Ctrl.HandleLocationDelete())
|
||||
// TODO: I don't like /groups being the URL for users
|
||||
r.Get(v1Base("/groups"), v1Ctrl.HandleGroupGet())
|
||||
r.Put(v1Base("/groups"), v1Ctrl.HandleGroupUpdate())
|
||||
|
||||
r.Get(v1Base("/labels"), v1Ctrl.HandleLabelsGetAll())
|
||||
r.Post(v1Base("/labels"), v1Ctrl.HandleLabelsCreate())
|
||||
r.Get(v1Base("/labels/{id}"), v1Ctrl.HandleLabelGet())
|
||||
r.Put(v1Base("/labels/{id}"), v1Ctrl.HandleLabelUpdate())
|
||||
r.Delete(v1Base("/labels/{id}"), v1Ctrl.HandleLabelDelete())
|
||||
r.Get(v1Base("/locations"), v1Ctrl.HandleLocationGetAll())
|
||||
r.Post(v1Base("/locations"), v1Ctrl.HandleLocationCreate())
|
||||
r.Get(v1Base("/locations/{id}"), v1Ctrl.HandleLocationGet())
|
||||
r.Put(v1Base("/locations/{id}"), v1Ctrl.HandleLocationUpdate())
|
||||
r.Delete(v1Base("/locations/{id}"), v1Ctrl.HandleLocationDelete())
|
||||
|
||||
r.Get(v1Base("/items"), v1Ctrl.HandleItemsGetAll())
|
||||
r.Post(v1Base("/items/import"), v1Ctrl.HandleItemsImport())
|
||||
r.Post(v1Base("/items"), v1Ctrl.HandleItemsCreate())
|
||||
r.Get(v1Base("/items/{id}"), v1Ctrl.HandleItemGet())
|
||||
r.Put(v1Base("/items/{id}"), v1Ctrl.HandleItemUpdate())
|
||||
r.Delete(v1Base("/items/{id}"), v1Ctrl.HandleItemDelete())
|
||||
r.Get(v1Base("/labels"), v1Ctrl.HandleLabelsGetAll())
|
||||
r.Post(v1Base("/labels"), v1Ctrl.HandleLabelsCreate())
|
||||
r.Get(v1Base("/labels/{id}"), v1Ctrl.HandleLabelGet())
|
||||
r.Put(v1Base("/labels/{id}"), v1Ctrl.HandleLabelUpdate())
|
||||
r.Delete(v1Base("/labels/{id}"), v1Ctrl.HandleLabelDelete())
|
||||
|
||||
r.Post(v1Base("/items/{id}/attachments"), v1Ctrl.HandleItemAttachmentCreate())
|
||||
r.Get(v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentToken())
|
||||
r.Put(v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentUpdate())
|
||||
r.Delete(v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentDelete())
|
||||
})
|
||||
}
|
||||
r.Get(v1Base("/items"), v1Ctrl.HandleItemsGetAll())
|
||||
r.Post(v1Base("/items/import"), v1Ctrl.HandleItemsImport())
|
||||
r.Post(v1Base("/items"), v1Ctrl.HandleItemsCreate())
|
||||
r.Get(v1Base("/items/{id}"), v1Ctrl.HandleItemGet())
|
||||
r.Put(v1Base("/items/{id}"), v1Ctrl.HandleItemUpdate())
|
||||
r.Delete(v1Base("/items/{id}"), v1Ctrl.HandleItemDelete())
|
||||
|
||||
r.Post(v1Base("/items/{id}/attachments"), v1Ctrl.HandleItemAttachmentCreate())
|
||||
r.Get(v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentToken())
|
||||
r.Put(v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentUpdate())
|
||||
r.Delete(v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentDelete())
|
||||
})
|
||||
|
||||
r.NotFound(notFoundHandler())
|
||||
return r
|
||||
|
||||
@@ -13,12 +13,28 @@ func WithMaxUploadSize(maxUploadSize int64) func(*V1Controller) {
|
||||
}
|
||||
}
|
||||
|
||||
func WithDemoStatus(demoStatus bool) func(*V1Controller) {
|
||||
return func(ctrl *V1Controller) {
|
||||
ctrl.isDemo = demoStatus
|
||||
}
|
||||
}
|
||||
|
||||
func WithRegistration(allowRegistration bool) func(*V1Controller) {
|
||||
return func(ctrl *V1Controller) {
|
||||
ctrl.allowRegistration = allowRegistration
|
||||
}
|
||||
}
|
||||
|
||||
type V1Controller struct {
|
||||
svc *services.AllServices
|
||||
maxUploadSize int64
|
||||
svc *services.AllServices
|
||||
maxUploadSize int64
|
||||
isDemo bool
|
||||
allowRegistration bool
|
||||
}
|
||||
|
||||
type (
|
||||
ReadyFunc func() bool
|
||||
|
||||
Build struct {
|
||||
Version string `json:"version"`
|
||||
Commit string `json:"commit"`
|
||||
@@ -30,35 +46,36 @@ type (
|
||||
Versions []string `json:"versions"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Build Build
|
||||
Build Build `json:"build"`
|
||||
Demo bool `json:"demo"`
|
||||
}
|
||||
)
|
||||
|
||||
func BaseUrlFunc(prefix string) func(s string) string {
|
||||
v1Base := prefix + "/v1"
|
||||
prefixFunc := func(s string) string {
|
||||
return v1Base + s
|
||||
return func(s string) string {
|
||||
return prefix + "/v1" + s
|
||||
}
|
||||
|
||||
return prefixFunc
|
||||
}
|
||||
|
||||
func NewControllerV1(svc *services.AllServices, options ...func(*V1Controller)) *V1Controller {
|
||||
ctrl := &V1Controller{
|
||||
svc: svc,
|
||||
svc: svc,
|
||||
allowRegistration: true,
|
||||
}
|
||||
|
||||
for _, opt := range options {
|
||||
opt(ctrl)
|
||||
}
|
||||
|
||||
return ctrl
|
||||
}
|
||||
|
||||
type ReadyFunc func() bool
|
||||
|
||||
// HandleBase godoc
|
||||
// @Summary Retrieves the basic information about the API
|
||||
// @Tags Base
|
||||
// @Produce json
|
||||
// @Success 200 {object} ApiSummary
|
||||
// @Router /v1/status [GET]
|
||||
// @Summary Retrieves the basic information about the API
|
||||
// @Tags Base
|
||||
// @Produce json
|
||||
// @Success 200 {object} ApiSummary
|
||||
// @Router /v1/status [GET]
|
||||
func (ctrl *V1Controller) HandleBase(ready ReadyFunc, build Build) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
server.Respond(w, http.StatusOK, ApiSummary{
|
||||
@@ -66,6 +83,7 @@ func (ctrl *V1Controller) HandleBase(ready ReadyFunc, build Build) http.HandlerF
|
||||
Title: "Go API Template",
|
||||
Message: "Welcome to the Go API Template Application!",
|
||||
Build: build,
|
||||
Demo: ctrl.isDemo,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,30 +5,17 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/homebox/backend/internal/repo"
|
||||
"github.com/hay-kot/homebox/backend/internal/services"
|
||||
"github.com/hay-kot/homebox/backend/pkgs/server"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
/*
|
||||
This is where we put partial snippets/functions for actions that are commonly
|
||||
used within the controller class. This _hopefully_ helps with code duplication
|
||||
and makes it a little more consistent when error handling and logging.
|
||||
*/
|
||||
|
||||
// partialParseIdAndUser parses the ID from the requests URL and pulls the user
|
||||
// from the context. If either of these fail, it will return an error. When an error
|
||||
// occurs it will also write the error to the response. As such, if an error is returned
|
||||
// from this function you can return immediately without writing to the response.
|
||||
func (ctrl *V1Controller) partialParseIdAndUser(w http.ResponseWriter, r *http.Request) (uuid.UUID, *repo.UserOut, error) {
|
||||
uid, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
func (ctrl *V1Controller) routeID(w http.ResponseWriter, r *http.Request) (uuid.UUID, error) {
|
||||
ID, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to parse id")
|
||||
server.RespondError(w, http.StatusBadRequest, err)
|
||||
return uuid.Nil, &repo.UserOut{}, err
|
||||
return uuid.Nil, err
|
||||
}
|
||||
|
||||
user := services.UseUserCtx(r.Context())
|
||||
return uid, user, nil
|
||||
return ID, nil
|
||||
}
|
||||
|
||||
@@ -23,15 +23,15 @@ type (
|
||||
)
|
||||
|
||||
// HandleAuthLogin godoc
|
||||
// @Summary User Login
|
||||
// @Tags Authentication
|
||||
// @Accept x-www-form-urlencoded
|
||||
// @Accept application/json
|
||||
// @Param username formData string false "string" example(admin@admin.com)
|
||||
// @Param password formData string false "string" example(admin)
|
||||
// @Produce json
|
||||
// @Success 200 {object} TokenResponse
|
||||
// @Router /v1/users/login [POST]
|
||||
// @Summary User Login
|
||||
// @Tags Authentication
|
||||
// @Accept x-www-form-urlencoded
|
||||
// @Accept application/json
|
||||
// @Param username formData string false "string" example(admin@admin.com)
|
||||
// @Param password formData string false "string" example(admin)
|
||||
// @Produce json
|
||||
// @Success 200 {object} TokenResponse
|
||||
// @Router /v1/users/login [POST]
|
||||
func (ctrl *V1Controller) HandleAuthLogin() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
loginForm := &LoginForm{}
|
||||
@@ -80,11 +80,11 @@ func (ctrl *V1Controller) HandleAuthLogin() http.HandlerFunc {
|
||||
}
|
||||
|
||||
// HandleAuthLogout godoc
|
||||
// @Summary User Logout
|
||||
// @Tags Authentication
|
||||
// @Success 204
|
||||
// @Router /v1/users/logout [POST]
|
||||
// @Security Bearer
|
||||
// @Summary User Logout
|
||||
// @Tags Authentication
|
||||
// @Success 204
|
||||
// @Router /v1/users/logout [POST]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleAuthLogout() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
token := services.UseTokenCtx(r.Context())
|
||||
@@ -106,13 +106,13 @@ func (ctrl *V1Controller) HandleAuthLogout() http.HandlerFunc {
|
||||
}
|
||||
|
||||
// HandleAuthLogout godoc
|
||||
// @Summary User Token Refresh
|
||||
// @Description handleAuthRefresh returns a handler that will issue a new token from an existing token.
|
||||
// @Description This does not validate that the user still exists within the database.
|
||||
// @Tags Authentication
|
||||
// @Success 200
|
||||
// @Router /v1/users/refresh [GET]
|
||||
// @Security Bearer
|
||||
// @Summary User Token Refresh
|
||||
// @Description handleAuthRefresh returns a handler that will issue a new token from an existing token.
|
||||
// @Description This does not validate that the user still exists within the database.
|
||||
// @Tags Authentication
|
||||
// @Success 200
|
||||
// @Router /v1/users/refresh [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleAuthRefresh() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
requestToken := services.UseTokenCtx(r.Context())
|
||||
|
||||
@@ -2,8 +2,10 @@ package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hay-kot/homebox/backend/internal/repo"
|
||||
"github.com/hay-kot/homebox/backend/internal/services"
|
||||
"github.com/hay-kot/homebox/backend/pkgs/server"
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -22,21 +24,78 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
// HandleUserSelf godoc
|
||||
// @Summary Get the current user
|
||||
// @Tags User
|
||||
// @Produce json
|
||||
// @Param payload body GroupInvitationCreate true "User Data"
|
||||
// @Success 200 {object} GroupInvitation
|
||||
// @Router /v1/groups/invitations [Post]
|
||||
// @Security Bearer
|
||||
// HandleGroupGet godoc
|
||||
// @Summary Get the current user's group
|
||||
// @Tags Group
|
||||
// @Produce json
|
||||
// @Success 200 {object} repo.Group
|
||||
// @Router /v1/groups [Get]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleGroupGet() http.HandlerFunc {
|
||||
return ctrl.handleGroupGeneral()
|
||||
}
|
||||
|
||||
// HandleGroupUpdate godoc
|
||||
// @Summary Updates some fields of the current users group
|
||||
// @Tags Group
|
||||
// @Produce json
|
||||
// @Param payload body repo.GroupUpdate true "User Data"
|
||||
// @Success 200 {object} repo.Group
|
||||
// @Router /v1/groups [Put]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleGroupUpdate() http.HandlerFunc {
|
||||
return ctrl.handleGroupGeneral()
|
||||
}
|
||||
|
||||
func (ctrl *V1Controller) handleGroupGeneral() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := services.NewContext(r.Context())
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
group, err := ctrl.svc.Group.Get(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to get group")
|
||||
server.RespondError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
group.Currency = strings.ToUpper(group.Currency) // TODO: Hack to fix the currency enums being lower caseÍ
|
||||
server.Respond(w, http.StatusOK, group)
|
||||
|
||||
case http.MethodPut:
|
||||
data := repo.GroupUpdate{}
|
||||
if err := server.Decode(r, &data); err != nil {
|
||||
server.RespondError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
group, err := ctrl.svc.Group.UpdateGroup(ctx, data)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to update group")
|
||||
server.RespondError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
group.Currency = strings.ToUpper(group.Currency) // TODO: Hack to fix the currency enums being lower case
|
||||
server.Respond(w, http.StatusOK, group)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HandleGroupInvitationsCreate godoc
|
||||
// @Summary Get the current user
|
||||
// @Tags Group
|
||||
// @Produce json
|
||||
// @Param payload body GroupInvitationCreate true "User Data"
|
||||
// @Success 200 {object} GroupInvitation
|
||||
// @Router /v1/groups/invitations [Post]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleGroupInvitationsCreate() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
data := GroupInvitationCreate{}
|
||||
|
||||
if err := server.Decode(r, &data); err != nil {
|
||||
log.Err(err).Msg("failed to decode user registration data")
|
||||
server.RespondError(w, http.StatusInternalServerError, err)
|
||||
server.RespondError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -46,7 +105,7 @@ func (ctrl *V1Controller) HandleGroupInvitationsCreate() http.HandlerFunc {
|
||||
|
||||
ctx := services.NewContext(r.Context())
|
||||
|
||||
token, err := ctrl.svc.User.NewInvitation(ctx, data.Uses, data.ExpiresAt)
|
||||
token, err := ctrl.svc.Group.NewInvitation(ctx, data.Uses, data.ExpiresAt)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to create new token")
|
||||
server.RespondError(w, http.StatusInternalServerError, err)
|
||||
|
||||
@@ -3,7 +3,10 @@ package v1
|
||||
import (
|
||||
"encoding/csv"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/homebox/backend/internal/repo"
|
||||
"github.com/hay-kot/homebox/backend/internal/services"
|
||||
"github.com/hay-kot/homebox/backend/pkgs/server"
|
||||
@@ -11,33 +14,70 @@ import (
|
||||
)
|
||||
|
||||
// HandleItemsGetAll godoc
|
||||
// @Summary Get All Items
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Success 200 {object} server.Results{items=[]repo.ItemSummary}
|
||||
// @Router /v1/items [GET]
|
||||
// @Security Bearer
|
||||
// @Summary Get All Items
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param q query string false "search string"
|
||||
// @Param page query int false "page number"
|
||||
// @Param pageSize query int false "items per page"
|
||||
// @Param labels query []string false "label Ids" collectionFormat(multi)
|
||||
// @Param locations query []string false "location Ids" collectionFormat(multi)
|
||||
// @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{}
|
||||
// @Router /v1/items [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemsGetAll() http.HandlerFunc {
|
||||
uuidList := func(params url.Values, key string) []uuid.UUID {
|
||||
var ids []uuid.UUID
|
||||
for _, id := range params[key] {
|
||||
uid, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, uid)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
intOrNegativeOne := func(s string) int {
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
extractQuery := func(r *http.Request) repo.ItemQuery {
|
||||
params := r.URL.Query()
|
||||
|
||||
return repo.ItemQuery{
|
||||
Page: intOrNegativeOne(params.Get("page")),
|
||||
PageSize: intOrNegativeOne(params.Get("perPage")),
|
||||
Search: params.Get("q"),
|
||||
LocationIDs: uuidList(params, "locations"),
|
||||
LabelIDs: uuidList(params, "labels"),
|
||||
}
|
||||
}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
user := services.UseUserCtx(r.Context())
|
||||
items, err := ctrl.svc.Items.GetAll(r.Context(), user.GroupID)
|
||||
ctx := services.NewContext(r.Context())
|
||||
items, err := ctrl.svc.Items.Query(ctx, extractQuery(r))
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to get items")
|
||||
server.RespondServerError(w)
|
||||
return
|
||||
}
|
||||
server.Respond(w, http.StatusOK, server.Results{Items: items})
|
||||
server.Respond(w, http.StatusOK, items)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleItemsCreate godoc
|
||||
// @Summary Create a new item
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param payload body repo.ItemCreate true "Item Data"
|
||||
// @Success 200 {object} repo.ItemSummary
|
||||
// @Router /v1/items [POST]
|
||||
// @Security Bearer
|
||||
// @Summary Create a new item
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param payload body repo.ItemCreate true "Item Data"
|
||||
// @Success 200 {object} repo.ItemSummary
|
||||
// @Router /v1/items [POST]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemsCreate() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
createData := repo.ItemCreate{}
|
||||
@@ -59,97 +99,98 @@ func (ctrl *V1Controller) HandleItemsCreate() http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// HandleItemDelete godocs
|
||||
// @Summary deletes a item
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param id path string true "Item ID"
|
||||
// @Success 204
|
||||
// @Router /v1/items/{id} [DELETE]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemDelete() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
uid, user, err := ctrl.partialParseIdAndUser(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = ctrl.svc.Items.Delete(r.Context(), user.GroupID, uid)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to delete item")
|
||||
server.RespondServerError(w)
|
||||
return
|
||||
}
|
||||
server.Respond(w, http.StatusNoContent, nil)
|
||||
}
|
||||
// HandleItemGet godocs
|
||||
// @Summary Gets a item and fields
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param id path string true "Item ID"
|
||||
// @Success 200 {object} repo.ItemOut
|
||||
// @Router /v1/items/{id} [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemGet() http.HandlerFunc {
|
||||
return ctrl.handleItemsGeneral()
|
||||
}
|
||||
|
||||
// HandleItemGet godocs
|
||||
// @Summary Gets a item and fields
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param id path string true "Item ID"
|
||||
// @Success 200 {object} repo.ItemOut
|
||||
// @Router /v1/items/{id} [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemGet() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
uid, user, err := ctrl.partialParseIdAndUser(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
items, err := ctrl.svc.Items.GetOne(r.Context(), user.GroupID, uid)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to get item")
|
||||
server.RespondServerError(w)
|
||||
return
|
||||
}
|
||||
server.Respond(w, http.StatusOK, items)
|
||||
}
|
||||
// HandleItemDelete godocs
|
||||
// @Summary deletes a item
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param id path string true "Item ID"
|
||||
// @Success 204
|
||||
// @Router /v1/items/{id} [DELETE]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemDelete() http.HandlerFunc {
|
||||
return ctrl.handleItemsGeneral()
|
||||
}
|
||||
|
||||
// HandleItemUpdate godocs
|
||||
// @Summary updates a item
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param payload body repo.ItemUpdate true "Item Data"
|
||||
// @Success 200 {object} repo.ItemOut
|
||||
// @Router /v1/items/{id} [PUT]
|
||||
// @Security Bearer
|
||||
// @Summary updates a item
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param payload body repo.ItemUpdate true "Item Data"
|
||||
// @Success 200 {object} repo.ItemOut
|
||||
// @Router /v1/items/{id} [PUT]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemUpdate() http.HandlerFunc {
|
||||
return ctrl.handleItemsGeneral()
|
||||
}
|
||||
|
||||
func (ctrl *V1Controller) handleItemsGeneral() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
body := repo.ItemUpdate{}
|
||||
if err := server.Decode(r, &body); err != nil {
|
||||
log.Err(err).Msg("failed to decode request body")
|
||||
server.RespondError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
uid, user, err := ctrl.partialParseIdAndUser(w, r)
|
||||
ctx := services.NewContext(r.Context())
|
||||
ID, err := ctrl.routeID(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
body.ID = uid
|
||||
result, err := ctrl.svc.Items.Update(r.Context(), user.GroupID, body)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to update item")
|
||||
server.RespondServerError(w)
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
items, err := ctrl.svc.Items.GetOne(r.Context(), ctx.GID, ID)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to get item")
|
||||
server.RespondServerError(w)
|
||||
return
|
||||
}
|
||||
server.Respond(w, http.StatusOK, items)
|
||||
return
|
||||
case http.MethodDelete:
|
||||
err = ctrl.svc.Items.Delete(r.Context(), ctx.GID, ID)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to delete item")
|
||||
server.RespondServerError(w)
|
||||
return
|
||||
}
|
||||
server.Respond(w, http.StatusNoContent, nil)
|
||||
return
|
||||
case http.MethodPut:
|
||||
body := repo.ItemUpdate{}
|
||||
if err := server.Decode(r, &body); err != nil {
|
||||
log.Err(err).Msg("failed to decode request body")
|
||||
server.RespondError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
body.ID = ID
|
||||
result, err := ctrl.svc.Items.Update(r.Context(), ctx.GID, body)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to update item")
|
||||
server.RespondServerError(w)
|
||||
return
|
||||
}
|
||||
server.Respond(w, http.StatusOK, result)
|
||||
}
|
||||
server.Respond(w, http.StatusOK, result)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// HandleItemsImport godocs
|
||||
// @Summary imports items into the database
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Success 204
|
||||
// @Param csv formData file true "Image to upload"
|
||||
// @Router /v1/items/import [Post]
|
||||
// @Security Bearer
|
||||
// @Summary imports items into the database
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Success 204
|
||||
// @Param csv formData file true "Image to upload"
|
||||
// @Router /v1/items/import [Post]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemsImport() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -177,7 +218,7 @@ func (ctrl *V1Controller) HandleItemsImport() http.HandlerFunc {
|
||||
|
||||
user := services.UseUserCtx(r.Context())
|
||||
|
||||
err = ctrl.svc.Items.CsvImport(r.Context(), user.GroupID, data)
|
||||
_, err = ctrl.svc.Items.CsvImport(r.Context(), user.GroupID, data)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to import items")
|
||||
server.RespondServerError(w)
|
||||
|
||||
@@ -21,17 +21,17 @@ type (
|
||||
)
|
||||
|
||||
// HandleItemsImport godocs
|
||||
// @Summary imports items into the database
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param file formData file true "File attachment"
|
||||
// @Param type formData string true "Type of file"
|
||||
// @Param name formData string true "name of the file including extension"
|
||||
// @Success 200 {object} repo.ItemOut
|
||||
// @Failure 422 {object} []server.ValidationError
|
||||
// @Router /v1/items/{id}/attachments [POST]
|
||||
// @Security Bearer
|
||||
// @Summary imports items into the database
|
||||
// @Tags Items Attachments
|
||||
// @Produce json
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param file formData file true "File attachment"
|
||||
// @Param type formData string true "Type of file"
|
||||
// @Param name formData string true "name of the file including extension"
|
||||
// @Success 200 {object} repo.ItemOut
|
||||
// @Failure 422 {object} []server.ValidationError
|
||||
// @Router /v1/items/{id}/attachments [POST]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemAttachmentCreate() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseMultipartForm(ctrl.maxUploadSize << 20)
|
||||
@@ -72,7 +72,7 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() http.HandlerFunc {
|
||||
attachmentType = attachment.TypeAttachment.String()
|
||||
}
|
||||
|
||||
id, _, err := ctrl.partialParseIdAndUser(w, r)
|
||||
id, err := ctrl.routeID(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -98,14 +98,14 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() http.HandlerFunc {
|
||||
}
|
||||
|
||||
// HandleItemAttachmentGet godocs
|
||||
// @Summary retrieves an attachment for an item
|
||||
// @Tags Items
|
||||
// @Produce application/octet-stream
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param token query string true "Attachment token"
|
||||
// @Success 200
|
||||
// @Router /v1/items/{id}/attachments/download [GET]
|
||||
// @Security Bearer
|
||||
// @Summary retrieves an attachment for an item
|
||||
// @Tags Items Attachments
|
||||
// @Produce application/octet-stream
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param token query string true "Attachment token"
|
||||
// @Success 200
|
||||
// @Router /v1/items/{id}/attachments/download [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemAttachmentDownload() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
token := server.GetParam(r, "token", "")
|
||||
@@ -125,45 +125,45 @@ func (ctrl *V1Controller) HandleItemAttachmentDownload() http.HandlerFunc {
|
||||
}
|
||||
|
||||
// HandleItemAttachmentToken godocs
|
||||
// @Summary retrieves an attachment for an item
|
||||
// @Tags Items
|
||||
// @Produce application/octet-stream
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param attachment_id path string true "Attachment ID"
|
||||
// @Success 200 {object} ItemAttachmentToken
|
||||
// @Router /v1/items/{id}/attachments/{attachment_id} [GET]
|
||||
// @Security Bearer
|
||||
// @Summary retrieves an attachment for an item
|
||||
// @Tags Items Attachments
|
||||
// @Produce application/octet-stream
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param attachment_id path string true "Attachment ID"
|
||||
// @Success 200 {object} ItemAttachmentToken
|
||||
// @Router /v1/items/{id}/attachments/{attachment_id} [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemAttachmentToken() http.HandlerFunc {
|
||||
return ctrl.handleItemAttachmentsHandler
|
||||
}
|
||||
|
||||
// HandleItemAttachmentDelete godocs
|
||||
// @Summary retrieves an attachment for an item
|
||||
// @Tags Items
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param attachment_id path string true "Attachment ID"
|
||||
// @Success 204
|
||||
// @Router /v1/items/{id}/attachments/{attachment_id} [DELETE]
|
||||
// @Security Bearer
|
||||
// @Summary retrieves an attachment for an item
|
||||
// @Tags Items Attachments
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param attachment_id path string true "Attachment ID"
|
||||
// @Success 204
|
||||
// @Router /v1/items/{id}/attachments/{attachment_id} [DELETE]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemAttachmentDelete() http.HandlerFunc {
|
||||
return ctrl.handleItemAttachmentsHandler
|
||||
}
|
||||
|
||||
// HandleItemAttachmentUpdate godocs
|
||||
// @Summary retrieves an attachment for an item
|
||||
// @Tags Items
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param attachment_id path string true "Attachment ID"
|
||||
// @Param payload body repo.ItemAttachmentUpdate true "Attachment Update"
|
||||
// @Success 200 {object} repo.ItemOut
|
||||
// @Router /v1/items/{id}/attachments/{attachment_id} [PUT]
|
||||
// @Security Bearer
|
||||
// @Summary retrieves an attachment for an item
|
||||
// @Tags Items Attachments
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param attachment_id path string true "Attachment ID"
|
||||
// @Param payload body repo.ItemAttachmentUpdate true "Attachment Update"
|
||||
// @Success 200 {object} repo.ItemOut
|
||||
// @Router /v1/items/{id}/attachments/{attachment_id} [PUT]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemAttachmentUpdate() http.HandlerFunc {
|
||||
return ctrl.handleItemAttachmentsHandler
|
||||
}
|
||||
|
||||
func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
uid, user, err := ctrl.partialParseIdAndUser(w, r)
|
||||
ID, err := ctrl.routeID(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -181,7 +181,7 @@ func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r
|
||||
|
||||
// Token Handler
|
||||
case http.MethodGet:
|
||||
token, err := ctrl.svc.Items.AttachmentToken(ctx, uid, attachmentId)
|
||||
token, err := ctrl.svc.Items.AttachmentToken(ctx, ID, attachmentId)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case services.ErrNotFound:
|
||||
@@ -210,7 +210,7 @@ func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r
|
||||
|
||||
// Delete Attachment Handler
|
||||
case http.MethodDelete:
|
||||
err = ctrl.svc.Items.AttachmentDelete(r.Context(), user.GroupID, uid, attachmentId)
|
||||
err = ctrl.svc.Items.AttachmentDelete(r.Context(), ctx.GID, ID, attachmentId)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to delete attachment")
|
||||
server.RespondServerError(w)
|
||||
@@ -230,7 +230,7 @@ func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r
|
||||
}
|
||||
|
||||
attachment.ID = attachmentId
|
||||
val, err := ctrl.svc.Items.AttachmentUpdate(ctx, uid, &attachment)
|
||||
val, err := ctrl.svc.Items.AttachmentUpdate(ctx, ID, &attachment)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to delete attachment")
|
||||
server.RespondServerError(w)
|
||||
|
||||
@@ -11,12 +11,12 @@ import (
|
||||
)
|
||||
|
||||
// HandleLabelsGetAll godoc
|
||||
// @Summary Get All Labels
|
||||
// @Tags Labels
|
||||
// @Produce json
|
||||
// @Success 200 {object} server.Results{items=[]repo.LabelOut}
|
||||
// @Router /v1/labels [GET]
|
||||
// @Security Bearer
|
||||
// @Summary Get All Labels
|
||||
// @Tags Labels
|
||||
// @Produce json
|
||||
// @Success 200 {object} server.Results{items=[]repo.LabelOut}
|
||||
// @Router /v1/labels [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLabelsGetAll() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
user := services.UseUserCtx(r.Context())
|
||||
@@ -31,13 +31,13 @@ func (ctrl *V1Controller) HandleLabelsGetAll() http.HandlerFunc {
|
||||
}
|
||||
|
||||
// HandleLabelsCreate godoc
|
||||
// @Summary Create a new label
|
||||
// @Tags Labels
|
||||
// @Produce json
|
||||
// @Param payload body repo.LabelCreate true "Label Data"
|
||||
// @Success 200 {object} repo.LabelSummary
|
||||
// @Router /v1/labels [POST]
|
||||
// @Security Bearer
|
||||
// @Summary Create a new label
|
||||
// @Tags Labels
|
||||
// @Produce json
|
||||
// @Param payload body repo.LabelCreate true "Label Data"
|
||||
// @Success 200 {object} repo.LabelSummary
|
||||
// @Router /v1/labels [POST]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLabelsCreate() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
createData := repo.LabelCreate{}
|
||||
@@ -56,95 +56,95 @@ func (ctrl *V1Controller) HandleLabelsCreate() http.HandlerFunc {
|
||||
}
|
||||
|
||||
server.Respond(w, http.StatusCreated, label)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// HandleLabelDelete godocs
|
||||
// @Summary deletes a label
|
||||
// @Tags Labels
|
||||
// @Produce json
|
||||
// @Param id path string true "Label ID"
|
||||
// @Success 204
|
||||
// @Router /v1/labels/{id} [DELETE]
|
||||
// @Security Bearer
|
||||
// @Summary deletes a label
|
||||
// @Tags Labels
|
||||
// @Produce json
|
||||
// @Param id path string true "Label ID"
|
||||
// @Success 204
|
||||
// @Router /v1/labels/{id} [DELETE]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLabelDelete() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
uid, user, err := ctrl.partialParseIdAndUser(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = ctrl.svc.Labels.Delete(r.Context(), user.GroupID, uid)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("error deleting label")
|
||||
server.RespondServerError(w)
|
||||
return
|
||||
}
|
||||
server.Respond(w, http.StatusNoContent, nil)
|
||||
}
|
||||
return ctrl.handleLabelsGeneral()
|
||||
}
|
||||
|
||||
// HandleLabelGet godocs
|
||||
// @Summary Gets a label and fields
|
||||
// @Tags Labels
|
||||
// @Produce json
|
||||
// @Param id path string true "Label ID"
|
||||
// @Success 200 {object} repo.LabelOut
|
||||
// @Router /v1/labels/{id} [GET]
|
||||
// @Security Bearer
|
||||
// @Summary Gets a label and fields
|
||||
// @Tags Labels
|
||||
// @Produce json
|
||||
// @Param id path string true "Label ID"
|
||||
// @Success 200 {object} repo.LabelOut
|
||||
// @Router /v1/labels/{id} [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLabelGet() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
uid, user, err := ctrl.partialParseIdAndUser(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
labels, err := ctrl.svc.Labels.Get(r.Context(), user.GroupID, uid)
|
||||
if err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
log.Err(err).
|
||||
Str("id", uid.String()).
|
||||
Msg("label not found")
|
||||
server.RespondError(w, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
log.Err(err).Msg("error getting label")
|
||||
server.RespondServerError(w)
|
||||
return
|
||||
}
|
||||
server.Respond(w, http.StatusOK, labels)
|
||||
}
|
||||
return ctrl.handleLabelsGeneral()
|
||||
}
|
||||
|
||||
// HandleLabelUpdate godocs
|
||||
// @Summary updates a label
|
||||
// @Tags Labels
|
||||
// @Produce json
|
||||
// @Param id path string true "Label ID"
|
||||
// @Success 200 {object} repo.LabelOut
|
||||
// @Router /v1/labels/{id} [PUT]
|
||||
// @Security Bearer
|
||||
// @Summary updates a label
|
||||
// @Tags Labels
|
||||
// @Produce json
|
||||
// @Param id path string true "Label ID"
|
||||
// @Success 200 {object} repo.LabelOut
|
||||
// @Router /v1/labels/{id} [PUT]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLabelUpdate() http.HandlerFunc {
|
||||
return ctrl.handleLabelsGeneral()
|
||||
}
|
||||
|
||||
func (ctrl *V1Controller) handleLabelsGeneral() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
body := repo.LabelUpdate{}
|
||||
if err := server.Decode(r, &body); err != nil {
|
||||
log.Err(err).Msg("error decoding label update data")
|
||||
server.RespondError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
uid, user, err := ctrl.partialParseIdAndUser(w, r)
|
||||
ctx := services.NewContext(r.Context())
|
||||
ID, err := ctrl.routeID(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
body.ID = uid
|
||||
result, err := ctrl.svc.Labels.Update(r.Context(), user.GroupID, body)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("error updating label")
|
||||
server.RespondServerError(w)
|
||||
return
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
labels, err := ctrl.svc.Labels.Get(r.Context(), ctx.GID, ID)
|
||||
if err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
log.Err(err).
|
||||
Str("id", ID.String()).
|
||||
Msg("label not found")
|
||||
server.RespondError(w, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
log.Err(err).Msg("error getting label")
|
||||
server.RespondServerError(w)
|
||||
return
|
||||
}
|
||||
server.Respond(w, http.StatusOK, labels)
|
||||
|
||||
case http.MethodDelete:
|
||||
err = ctrl.svc.Labels.Delete(r.Context(), ctx.GID, ID)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("error deleting label")
|
||||
server.RespondServerError(w)
|
||||
return
|
||||
}
|
||||
server.Respond(w, http.StatusNoContent, nil)
|
||||
|
||||
case http.MethodPut:
|
||||
body := repo.LabelUpdate{}
|
||||
if err := server.Decode(r, &body); err != nil {
|
||||
log.Err(err).Msg("error decoding label update data")
|
||||
server.RespondError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
body.ID = ID
|
||||
result, err := ctrl.svc.Labels.Update(r.Context(), ctx.GID, body)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("error updating label")
|
||||
server.RespondServerError(w)
|
||||
return
|
||||
}
|
||||
server.Respond(w, http.StatusOK, result)
|
||||
}
|
||||
server.Respond(w, http.StatusOK, result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,12 +11,12 @@ import (
|
||||
)
|
||||
|
||||
// HandleLocationGetAll godoc
|
||||
// @Summary Get All Locations
|
||||
// @Tags Locations
|
||||
// @Produce json
|
||||
// @Success 200 {object} server.Results{items=[]repo.LocationOutCount}
|
||||
// @Router /v1/locations [GET]
|
||||
// @Security Bearer
|
||||
// @Summary Get All Locations
|
||||
// @Tags Locations
|
||||
// @Produce json
|
||||
// @Success 200 {object} server.Results{items=[]repo.LocationOutCount}
|
||||
// @Router /v1/locations [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLocationGetAll() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
user := services.UseUserCtx(r.Context())
|
||||
@@ -32,13 +32,13 @@ func (ctrl *V1Controller) HandleLocationGetAll() http.HandlerFunc {
|
||||
}
|
||||
|
||||
// HandleLocationCreate godoc
|
||||
// @Summary Create a new location
|
||||
// @Tags Locations
|
||||
// @Produce json
|
||||
// @Param payload body repo.LocationCreate true "Location Data"
|
||||
// @Success 200 {object} repo.LocationSummary
|
||||
// @Router /v1/locations [POST]
|
||||
// @Security Bearer
|
||||
// @Summary Create a new location
|
||||
// @Tags Locations
|
||||
// @Produce json
|
||||
// @Param payload body repo.LocationCreate true "Location Data"
|
||||
// @Success 200 {object} repo.LocationSummary
|
||||
// @Router /v1/locations [POST]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLocationCreate() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
createData := repo.LocationCreate{}
|
||||
@@ -61,97 +61,93 @@ func (ctrl *V1Controller) HandleLocationCreate() http.HandlerFunc {
|
||||
}
|
||||
|
||||
// HandleLocationDelete godocs
|
||||
// @Summary deletes a location
|
||||
// @Tags Locations
|
||||
// @Produce json
|
||||
// @Param id path string true "Location ID"
|
||||
// @Success 204
|
||||
// @Router /v1/locations/{id} [DELETE]
|
||||
// @Security Bearer
|
||||
// @Summary deletes a location
|
||||
// @Tags Locations
|
||||
// @Produce json
|
||||
// @Param id path string true "Location ID"
|
||||
// @Success 204
|
||||
// @Router /v1/locations/{id} [DELETE]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLocationDelete() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
uid, user, err := ctrl.partialParseIdAndUser(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = ctrl.svc.Location.Delete(r.Context(), user.GroupID, uid)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to delete location")
|
||||
server.RespondServerError(w)
|
||||
return
|
||||
}
|
||||
server.Respond(w, http.StatusNoContent, nil)
|
||||
}
|
||||
return ctrl.handleLocationGeneral()
|
||||
}
|
||||
|
||||
// HandleLocationGet godocs
|
||||
// @Summary Gets a location and fields
|
||||
// @Tags Locations
|
||||
// @Produce json
|
||||
// @Param id path string true "Location ID"
|
||||
// @Success 200 {object} repo.LocationOut
|
||||
// @Router /v1/locations/{id} [GET]
|
||||
// @Security Bearer
|
||||
// @Summary Gets a location and fields
|
||||
// @Tags Locations
|
||||
// @Produce json
|
||||
// @Param id path string true "Location ID"
|
||||
// @Success 200 {object} repo.LocationOut
|
||||
// @Router /v1/locations/{id} [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLocationGet() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
uid, user, err := ctrl.partialParseIdAndUser(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
location, err := ctrl.svc.Location.GetOne(r.Context(), user.GroupID, uid)
|
||||
if err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
log.Err(err).
|
||||
Str("id", uid.String()).
|
||||
Str("gid", user.GroupID.String()).
|
||||
Msg("location not found")
|
||||
server.RespondError(w, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Err(err).
|
||||
Str("id", uid.String()).
|
||||
Str("gid", user.GroupID.String()).
|
||||
Msg("failed to get location")
|
||||
server.RespondServerError(w)
|
||||
return
|
||||
}
|
||||
server.Respond(w, http.StatusOK, location)
|
||||
}
|
||||
return ctrl.handleLocationGeneral()
|
||||
}
|
||||
|
||||
// HandleLocationUpdate godocs
|
||||
// @Summary updates a location
|
||||
// @Tags Locations
|
||||
// @Produce json
|
||||
// @Param id path string true "Location ID"
|
||||
// @Success 200 {object} repo.LocationOut
|
||||
// @Router /v1/locations/{id} [PUT]
|
||||
// @Security Bearer
|
||||
// @Summary updates a location
|
||||
// @Tags Locations
|
||||
// @Produce json
|
||||
// @Param id path string true "Location ID"
|
||||
// @Success 200 {object} repo.LocationOut
|
||||
// @Router /v1/locations/{id} [PUT]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLocationUpdate() http.HandlerFunc {
|
||||
return ctrl.handleLocationGeneral()
|
||||
}
|
||||
|
||||
func (ctrl *V1Controller) handleLocationGeneral() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
body := repo.LocationUpdate{}
|
||||
if err := server.Decode(r, &body); err != nil {
|
||||
log.Err(err).Msg("failed to decode location update data")
|
||||
server.RespondError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
uid, user, err := ctrl.partialParseIdAndUser(w, r)
|
||||
ctx := services.NewContext(r.Context())
|
||||
ID, err := ctrl.routeID(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
body.ID = uid
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
location, err := ctrl.svc.Location.GetOne(r.Context(), ctx.GID, ID)
|
||||
if err != nil {
|
||||
l := log.Err(err).
|
||||
Str("ID", ID.String()).
|
||||
Str("GID", ctx.GID.String())
|
||||
|
||||
result, err := ctrl.svc.Location.Update(r.Context(), user.GroupID, body)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to update location")
|
||||
server.RespondServerError(w)
|
||||
return
|
||||
if ent.IsNotFound(err) {
|
||||
l.Msg("location not found")
|
||||
server.RespondError(w, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
l.Msg("failed to get location")
|
||||
server.RespondServerError(w)
|
||||
return
|
||||
}
|
||||
server.Respond(w, http.StatusOK, location)
|
||||
case http.MethodPut:
|
||||
body := repo.LocationUpdate{}
|
||||
if err := server.Decode(r, &body); err != nil {
|
||||
log.Err(err).Msg("failed to decode location update data")
|
||||
server.RespondError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
body.ID = ID
|
||||
|
||||
result, err := ctrl.svc.Location.Update(r.Context(), ctx.GID, body)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to update location")
|
||||
server.RespondServerError(w)
|
||||
return
|
||||
}
|
||||
server.Respond(w, http.StatusOK, result)
|
||||
case http.MethodDelete:
|
||||
err = ctrl.svc.Location.Delete(r.Context(), ctx.GID, ID)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to delete location")
|
||||
server.RespondServerError(w)
|
||||
return
|
||||
}
|
||||
server.Respond(w, http.StatusNoContent, nil)
|
||||
}
|
||||
server.Respond(w, http.StatusOK, result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,12 +11,12 @@ import (
|
||||
)
|
||||
|
||||
// HandleUserSelf godoc
|
||||
// @Summary Get the current user
|
||||
// @Tags User
|
||||
// @Produce json
|
||||
// @Param payload body services.UserRegistration true "User Data"
|
||||
// @Success 204
|
||||
// @Router /v1/users/register [Post]
|
||||
// @Summary Get the current user
|
||||
// @Tags User
|
||||
// @Produce json
|
||||
// @Param payload body services.UserRegistration true "User Data"
|
||||
// @Success 204
|
||||
// @Router /v1/users/register [Post]
|
||||
func (ctrl *V1Controller) HandleUserRegistration() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
regData := services.UserRegistration{}
|
||||
@@ -27,6 +27,11 @@ func (ctrl *V1Controller) HandleUserRegistration() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
if !ctrl.allowRegistration && regData.GroupToken == "" {
|
||||
server.RespondError(w, http.StatusForbidden, nil)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := ctrl.svc.User.RegisterUser(r.Context(), regData)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to register user")
|
||||
@@ -39,12 +44,12 @@ func (ctrl *V1Controller) HandleUserRegistration() http.HandlerFunc {
|
||||
}
|
||||
|
||||
// HandleUserSelf godoc
|
||||
// @Summary Get the current user
|
||||
// @Tags User
|
||||
// @Produce json
|
||||
// @Success 200 {object} server.Result{item=repo.UserOut}
|
||||
// @Router /v1/users/self [GET]
|
||||
// @Security Bearer
|
||||
// @Summary Get the current user
|
||||
// @Tags User
|
||||
// @Produce json
|
||||
// @Success 200 {object} server.Result{item=repo.UserOut}
|
||||
// @Router /v1/users/self [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleUserSelf() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
token := services.UseTokenCtx(r.Context())
|
||||
@@ -60,13 +65,13 @@ func (ctrl *V1Controller) HandleUserSelf() http.HandlerFunc {
|
||||
}
|
||||
|
||||
// HandleUserSelfUpdate godoc
|
||||
// @Summary Update the current user
|
||||
// @Tags User
|
||||
// @Produce json
|
||||
// @Param payload body repo.UserUpdate true "User Data"
|
||||
// @Success 200 {object} server.Result{item=repo.UserUpdate}
|
||||
// @Router /v1/users/self [PUT]
|
||||
// @Security Bearer
|
||||
// @Summary Update the current user
|
||||
// @Tags User
|
||||
// @Produce json
|
||||
// @Param payload body repo.UserUpdate true "User Data"
|
||||
// @Success 200 {object} server.Result{item=repo.UserUpdate}
|
||||
// @Router /v1/users/self [PUT]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleUserSelfUpdate() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
updateData := repo.UserUpdate{}
|
||||
@@ -80,7 +85,6 @@ func (ctrl *V1Controller) HandleUserSelfUpdate() http.HandlerFunc {
|
||||
newData, err := ctrl.svc.User.UpdateSelf(r.Context(), actor.ID, updateData)
|
||||
|
||||
if err != nil {
|
||||
|
||||
server.RespondError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
@@ -89,27 +93,20 @@ func (ctrl *V1Controller) HandleUserSelfUpdate() http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// HandleUserUpdatePassword godoc
|
||||
// @Summary Update the current user's password // TODO:
|
||||
// @Tags User
|
||||
// @Produce json
|
||||
// @Success 204
|
||||
// @Router /v1/users/self/password [PUT]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleUserUpdatePassword() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// HandleUserSelfDelete godoc
|
||||
// @Summary Deletes the user account
|
||||
// @Tags User
|
||||
// @Produce json
|
||||
// @Success 204
|
||||
// @Router /v1/users/self [DELETE]
|
||||
// @Security Bearer
|
||||
// @Summary Deletes the user account
|
||||
// @Tags User
|
||||
// @Produce json
|
||||
// @Success 204
|
||||
// @Router /v1/users/self [DELETE]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleUserSelfDelete() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if ctrl.isDemo {
|
||||
server.RespondError(w, http.StatusForbidden, nil)
|
||||
return
|
||||
}
|
||||
|
||||
actor := services.UseUserCtx(r.Context())
|
||||
if err := ctrl.svc.User.DeleteSelf(r.Context(), actor.ID); err != nil {
|
||||
server.RespondError(w, http.StatusInternalServerError, err)
|
||||
@@ -128,14 +125,19 @@ type (
|
||||
)
|
||||
|
||||
// HandleUserSelfChangePassword godoc
|
||||
// @Summary Updates the users password
|
||||
// @Tags User
|
||||
// @Success 204
|
||||
// @Param payload body ChangePassword true "Password Payload"
|
||||
// @Router /v1/users/change-password [PUT]
|
||||
// @Security Bearer
|
||||
// @Summary Updates the users password
|
||||
// @Tags User
|
||||
// @Success 204
|
||||
// @Param payload body ChangePassword true "Password Payload"
|
||||
// @Router /v1/users/change-password [PUT]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleUserSelfChangePassword() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if ctrl.isDemo {
|
||||
server.RespondError(w, http.StatusForbidden, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var cp ChangePassword
|
||||
err := server.Decode(r, &cp)
|
||||
if err != nil {
|
||||
|
||||
@@ -95,6 +95,7 @@ const (
|
||||
TypeManual Type = "manual"
|
||||
TypeWarranty Type = "warranty"
|
||||
TypeAttachment Type = "attachment"
|
||||
TypeReceipt Type = "receipt"
|
||||
)
|
||||
|
||||
func (_type Type) String() string {
|
||||
@@ -104,7 +105,7 @@ func (_type Type) String() string {
|
||||
// TypeValidator is a validator for the "type" field enum values. It is called by the builders before save.
|
||||
func TypeValidator(_type Type) error {
|
||||
switch _type {
|
||||
case TypePhoto, TypeManual, TypeWarranty, TypeAttachment:
|
||||
case TypePhoto, TypeManual, TypeWarranty, TypeAttachment, TypeReceipt:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("attachment: invalid enum value for type field: %q", _type)
|
||||
|
||||
@@ -121,6 +121,9 @@ const DefaultCurrency = CurrencyUsd
|
||||
// Currency values.
|
||||
const (
|
||||
CurrencyUsd Currency = "usd"
|
||||
CurrencyEur Currency = "eur"
|
||||
CurrencyGbp Currency = "gbp"
|
||||
CurrencyJpy Currency = "jpy"
|
||||
)
|
||||
|
||||
func (c Currency) String() string {
|
||||
@@ -130,7 +133,7 @@ func (c Currency) String() string {
|
||||
// CurrencyValidator is a validator for the "currency" field enum values. It is called by the builders before save.
|
||||
func CurrencyValidator(c Currency) error {
|
||||
switch c {
|
||||
case CurrencyUsd:
|
||||
case CurrencyUsd, CurrencyEur, CurrencyGbp, CurrencyJpy:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("group: invalid enum value for currency field: %q", c)
|
||||
|
||||
@@ -13,7 +13,7 @@ var (
|
||||
{Name: "id", Type: field.TypeUUID},
|
||||
{Name: "created_at", Type: field.TypeTime},
|
||||
{Name: "updated_at", Type: field.TypeTime},
|
||||
{Name: "type", Type: field.TypeEnum, Enums: []string{"photo", "manual", "warranty", "attachment"}, Default: "attachment"},
|
||||
{Name: "type", Type: field.TypeEnum, Enums: []string{"photo", "manual", "warranty", "attachment", "receipt"}, Default: "attachment"},
|
||||
{Name: "document_attachments", Type: field.TypeUUID},
|
||||
{Name: "item_attachments", Type: field.TypeUUID},
|
||||
}
|
||||
@@ -127,7 +127,7 @@ var (
|
||||
{Name: "created_at", Type: field.TypeTime},
|
||||
{Name: "updated_at", Type: field.TypeTime},
|
||||
{Name: "name", Type: field.TypeString, Size: 255},
|
||||
{Name: "currency", Type: field.TypeEnum, Enums: []string{"usd"}, Default: "usd"},
|
||||
{Name: "currency", Type: field.TypeEnum, Enums: []string{"usd", "eur", "gbp", "jpy"}, Default: "usd"},
|
||||
}
|
||||
// GroupsTable holds the schema information for the "groups" table.
|
||||
GroupsTable = &schema.Table{
|
||||
|
||||
@@ -22,7 +22,7 @@ func (Attachment) Mixin() []ent.Mixin {
|
||||
func (Attachment) Fields() []ent.Field {
|
||||
return []ent.Field{
|
||||
field.Enum("type").
|
||||
Values("photo", "manual", "warranty", "attachment").
|
||||
Values("photo", "manual", "warranty", "attachment", "receipt").
|
||||
Default("attachment"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func (Group) Fields() []ent.Field {
|
||||
NotEmpty(),
|
||||
field.Enum("currency").
|
||||
Default("usd").
|
||||
Values("usd"), // TODO: add more currencies
|
||||
Values("usd", "eur", "gbp", "jpy"), // TODO: add more currencies
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ require (
|
||||
github.com/rs/zerolog v1.28.0
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/swaggo/http-swagger v1.3.3
|
||||
github.com/swaggo/swag v1.8.6
|
||||
github.com/swaggo/swag v1.8.7
|
||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90
|
||||
)
|
||||
|
||||
@@ -42,6 +42,5 @@ require (
|
||||
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/tools v0.1.13-0.20220804200503-81c7dc4e4efa // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -90,8 +90,8 @@ github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a h1:kAe4YSu0O0UFn1DowN
|
||||
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
|
||||
github.com/swaggo/http-swagger v1.3.3 h1:Hu5Z0L9ssyBLofaama21iYaF2VbWyA8jdohaaCGpHsc=
|
||||
github.com/swaggo/http-swagger v1.3.3/go.mod h1:sE+4PjD89IxMPm77FnkDz0sdO+p5lbXzrVWT6OTVVGo=
|
||||
github.com/swaggo/swag v1.8.6 h1:2rgOaLbonWu1PLP6G+/rYjSvPg0jQE0HtrEKuE380eg=
|
||||
github.com/swaggo/swag v1.8.6/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg=
|
||||
github.com/swaggo/swag v1.8.7 h1:2K9ivTD3teEO+2fXV6zrZKDqk5IuU2aJtBDo8U7omWU=
|
||||
github.com/swaggo/swag v1.8.7/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk=
|
||||
github.com/zclconf/go-cty v1.11.0 h1:726SxLdi2SDnjY+BStqB9J1hNp4+2WlzyXLuimibIe0=
|
||||
github.com/zclconf/go-cty v1.11.0/go.mod h1:s9IfD1LK5ccNMSWCVFCE2rJfHiZgi7JijgeWIMfhLvA=
|
||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ardanlabs/conf/v2"
|
||||
"github.com/ardanlabs/conf/v2/yaml"
|
||||
|
||||
"os"
|
||||
)
|
||||
@@ -17,12 +16,14 @@ const (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Mode string `yaml:"mode" conf:"default:development"` // development or production
|
||||
Web WebConfig `yaml:"web"`
|
||||
Storage Storage `yaml:"storage"`
|
||||
Log LoggerConf `yaml:"logger"`
|
||||
Mailer MailerConf `yaml:"mailer"`
|
||||
Swagger SwaggerConf `yaml:"swagger"`
|
||||
Mode string `yaml:"mode" conf:"default:development"` // development or production
|
||||
Web WebConfig `yaml:"web"`
|
||||
Storage Storage `yaml:"storage"`
|
||||
Log LoggerConf `yaml:"logger"`
|
||||
Mailer MailerConf `yaml:"mailer"`
|
||||
Swagger SwaggerConf `yaml:"swagger"`
|
||||
Demo bool `yaml:"demo"`
|
||||
AllowRegistration bool `yaml:"disable_registration" conf:"default:true"`
|
||||
}
|
||||
|
||||
type SwaggerConf struct {
|
||||
@@ -36,24 +37,13 @@ type WebConfig struct {
|
||||
MaxUploadSize int64 `yaml:"max_file_upload" conf:"default:10"`
|
||||
}
|
||||
|
||||
// NewConfig parses the CLI/Config file and returns a Config struct. If the file argument is an empty string, the
|
||||
// New parses the CLI/Config file and returns a Config struct. If the file argument is an empty string, the
|
||||
// file is not read. If the file is not empty, the file is read and the Config struct is returned.
|
||||
func NewConfig(file string) (*Config, error) {
|
||||
func New() (*Config, error) {
|
||||
var cfg Config
|
||||
|
||||
const prefix = "HBOX"
|
||||
|
||||
help, err := func() (string, error) {
|
||||
if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) {
|
||||
return conf.Parse(prefix, &cfg)
|
||||
} else {
|
||||
yamlData, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return conf.Parse(prefix, &cfg, yaml.WithData(yamlData))
|
||||
}
|
||||
}()
|
||||
help, err := conf.Parse(prefix, &cfg)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, conf.ErrHelpWanted) {
|
||||
|
||||
12
backend/internal/repo/pagination.go
Normal file
12
backend/internal/repo/pagination.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package repo
|
||||
|
||||
type PaginationResult[T any] struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
Total int `json:"total"`
|
||||
Items []T `json:"items"`
|
||||
}
|
||||
|
||||
func calculateOffset(page, pageSize int) int {
|
||||
return (page - 1) * pageSize
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/homebox/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/ent/group"
|
||||
"github.com/hay-kot/homebox/backend/ent/groupinvitationtoken"
|
||||
)
|
||||
|
||||
@@ -15,11 +16,16 @@ type GroupRepository struct {
|
||||
|
||||
type (
|
||||
Group struct {
|
||||
ID uuid.UUID
|
||||
Name string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Currency string
|
||||
ID uuid.UUID `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||
UpdatedAt time.Time `json:"updatedAt,omitempty"`
|
||||
Currency string `json:"currency,omitempty"`
|
||||
}
|
||||
|
||||
GroupUpdate struct {
|
||||
Name string `json:"name"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
|
||||
GroupInvitationCreate struct {
|
||||
@@ -69,6 +75,17 @@ func (r *GroupRepository) GroupCreate(ctx context.Context, name string) (Group,
|
||||
Save(ctx))
|
||||
}
|
||||
|
||||
func (r *GroupRepository) GroupUpdate(ctx context.Context, ID uuid.UUID, data GroupUpdate) (Group, error) {
|
||||
currency := group.Currency(data.Currency)
|
||||
|
||||
entity, err := r.db.Group.UpdateOneID(ID).
|
||||
SetName(data.Name).
|
||||
SetCurrency(currency).
|
||||
Save(ctx)
|
||||
|
||||
return mapToGroupErr(entity, err)
|
||||
}
|
||||
|
||||
func (r *GroupRepository) GroupByID(ctx context.Context, id uuid.UUID) (Group, error) {
|
||||
return mapToGroupErr(r.db.Group.Get(ctx, id))
|
||||
}
|
||||
|
||||
@@ -18,3 +18,16 @@ func Test_Group_Create(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, g.ID, foundGroup.ID)
|
||||
}
|
||||
|
||||
func Test_Group_Update(t *testing.T) {
|
||||
g, err := tRepos.Groups.GroupCreate(context.Background(), "test")
|
||||
assert.NoError(t, err)
|
||||
|
||||
g, err = tRepos.Groups.GroupUpdate(context.Background(), g.ID, GroupUpdate{
|
||||
Name: "test2",
|
||||
Currency: "eur",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test2", g.Name)
|
||||
assert.Equal(t, "eur", g.Currency)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ import (
|
||||
"github.com/hay-kot/homebox/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/ent/group"
|
||||
"github.com/hay-kot/homebox/backend/ent/item"
|
||||
"github.com/hay-kot/homebox/backend/ent/itemfield"
|
||||
"github.com/hay-kot/homebox/backend/ent/label"
|
||||
"github.com/hay-kot/homebox/backend/ent/location"
|
||||
"github.com/hay-kot/homebox/backend/ent/predicate"
|
||||
)
|
||||
|
||||
@@ -16,6 +19,25 @@ type ItemsRepository struct {
|
||||
}
|
||||
|
||||
type (
|
||||
ItemQuery struct {
|
||||
Page int
|
||||
PageSize int
|
||||
Search string `json:"search"`
|
||||
LocationIDs []uuid.UUID `json:"locationIds"`
|
||||
LabelIDs []uuid.UUID `json:"labelIds"`
|
||||
SortBy string `json:"sortBy"`
|
||||
}
|
||||
|
||||
ItemField struct {
|
||||
ID uuid.UUID `json:"id,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
TextValue string `json:"textValue"`
|
||||
NumberValue int `json:"numberValue"`
|
||||
BooleanValue bool `json:"booleanValue"`
|
||||
TimeValue time.Time `json:"timeValue,omitempty"`
|
||||
}
|
||||
|
||||
ItemCreate struct {
|
||||
ImportRef string `json:"-"`
|
||||
Name string `json:"name"`
|
||||
@@ -58,8 +80,8 @@ type (
|
||||
SoldNotes string `json:"soldNotes"`
|
||||
|
||||
// Extras
|
||||
Notes string `json:"notes"`
|
||||
// Fields []*FieldSummary `json:"fields"`
|
||||
Notes string `json:"notes"`
|
||||
Fields []ItemField `json:"fields"`
|
||||
}
|
||||
|
||||
ItemSummary struct {
|
||||
@@ -105,7 +127,7 @@ type (
|
||||
|
||||
Attachments []ItemAttachment `json:"attachments"`
|
||||
// Future
|
||||
// Fields []*FieldSummary `json:"fields"`
|
||||
Fields []ItemField `json:"fields"`
|
||||
}
|
||||
)
|
||||
|
||||
@@ -145,12 +167,33 @@ var (
|
||||
mapItemOutErr = mapTErrFunc(mapItemOut)
|
||||
)
|
||||
|
||||
func mapFields(fields []*ent.ItemField) []ItemField {
|
||||
result := make([]ItemField, len(fields))
|
||||
for i, f := range fields {
|
||||
result[i] = ItemField{
|
||||
ID: f.ID,
|
||||
Type: f.Type.String(),
|
||||
Name: f.Name,
|
||||
TextValue: f.TextValue,
|
||||
NumberValue: f.NumberValue,
|
||||
BooleanValue: f.BooleanValue,
|
||||
TimeValue: f.TimeValue,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func mapItemOut(item *ent.Item) ItemOut {
|
||||
var attachments []ItemAttachment
|
||||
if item.Edges.Attachments != nil {
|
||||
attachments = mapEach(item.Edges.Attachments, ToItemAttachment)
|
||||
}
|
||||
|
||||
var fields []ItemField
|
||||
if item.Edges.Fields != nil {
|
||||
fields = mapFields(item.Edges.Fields)
|
||||
}
|
||||
|
||||
return ItemOut{
|
||||
ItemSummary: mapItemSummary(item),
|
||||
LifetimeWarranty: item.LifetimeWarranty,
|
||||
@@ -176,6 +219,7 @@ func mapItemOut(item *ent.Item) ItemOut {
|
||||
// Extras
|
||||
Notes: item.Notes,
|
||||
Attachments: attachments,
|
||||
Fields: fields,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,12 +244,76 @@ func (e *ItemsRepository) GetOne(ctx context.Context, id uuid.UUID) (ItemOut, er
|
||||
return e.getOne(ctx, item.ID(id))
|
||||
}
|
||||
|
||||
func (e *ItemsRepository) CheckRef(ctx context.Context, GID uuid.UUID, ref string) (bool, error) {
|
||||
q := e.db.Item.Query().Where(item.HasGroupWith(group.ID(GID)))
|
||||
return q.Where(item.ImportRef(ref)).Exist(ctx)
|
||||
}
|
||||
|
||||
// GetOneByGroup returns a single item by ID. If the item does not exist, an error is returned.
|
||||
// GetOneByGroup ensures that the item belongs to a specific group.
|
||||
func (e *ItemsRepository) GetOneByGroup(ctx context.Context, gid, id uuid.UUID) (ItemOut, error) {
|
||||
return e.getOne(ctx, item.ID(id), item.HasGroupWith(group.ID(gid)))
|
||||
}
|
||||
|
||||
// QueryByGroup returns a list of items that belong to a specific group based on the provided query.
|
||||
func (e *ItemsRepository) QueryByGroup(ctx context.Context, gid uuid.UUID, q ItemQuery) (PaginationResult[ItemSummary], error) {
|
||||
qb := e.db.Item.Query().Where(item.HasGroupWith(group.ID(gid)))
|
||||
|
||||
if len(q.LabelIDs) > 0 {
|
||||
labels := make([]predicate.Item, 0, len(q.LabelIDs))
|
||||
for _, l := range q.LabelIDs {
|
||||
labels = append(labels, item.HasLabelWith(label.ID(l)))
|
||||
}
|
||||
qb = qb.Where(item.Or(labels...))
|
||||
}
|
||||
|
||||
if len(q.LocationIDs) > 0 {
|
||||
locations := make([]predicate.Item, 0, len(q.LocationIDs))
|
||||
for _, l := range q.LocationIDs {
|
||||
locations = append(locations, item.HasLocationWith(location.ID(l)))
|
||||
}
|
||||
qb = qb.Where(item.Or(locations...))
|
||||
}
|
||||
|
||||
if q.Search != "" {
|
||||
qb.Where(
|
||||
item.Or(
|
||||
item.NameContainsFold(q.Search),
|
||||
item.DescriptionContainsFold(q.Search),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if q.Page != -1 || q.PageSize != -1 {
|
||||
qb = qb.
|
||||
Offset(calculateOffset(q.Page, q.PageSize)).
|
||||
Limit(q.PageSize)
|
||||
}
|
||||
|
||||
items, err := mapItemsSummaryErr(
|
||||
qb.Order(ent.Asc(item.FieldName)).
|
||||
WithLabel().
|
||||
WithLocation().
|
||||
All(ctx),
|
||||
)
|
||||
if err != nil {
|
||||
return PaginationResult[ItemSummary]{}, err
|
||||
}
|
||||
|
||||
count, err := qb.Count(ctx)
|
||||
if err != nil {
|
||||
return PaginationResult[ItemSummary]{}, err
|
||||
}
|
||||
|
||||
return PaginationResult[ItemSummary]{
|
||||
Page: q.Page,
|
||||
PageSize: q.PageSize,
|
||||
Total: count,
|
||||
Items: items,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
// GetAll returns all the items in the database with the Labels and Locations eager loaded.
|
||||
func (e *ItemsRepository) GetAll(ctx context.Context, gid uuid.UUID) ([]ItemSummary, error) {
|
||||
return mapItemsSummaryErr(e.db.Item.Query().
|
||||
@@ -217,6 +325,7 @@ func (e *ItemsRepository) GetAll(ctx context.Context, gid uuid.UUID) ([]ItemSumm
|
||||
|
||||
func (e *ItemsRepository) Create(ctx context.Context, gid uuid.UUID, data ItemCreate) (ItemOut, error) {
|
||||
q := e.db.Item.Create().
|
||||
SetImportRef(data.ImportRef).
|
||||
SetName(data.Name).
|
||||
SetDescription(data.Description).
|
||||
SetGroupID(gid).
|
||||
@@ -294,5 +403,63 @@ func (e *ItemsRepository) UpdateByGroup(ctx context.Context, gid uuid.UUID, data
|
||||
return ItemOut{}, err
|
||||
}
|
||||
|
||||
fields, err := e.db.ItemField.Query().Where(itemfield.HasItemWith(item.ID(data.ID))).All(ctx)
|
||||
if err != nil {
|
||||
return ItemOut{}, err
|
||||
}
|
||||
|
||||
fieldIds := newIDSet(fields)
|
||||
|
||||
// Update Existing Fields
|
||||
for _, f := range data.Fields {
|
||||
if f.ID == uuid.Nil {
|
||||
// Create New Field
|
||||
_, err = e.db.ItemField.Create().
|
||||
SetItemID(data.ID).
|
||||
SetType(itemfield.Type(f.Type)).
|
||||
SetName(f.Name).
|
||||
SetTextValue(f.TextValue).
|
||||
SetNumberValue(f.NumberValue).
|
||||
SetBooleanValue(f.BooleanValue).
|
||||
SetTimeValue(f.TimeValue).
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
return ItemOut{}, err
|
||||
}
|
||||
}
|
||||
|
||||
opt := e.db.ItemField.Update().
|
||||
Where(
|
||||
itemfield.ID(f.ID),
|
||||
itemfield.HasItemWith(item.ID(data.ID)),
|
||||
).
|
||||
SetType(itemfield.Type(f.Type)).
|
||||
SetName(f.Name).
|
||||
SetTextValue(f.TextValue).
|
||||
SetNumberValue(f.NumberValue).
|
||||
SetBooleanValue(f.BooleanValue).
|
||||
SetTimeValue(f.TimeValue)
|
||||
|
||||
_, err = opt.Save(ctx)
|
||||
if err != nil {
|
||||
return ItemOut{}, err
|
||||
}
|
||||
|
||||
fieldIds.Remove(f.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
// Delete Fields that are no longer present
|
||||
if fieldIds.Len() > 0 {
|
||||
_, err = e.db.ItemField.Delete().
|
||||
Where(
|
||||
itemfield.IDIn(fieldIds.Slice()...),
|
||||
itemfield.HasItemWith(item.ID(data.ID)),
|
||||
).Exec(ctx)
|
||||
if err != nil {
|
||||
return ItemOut{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return e.GetOne(ctx, data.ID)
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ func (r *LabelRepository) GetOneByGroup(ctx context.Context, gid, ld uuid.UUID)
|
||||
func (r *LabelRepository) GetAll(ctx context.Context, groupId uuid.UUID) ([]LabelSummary, error) {
|
||||
return mapLabelsOut(r.db.Label.Query().
|
||||
Where(label.HasGroupWith(group.ID(groupId))).
|
||||
Order(ent.Asc(label.FieldName)).
|
||||
WithGroup().
|
||||
All(ctx),
|
||||
)
|
||||
|
||||
@@ -94,7 +94,9 @@ func (r *LocationRepository) GetAll(ctx context.Context, groupId uuid.UUID) ([]L
|
||||
locations
|
||||
WHERE
|
||||
locations.group_locations = ?
|
||||
`
|
||||
ORDER BY
|
||||
locations.name ASC
|
||||
`
|
||||
|
||||
rows, err := r.db.Sql().QueryContext(ctx, query, groupId)
|
||||
if err != nil {
|
||||
|
||||
@@ -4,18 +4,20 @@ import "github.com/hay-kot/homebox/backend/internal/repo"
|
||||
|
||||
type AllServices struct {
|
||||
User *UserService
|
||||
Group *GroupService
|
||||
Location *LocationService
|
||||
Labels *LabelService
|
||||
Items *ItemService
|
||||
}
|
||||
|
||||
func NewServices(repos *repo.AllRepos) *AllServices {
|
||||
func New(repos *repo.AllRepos) *AllServices {
|
||||
if repos == nil {
|
||||
panic("repos cannot be nil")
|
||||
}
|
||||
|
||||
return &AllServices{
|
||||
User: &UserService{repos},
|
||||
Group: &GroupService{repos},
|
||||
Location: &LocationService{repos},
|
||||
Labels: &LabelService{repos},
|
||||
Items: &ItemService{
|
||||
|
||||
@@ -63,7 +63,7 @@ func TestMain(m *testing.M) {
|
||||
|
||||
tClient = client
|
||||
tRepos = repo.New(tClient, os.TempDir()+"/homebox")
|
||||
tSvc = NewServices(tRepos)
|
||||
tSvc = New(tRepos)
|
||||
defer client.Close()
|
||||
|
||||
bootstrap()
|
||||
|
||||
47
backend/internal/services/service_group.go
Normal file
47
backend/internal/services/service_group.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hay-kot/homebox/backend/internal/repo"
|
||||
"github.com/hay-kot/homebox/backend/pkgs/hasher"
|
||||
)
|
||||
|
||||
type GroupService struct {
|
||||
repos *repo.AllRepos
|
||||
}
|
||||
|
||||
func (svc *GroupService) Get(ctx Context) (repo.Group, error) {
|
||||
return svc.repos.Groups.GroupByID(ctx.Context, ctx.GID)
|
||||
}
|
||||
|
||||
func (svc *GroupService) UpdateGroup(ctx Context, data repo.GroupUpdate) (repo.Group, error) {
|
||||
if data.Name == "" {
|
||||
data.Name = ctx.User.GroupName
|
||||
}
|
||||
|
||||
if data.Currency == "" {
|
||||
return repo.Group{}, errors.New("currency cannot be empty")
|
||||
}
|
||||
|
||||
data.Currency = strings.ToLower(data.Currency)
|
||||
|
||||
return svc.repos.Groups.GroupUpdate(ctx.Context, ctx.GID, data)
|
||||
}
|
||||
|
||||
func (svc *GroupService) NewInvitation(ctx Context, uses int, expiresAt time.Time) (string, error) {
|
||||
token := hasher.GenerateToken()
|
||||
|
||||
_, err := svc.repos.Groups.InvitationCreate(ctx, ctx.GID, repo.GroupInvitationCreate{
|
||||
Token: token.Hash,
|
||||
Uses: uses,
|
||||
ExpiresAt: expiresAt,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token.Raw, nil
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/homebox/backend/internal/repo"
|
||||
@@ -28,6 +27,10 @@ func (svc *ItemService) GetOne(ctx context.Context, gid uuid.UUID, id uuid.UUID)
|
||||
return svc.repo.Items.GetOneByGroup(ctx, gid, id)
|
||||
}
|
||||
|
||||
func (svc *ItemService) Query(ctx Context, q repo.ItemQuery) (repo.PaginationResult[repo.ItemSummary], error) {
|
||||
return svc.repo.Items.QueryByGroup(ctx, ctx.GID, q)
|
||||
}
|
||||
|
||||
func (svc *ItemService) GetAll(ctx context.Context, gid uuid.UUID) ([]repo.ItemSummary, error) {
|
||||
return svc.repo.Items.GetAll(ctx, gid)
|
||||
}
|
||||
@@ -44,7 +47,7 @@ func (svc *ItemService) Update(ctx context.Context, gid uuid.UUID, data repo.Ite
|
||||
return svc.repo.Items.UpdateByGroup(ctx, gid, data)
|
||||
}
|
||||
|
||||
func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]string) error {
|
||||
func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]string) (int, error) {
|
||||
loaded := []csvRow{}
|
||||
|
||||
// Skip first row
|
||||
@@ -55,18 +58,41 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
|
||||
}
|
||||
|
||||
if len(row) != NumOfCols {
|
||||
return ErrInvalidCsv
|
||||
return 0, ErrInvalidCsv
|
||||
}
|
||||
|
||||
r := newCsvRow(row)
|
||||
loaded = append(loaded, r)
|
||||
}
|
||||
|
||||
// validate rows
|
||||
var errMap = map[int][]error{}
|
||||
var hasErr bool
|
||||
for i, r := range loaded {
|
||||
|
||||
errs := r.validate()
|
||||
|
||||
if len(errs) > 0 {
|
||||
hasErr = true
|
||||
lineNum := i + 2
|
||||
|
||||
errMap[lineNum] = errs
|
||||
}
|
||||
}
|
||||
|
||||
if hasErr {
|
||||
for lineNum, errs := range errMap {
|
||||
for _, err := range errs {
|
||||
log.Error().Err(err).Int("line", lineNum).Msg("csv import error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bootstrap the locations and labels so we can reuse the created IDs for the items
|
||||
locations := map[string]uuid.UUID{}
|
||||
existingLocation, err := svc.repo.Locations.GetAll(ctx, gid)
|
||||
if err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
for _, loc := range existingLocation {
|
||||
locations[loc.Name] = loc.ID
|
||||
@@ -75,7 +101,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
|
||||
labels := map[string]uuid.UUID{}
|
||||
existingLabels, err := svc.repo.Labels.GetAll(ctx, gid)
|
||||
if err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
for _, label := range existingLabels {
|
||||
labels[label.Name] = label.ID
|
||||
@@ -84,25 +110,21 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
|
||||
for _, row := range loaded {
|
||||
|
||||
// Locations
|
||||
if _, ok := locations[row.Location]; ok {
|
||||
continue
|
||||
if _, exists := locations[row.Location]; !exists {
|
||||
result, err := svc.repo.Locations.Create(ctx, gid, repo.LocationCreate{
|
||||
Name: row.Location,
|
||||
Description: "",
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
locations[row.Location] = result.ID
|
||||
}
|
||||
|
||||
fmt.Println("Creating Location: ", row.Location)
|
||||
|
||||
result, err := svc.repo.Locations.Create(ctx, gid, repo.LocationCreate{
|
||||
Name: row.Location,
|
||||
Description: "",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
locations[row.Location] = result.ID
|
||||
|
||||
// Labels
|
||||
|
||||
for _, label := range row.getLabels() {
|
||||
if _, ok := labels[label]; ok {
|
||||
if _, exists := labels[label]; exists {
|
||||
continue
|
||||
}
|
||||
result, err := svc.repo.Labels.Create(ctx, gid, repo.LabelCreate{
|
||||
@@ -110,14 +132,26 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
|
||||
Description: "",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
labels[label] = result.ID
|
||||
}
|
||||
}
|
||||
|
||||
// Create the items
|
||||
var count int
|
||||
for _, row := range loaded {
|
||||
// Check Import Ref
|
||||
if row.Item.ImportRef != "" {
|
||||
exists, err := svc.repo.Items.CheckRef(ctx, gid, row.Item.ImportRef)
|
||||
if exists {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
log.Err(err).Msg("error checking import ref")
|
||||
}
|
||||
}
|
||||
|
||||
locationID := locations[row.Location]
|
||||
labelIDs := []uuid.UUID{}
|
||||
for _, label := range row.getLabels() {
|
||||
@@ -127,8 +161,6 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
|
||||
log.Info().
|
||||
Str("name", row.Item.Name).
|
||||
Str("location", row.Location).
|
||||
Strs("labels", row.getLabels()).
|
||||
Str("locationId", locationID.String()).
|
||||
Msgf("Creating Item: %s", row.Item.Name)
|
||||
|
||||
result, err := svc.repo.Items.Create(ctx, gid, repo.ItemCreate{
|
||||
@@ -140,7 +172,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
return count, err
|
||||
}
|
||||
|
||||
// Update the item with the rest of the data
|
||||
@@ -179,8 +211,10 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
return count, err
|
||||
}
|
||||
|
||||
count++
|
||||
}
|
||||
return nil
|
||||
return count, nil
|
||||
}
|
||||
|
||||
@@ -97,3 +97,22 @@ func (c csvRow) getLabels() []string {
|
||||
|
||||
return split
|
||||
}
|
||||
|
||||
func (c csvRow) validate() []error {
|
||||
var errs []error
|
||||
|
||||
add := func(err error) {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
required := func(s string, name string) {
|
||||
if s == "" {
|
||||
add(errors.New(name + " is required"))
|
||||
}
|
||||
}
|
||||
|
||||
required(c.Location, "Location")
|
||||
required(c.Item.Name, "Name")
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@ import (
|
||||
|
||||
const CSV_DATA = `
|
||||
Import Ref,Location,Labels,Quantity,Name,Description,Insured,Serial Number,Mode Number,Manufacturer,Notes,Purchase From,Purchased Price,Purchased Time,Lifetime Warranty,Warranty Expires,Warranty Details,Sold To,Sold Price,Sold Time,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,,,,,,,
|
||||
,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,,,,,,,
|
||||
,Office,IOT;Home Assistant; Z-Wave,1,Zooz 110v Power Switch,"Zooz Z-Wave Plus Power Switch ZEN15 for 110V AC Units, Sump Pumps, Humidifiers, and More",,,ZEN15,Zooz,,Amazon,39.95,10/13/2021,,,,,,,
|
||||
,Downstairs,IOT;Home Assistant; Z-Wave,1,Ecolink Z-Wave PIR Motion Sensor,"Ecolink Z-Wave PIR Motion Detector Pet Immune, White (PIRZWAVE2.5-ECO)",,,PIRZWAVE2.5-ECO,Ecolink,,Amazon,35.58,10/21/2020,,,,,,,
|
||||
,Entry,IOT;Home Assistant; Z-Wave,1,Yale Security Touchscreen Deadbolt,"Yale Security YRD226-ZW2-619 YRD226ZW2619 Touchscreen Deadbolt, Satin Nickel",,,YRD226ZW2619,Yale,,Amazon,120.39,10/14/2020,,,,,,,
|
||||
,Kitchen,IOT;Home Assistant; Z-Wave,1,Smart Rocker Light Dimmer,"UltraPro Z-Wave Smart Rocker Light Dimmer with QuickFit and SimpleWire, 3-Way Ready, Compatible with Alexa, Google Assistant, ZWave Hub Required, Repeater/Range Extender, White Paddle Only, 39351",,,39351,Honeywell,,Amazon,65.98,09/30/0202,,,,,,,`
|
||||
A,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,,,,,,,
|
||||
B,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,,,,,,,
|
||||
C,Office,IOT;Home Assistant; Z-Wave,1,Zooz 110v Power Switch,"Zooz Z-Wave Plus Power Switch ZEN15 for 110V AC Units, Sump Pumps, Humidifiers, and More",,,ZEN15,Zooz,,Amazon,39.95,10/13/2021,,,,,,,
|
||||
D,Downstairs,IOT;Home Assistant; Z-Wave,1,Ecolink Z-Wave PIR Motion Sensor,"Ecolink Z-Wave PIR Motion Detector Pet Immune, White (PIRZWAVE2.5-ECO)",,,PIRZWAVE2.5-ECO,Ecolink,,Amazon,35.58,10/21/2020,,,,,,,
|
||||
E,Entry,IOT;Home Assistant; Z-Wave,1,Yale Security Touchscreen Deadbolt,"Yale Security YRD226-ZW2-619 YRD226ZW2619 Touchscreen Deadbolt, Satin Nickel",,,YRD226ZW2619,Yale,,Amazon,120.39,10/14/2020,,,,,,,
|
||||
F,Kitchen,IOT;Home Assistant; Z-Wave,1,Smart Rocker Light Dimmer,"UltraPro Z-Wave Smart Rocker Light Dimmer with QuickFit and SimpleWire, 3-Way Ready, Compatible with Alexa, Google Assistant, ZWave Hub Required, Repeater/Range Extender, White Paddle Only, 39351",,,39351,Honeywell,,Amazon,65.98,09/30/0202,,,,,,,`
|
||||
|
||||
func loadcsv() [][]string {
|
||||
reader := csv.NewReader(bytes.NewBuffer([]byte(CSV_DATA)))
|
||||
|
||||
@@ -13,7 +13,13 @@ func TestItemService_CsvImport(t *testing.T) {
|
||||
svc := &ItemService{
|
||||
repo: tRepos,
|
||||
}
|
||||
err := svc.CsvImport(context.Background(), tGroup.ID, data)
|
||||
count, err := svc.CsvImport(context.Background(), tGroup.ID, data)
|
||||
assert.Equal(t, 6, count)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check import refs are deduplicated
|
||||
count, err = svc.CsvImport(context.Background(), tGroup.ID, data)
|
||||
assert.Equal(t, 0, count)
|
||||
assert.NoError(t, err)
|
||||
|
||||
items, err := svc.GetAll(context.Background(), tGroup.ID)
|
||||
|
||||
@@ -186,21 +186,6 @@ func (svc *UserService) DeleteSelf(ctx context.Context, ID uuid.UUID) error {
|
||||
return svc.repos.Users.Delete(ctx, ID)
|
||||
}
|
||||
|
||||
func (svc *UserService) NewInvitation(ctx Context, uses int, expiresAt time.Time) (string, error) {
|
||||
token := hasher.GenerateToken()
|
||||
|
||||
_, err := svc.repos.Groups.InvitationCreate(ctx, ctx.GID, repo.GroupInvitationCreate{
|
||||
Token: token.Hash,
|
||||
Uses: uses,
|
||||
ExpiresAt: expiresAt,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token.Raw, nil
|
||||
}
|
||||
|
||||
func (svc *UserService) ChangePassword(ctx Context, current string, new string) (ok bool) {
|
||||
usr, err := svc.repos.Users.GetOneId(ctx, ctx.UID)
|
||||
if err != nil {
|
||||
|
||||
@@ -33,7 +33,7 @@ func (f *Faker) Path() string {
|
||||
}
|
||||
|
||||
func (f *Faker) Email() string {
|
||||
return f.Str(10) + "@email.com"
|
||||
return f.Str(10) + "@example.com"
|
||||
}
|
||||
|
||||
func (f *Faker) Bool() bool {
|
||||
|
||||
@@ -1,13 +1,37 @@
|
||||
package hasher
|
||||
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var enabled = true
|
||||
|
||||
func init() {
|
||||
disableHas := os.Getenv("UNSAFE_DISABLE_PASSWORD_PROJECTION") == "yes_i_am_sure"
|
||||
|
||||
if disableHas {
|
||||
fmt.Println("WARNING: Password projection is disabled. This is unsafe in production.")
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
if !enabled {
|
||||
return password, nil
|
||||
}
|
||||
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
func CheckPasswordHash(password, hash string) bool {
|
||||
if !enabled {
|
||||
return password == hash
|
||||
}
|
||||
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
@@ -23,29 +23,29 @@ Import RefLocation Labels Quantity Name Description Insured Serial Number Model
|
||||
|
||||
## CSV Reference
|
||||
|
||||
| Column | Type | Description |
|
||||
| ----------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ImportRef | String (100) | Future |
|
||||
| Location | String | This is the location of the item that will be created. These are de-duplicated and won't create another instance when reused. |
|
||||
| Labels | `;` Separated String | List of labels to apply to the item separated by a `;`, can be existing or new |
|
||||
| Quantity | Integer | The quantity of items to create |
|
||||
| Name | String | Name of the item |
|
||||
| Description | String | Description of the item |
|
||||
| Insured | Boolean | Whether or not the item is insured |
|
||||
| Serial Number | String | Serial number of the item |
|
||||
| Model Number | String | Model of the item |
|
||||
| Manufacturer | String | Manufacturer of the item |
|
||||
| Notes | String (1000) | General notes about the product |
|
||||
| Purchase From | String | Name of the place the item was purchased from |
|
||||
| Purchase Price | Float64 | |
|
||||
| Purchase At | Date | Date the item was purchased |
|
||||
| Lifetime Warranty | Boolean | true or false - case insensitive |
|
||||
| Warranty Expires | Date | Date in the format |
|
||||
| Warranty Details | String | Details about the warranty |
|
||||
| Sold To | String | Name of the person the item was sold to |
|
||||
| Sold At | Date | Date the item was sold |
|
||||
| Sold Price | Float64 | |
|
||||
| Sold Notes | String (1000) | |
|
||||
| Column | Type | Description |
|
||||
| ----------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ImportRef | String (100) | Import Refs are unique strings that can be used to deduplicate imports. Before an item is imported, we check the database for a matching ref. If the ref exists, we skip that item. |
|
||||
| Location | String | This is the location of the item that will be created. These are de-duplicated and won't create another instance when reused. |
|
||||
| Labels | `;` Separated String | List of labels to apply to the item separated by a `;`, can be existing or new |
|
||||
| Quantity | Integer | The quantity of items to create |
|
||||
| Name | String | Name of the item |
|
||||
| Description | String | Description of the item |
|
||||
| Insured | Boolean | Whether or not the item is insured |
|
||||
| Serial Number | String | Serial number of the item |
|
||||
| Model Number | String | Model of the item |
|
||||
| Manufacturer | String | Manufacturer of the item |
|
||||
| Notes | String (1000) | General notes about the product |
|
||||
| Purchase From | String | Name of the place the item was purchased from |
|
||||
| Purchase Price | Float64 | |
|
||||
| Purchase At | Date | Date the item was purchased |
|
||||
| Lifetime Warranty | Boolean | true or false - case insensitive |
|
||||
| Warranty Expires | Date | Date in the format |
|
||||
| Warranty Details | String | Details about the warranty |
|
||||
| Sold To | String | Name of the person the item was sold to |
|
||||
| Sold At | Date | Date the item was sold |
|
||||
| Sold Price | Float64 | |
|
||||
| Sold Notes | String (1000) | |
|
||||
|
||||
**Type Key**
|
||||
|
||||
|
||||
@@ -13,21 +13,22 @@ docker run --name=homebox \
|
||||
|
||||
## Docker-Compose
|
||||
|
||||
```yml
|
||||
```yaml
|
||||
version: "3.4"
|
||||
services:
|
||||
homebox:
|
||||
image: ghcr.io/hay-kot/homebox:latest
|
||||
container_name: homebox
|
||||
restart: always
|
||||
environment:
|
||||
- HBOX_LOG_LEVEL=info
|
||||
- HBOX_LOG_FORMAT=text
|
||||
- HBOX_WEB_MAX_UPLOAD_SIZE=10
|
||||
volumes:
|
||||
- homebox-data:/data/
|
||||
ports:
|
||||
- 3100:7745
|
||||
|
||||
services:
|
||||
homebox:
|
||||
image: ghcr.io/hay-kot/homebox:latest
|
||||
container_name: homebox
|
||||
restart: always
|
||||
environment:
|
||||
- HBOX_LOG_LEVEL=info
|
||||
- HBOX_LOG_FORMAT=text
|
||||
- HBOX_WEB_MAX_UPLOAD_SIZE=10
|
||||
volumes:
|
||||
- homebox-data:/data/
|
||||
ports:
|
||||
- 3100:7745
|
||||
|
||||
volumes:
|
||||
homebox-data:
|
||||
@@ -41,9 +42,10 @@ volumes:
|
||||
| HBOX_MODE | production | application mode used for runtime behavior can be one of: development, production |
|
||||
| HBOX_WEB_PORT | 7745 | port to run the web server on, in you're using docker do not change this |
|
||||
| HBOX_WEB_HOST | | host to run the web server on, in you're using docker do not change this |
|
||||
| HBOX_ALLOW_REGISTRATION | true | allow users to register themselves |
|
||||
| HBOX_WEB_MAX_UPLOAD_SIZE | 10 | maximum file upload size supported in MB |
|
||||
| HBOX_STORAGE_DATA | /data/ | path to the data directory, do not change this if you're using docker |
|
||||
| HBOX_STORAGE_SQLITE_URL | /data/homebox.db?_fk=1 | sqlite database url, in you're using docker do not change this |
|
||||
| HBOX_WEB_MAX_UPLOAD_SIZE | 10 | maximum file upload size supported in MB |
|
||||
| HBOX_LOG_LEVEL | info | log level to use, can be one of: trace, debug, info, warn, error, critical |
|
||||
| HBOX_LOG_FORMAT | text | log format to use, can be one of: text, json |
|
||||
| HBOX_MAILER_HOST | | email host to use, if not set no email provider will be used |
|
||||
@@ -76,6 +78,8 @@ volumes:
|
||||
--mailer-from/$HBOX_MAILER_FROM <string>
|
||||
--swagger-host/$HBOX_SWAGGER_HOST <string> (default: localhost:7745)
|
||||
--swagger-scheme/$HBOX_SWAGGER_SCHEME <string> (default: http)
|
||||
--demo/$HBOX_DEMO <bool>
|
||||
--allow-registration/$HBOX_ALLOW_REGISTRATION <bool> (default: true)
|
||||
--help/-h
|
||||
display this help message
|
||||
```
|
||||
16
docs/docs/tips-tricks.md
Normal file
16
docs/docs/tips-tricks.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Tips and Tricks
|
||||
|
||||
## Custom Fields
|
||||
|
||||
Custom fields are a great way to add any extra information to your item. The following types are supported:
|
||||
|
||||
- [x] Text
|
||||
- [ ] Integer (Future)
|
||||
- [ ] Boolean (Future)
|
||||
- [ ] Timestamp (Future)
|
||||
|
||||
Custom fields are appended to the main details section of your item.
|
||||
|
||||
!!! tip
|
||||
Homebox Custom Fields also have special support for URLs. Provide a URL (`https://google.com`) and it will be automatically converted to a clickable link in the UI. Optionally, you can also use markdown syntax to add a custom text to the button. `[Google](https://google.com)`
|
||||
|
||||
@@ -47,5 +47,6 @@ markdown_extensions:
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Quick Start: quick-start.md
|
||||
- Tips and Tricks: tips-tricks.md
|
||||
- Importing Data: import-csv.md
|
||||
- Building The Binary: build.md
|
||||
|
||||
4
fly.toml
4
fly.toml
@@ -5,9 +5,13 @@ kill_signal = "SIGINT"
|
||||
kill_timeout = 5
|
||||
processes = []
|
||||
|
||||
[build.args]
|
||||
COMMIT = "HEAD"
|
||||
VERSION = "nightly"
|
||||
|
||||
[env]
|
||||
PORT = "7745"
|
||||
HBOX_DEMO = "true"
|
||||
|
||||
[experimental]
|
||||
allowed_public_ports = []
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
name: "Home",
|
||||
href: "/home",
|
||||
},
|
||||
{
|
||||
name: "Items",
|
||||
href: "/items",
|
||||
},
|
||||
{
|
||||
name: "Logout",
|
||||
action: logout,
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
<template>
|
||||
<div ref="label" class="dropdown dropdown-end dropdown-top w-full">
|
||||
<FormTextField v-model="dateText" tabindex="0" label="Date" :inline="inline" readonly />
|
||||
<div tabindex="0" class="mt-1 card compact dropdown-content shadow bg-base-100 rounded-box w-64" @blur="resetTime">
|
||||
<div tabindex="0" class="card compact dropdown-content shadow bg-base-100 rounded-box w-64" @blur="resetTime">
|
||||
<div class="card-body">
|
||||
<div class="flex justify-between items-center">
|
||||
<button class="btn btn-xs" @click="prevMonth">
|
||||
<Icon class="h-5 w-5" name="mdi-arrow-left"></Icon>
|
||||
</button>
|
||||
<p class="text-center">{{ month }} {{ year }}</p>
|
||||
<button class="btn btn-xs" @click="nextMonth">
|
||||
<Icon class="h-5 w-5" name="mdi-arrow-right"></Icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-7 gap-2">
|
||||
<div v-for="d in daysIdx" :key="d">
|
||||
<p class="text-center">
|
||||
@@ -30,6 +21,15 @@
|
||||
<div v-else :key="`${day.number}-empty`"></div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex justify-between mt-1 items-center">
|
||||
<button class="btn btn-xs" @click="prevMonth">
|
||||
<Icon class="h-5 w-5" name="mdi-arrow-left"></Icon>
|
||||
</button>
|
||||
<p class="text-center">{{ month }} {{ year }}</p>
|
||||
<button class="btn btn-xs" @click="nextMonth">
|
||||
<Icon class="h-5 w-5" name="mdi-arrow-right"></Icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
</div>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="dropdown-content mb-1 menu shadow border border-gray-400 rounded bg-base-100 w-full z-[9999] max-h-60 overflow-y-scroll scroll-bar"
|
||||
style="display: inline"
|
||||
class="dropdown-content mb-1 menu shadow border border-gray-400 rounded bg-base-100 w-full z-[9999] max-h-60 overflow-y-scroll"
|
||||
>
|
||||
<li
|
||||
v-for="(obj, idx) in items"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
const emit = defineEmits(["update:modelValue", "update:value"]);
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
modelValue: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type: [Object, String, Boolean] as any,
|
||||
type: [Object, String] as any,
|
||||
default: null,
|
||||
},
|
||||
items: {
|
||||
@@ -37,59 +37,63 @@
|
||||
type: String,
|
||||
default: "name",
|
||||
},
|
||||
value: {
|
||||
valueKey: {
|
||||
type: String,
|
||||
default: null,
|
||||
required: false,
|
||||
},
|
||||
selectFirst: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
value: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
function syncSelect() {
|
||||
if (!props.modelValue) {
|
||||
if (props.selectFirst) {
|
||||
selectedIdx.value = 0;
|
||||
}
|
||||
return;
|
||||
const selectedIdx = ref(-1);
|
||||
|
||||
const internalSelected = useVModel(props, "modelValue", emit);
|
||||
const internalValue = useVModel(props, "value", emit);
|
||||
|
||||
watch(selectedIdx, newVal => {
|
||||
internalSelected.value = props.items[newVal];
|
||||
});
|
||||
|
||||
watch(selectedIdx, newVal => {
|
||||
if (props.valueKey) {
|
||||
internalValue.value = props.items[newVal][props.valueKey];
|
||||
}
|
||||
// Check if we're already synced
|
||||
if (props.value) {
|
||||
if (props.modelValue[props.value] === props.items[selectedIdx.value][props.value]) {
|
||||
return;
|
||||
}
|
||||
} else if (props.modelValue === props.items[selectedIdx.value]) {
|
||||
return;
|
||||
});
|
||||
|
||||
watch(
|
||||
internalSelected,
|
||||
() => {
|
||||
const idx = props.items.findIndex(item => compare(item, internalSelected.value));
|
||||
selectedIdx.value = idx;
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
internalValue,
|
||||
() => {
|
||||
const idx = props.items.findIndex(item => compare(item[props.valueKey], internalValue.value));
|
||||
selectedIdx.value = idx;
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function compare(a: any, b: any): boolean {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const idx = props.items.findIndex(item => {
|
||||
if (props.value) {
|
||||
return item[props.value] === props.modelValue;
|
||||
}
|
||||
return item === props.modelValue;
|
||||
});
|
||||
if (!a || !b) {
|
||||
return false;
|
||||
}
|
||||
|
||||
selectedIdx.value = idx;
|
||||
return JSON.stringify(a) === JSON.stringify(b);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
syncSelect();
|
||||
}
|
||||
);
|
||||
|
||||
const selectedIdx = ref(0);
|
||||
watch(
|
||||
() => selectedIdx.value,
|
||||
() => {
|
||||
if (props.value) {
|
||||
emit("update:modelValue", props.items[selectedIdx.value][props.value]);
|
||||
return;
|
||||
}
|
||||
emit("update:modelValue", props.items[selectedIdx.value]);
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
<label class="label">
|
||||
<span class="label-text">{{ label }}</span>
|
||||
</label>
|
||||
<input ref="input" v-model="value" :type="type" class="input input-bordered w-full" />
|
||||
<input ref="input" v-model="value" :placeholder="placeholder" :type="type" class="input input-bordered w-full" />
|
||||
</div>
|
||||
<div v-else class="sm:grid sm:grid-cols-4 sm:items-start sm:gap-4">
|
||||
<label class="label">
|
||||
<span class="label-text">{{ label }}</span>
|
||||
</label>
|
||||
<input v-model="value" class="input input-bordered col-span-3 w-full mt-2" />
|
||||
<input v-model="value" :placeholder="placeholder" class="input input-bordered col-span-3 w-full mt-2" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -35,6 +35,10 @@
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const input = ref<HTMLElement | null>(null);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<BaseModal v-model="modal">
|
||||
<template #title> Create Item </template>
|
||||
<form @submit.prevent="create">
|
||||
<FormSelect v-model="form.location" label="Location" :items="locations ?? []" select-first />
|
||||
<FormSelect v-model="form.location" label="Location" :items="locations ?? []" />
|
||||
<FormTextField
|
||||
ref="locationNameRef"
|
||||
v-model="form.name"
|
||||
|
||||
22
frontend/components/global/Currency.vue
Normal file
22
frontend/components/global/Currency.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
{{ value }}
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
amount: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const fmt = await useFormatCurrency();
|
||||
|
||||
const value = computed(() => {
|
||||
if (!props.amount || props.amount === "0") {
|
||||
return "";
|
||||
}
|
||||
|
||||
return fmt(props.amount);
|
||||
});
|
||||
</script>
|
||||
@@ -7,8 +7,15 @@
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-base-content sm:col-span-2 sm:mt-0">
|
||||
<slot :name="detail.slot || detail.name" v-bind="{ detail }">
|
||||
<template v-if="detail.type == 'date'">
|
||||
<DateTime :date="detail.text" />
|
||||
<DateTime v-if="detail.type == 'date'" :date="detail.text" />
|
||||
<Currency v-else-if="detail.type == 'currency'" :amount="detail.text" />
|
||||
<template v-else-if="detail.type === 'link'">
|
||||
<div class="tooltip tooltip-primary tooltip-right" :data-tip="detail.href">
|
||||
<a class="btn btn-primary btn-xs" :href="detail.href" target="_blank">
|
||||
<Icon name="mdi-open-in-new" class="mr-2 swap-on"></Icon>
|
||||
{{ detail.text }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ detail.text }}
|
||||
@@ -21,11 +28,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DateDetail, Detail } from "./types";
|
||||
import type { CustomDetail, Detail } from "./types";
|
||||
|
||||
defineProps({
|
||||
details: {
|
||||
type: Object as () => (Detail | DateDetail)[],
|
||||
type: Object as () => (Detail | CustomDetail)[],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
export type StringLike = string | number | boolean;
|
||||
|
||||
export type DateDetail = {
|
||||
type BaseDetail = {
|
||||
name: string;
|
||||
text: string | Date;
|
||||
slot?: string;
|
||||
type: "date";
|
||||
};
|
||||
|
||||
export type Detail = {
|
||||
name: string;
|
||||
type DateDetail = BaseDetail & {
|
||||
type: "date";
|
||||
text: Date | string;
|
||||
};
|
||||
|
||||
type CurrencyDetail = BaseDetail & {
|
||||
type: "currency";
|
||||
text: string;
|
||||
};
|
||||
|
||||
type LinkDetail = BaseDetail & {
|
||||
type: "link";
|
||||
text: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
export type CustomDetail = DateDetail | CurrencyDetail | LinkDetail;
|
||||
|
||||
export type Detail = BaseDetail & {
|
||||
text: StringLike;
|
||||
slot?: string;
|
||||
type?: "text";
|
||||
};
|
||||
|
||||
export type Details = Array<Detail | CustomDetail>;
|
||||
|
||||
21
frontend/composables/use-formatters.ts
Normal file
21
frontend/composables/use-formatters.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
const cache = {
|
||||
currency: "",
|
||||
};
|
||||
|
||||
export function ResetCurrency() {
|
||||
cache.currency = "";
|
||||
}
|
||||
|
||||
export async function useFormatCurrency() {
|
||||
if (!cache.currency) {
|
||||
const client = useUserApi();
|
||||
|
||||
const { data: group } = await client.group.get();
|
||||
|
||||
if (group) {
|
||||
cache.currency = group.currency;
|
||||
}
|
||||
}
|
||||
|
||||
return (value: number | string) => fmtCurrency(value, cache.currency);
|
||||
}
|
||||
32
frontend/composables/use-min-loader.ts
Normal file
32
frontend/composables/use-min-loader.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { WritableComputedRef } from "vue";
|
||||
|
||||
export function useMinLoader(ms = 500): WritableComputedRef<boolean> {
|
||||
const loading = ref(false);
|
||||
|
||||
const locked = ref(false);
|
||||
|
||||
const minLoading = computed({
|
||||
get: () => loading.value,
|
||||
set: value => {
|
||||
if (value) {
|
||||
loading.value = true;
|
||||
|
||||
if (!locked.value) {
|
||||
locked.value = true;
|
||||
setTimeout(() => {
|
||||
locked.value = false;
|
||||
}, ms);
|
||||
}
|
||||
}
|
||||
|
||||
if (!value && !locked.value) {
|
||||
loading.value = false;
|
||||
} else if (!value && locked.value) {
|
||||
setTimeout(() => {
|
||||
loading.value = false;
|
||||
}, ms);
|
||||
}
|
||||
},
|
||||
});
|
||||
return minLoading;
|
||||
}
|
||||
@@ -1,35 +1,5 @@
|
||||
import { Ref } from "vue";
|
||||
|
||||
export type DaisyTheme =
|
||||
| "light"
|
||||
| "dark"
|
||||
| "cupcake"
|
||||
| "bumblebee"
|
||||
| "emerald"
|
||||
| "corporate"
|
||||
| "synthwave"
|
||||
| "retro"
|
||||
| "cyberpunk"
|
||||
| "valentine"
|
||||
| "halloween"
|
||||
| "garden"
|
||||
| "forest"
|
||||
| "aqua"
|
||||
| "lofi"
|
||||
| "pastel"
|
||||
| "fantasy"
|
||||
| "wireframe"
|
||||
| "black"
|
||||
| "luxury"
|
||||
| "dracula"
|
||||
| "cmyk"
|
||||
| "autumn"
|
||||
| "business"
|
||||
| "acid"
|
||||
| "lemonade"
|
||||
| "night"
|
||||
| "coffee"
|
||||
| "winter";
|
||||
import { DaisyTheme } from "~~/lib/data/themes";
|
||||
|
||||
export type LocationViewPreferences = {
|
||||
showDetails: boolean;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ComputedRef } from "vue";
|
||||
import { DaisyTheme } from "./use-preferences";
|
||||
import { DaisyTheme } from "~~/lib/data/themes";
|
||||
|
||||
export interface UseTheme {
|
||||
theme: ComputedRef<DaisyTheme>;
|
||||
|
||||
32
frontend/composables/utils.test.ts
Normal file
32
frontend/composables/utils.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { maybeUrl } from "./utils";
|
||||
|
||||
describe("maybeURL works as expected", () => {
|
||||
test("basic valid URL case", () => {
|
||||
const result = maybeUrl("https://example.com");
|
||||
expect(result.isUrl).toBe(true);
|
||||
expect(result.url).toBe("https://example.com");
|
||||
expect(result.text).toBe("Link");
|
||||
});
|
||||
|
||||
test("special URL syntax", () => {
|
||||
const result = maybeUrl("[My Text](http://example.com)");
|
||||
expect(result.isUrl).toBe(true);
|
||||
expect(result.url).toBe("http://example.com");
|
||||
expect(result.text).toBe("My Text");
|
||||
});
|
||||
|
||||
test("not a url", () => {
|
||||
const result = maybeUrl("not a url");
|
||||
expect(result.isUrl).toBe(false);
|
||||
expect(result.url).toBe("");
|
||||
expect(result.text).toBe("");
|
||||
});
|
||||
|
||||
test("malformed special syntax", () => {
|
||||
const result = maybeUrl("[My Text(http://example.com)");
|
||||
expect(result.isUrl).toBe(false);
|
||||
expect(result.url).toBe("");
|
||||
expect(result.text).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -33,3 +33,35 @@ export function fmtCurrency(value: number | string, currency = "USD", locale = "
|
||||
});
|
||||
return formatter.format(value);
|
||||
}
|
||||
|
||||
export type MaybeUrlResult = {
|
||||
isUrl: boolean;
|
||||
url: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export function maybeUrl(str: string): MaybeUrlResult {
|
||||
const result: MaybeUrlResult = {
|
||||
isUrl: str.startsWith("http://") || str.startsWith("https://"),
|
||||
url: "",
|
||||
text: "",
|
||||
};
|
||||
|
||||
if (!result.isUrl && !str.startsWith("[")) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (str.startsWith("[")) {
|
||||
const match = str.match(/\[(.*)\]\((.*)\)/);
|
||||
if (match && match.length === 3) {
|
||||
result.isUrl = true;
|
||||
result.text = match[1];
|
||||
result.url = match[2];
|
||||
}
|
||||
} else {
|
||||
result.url = str;
|
||||
result.text = "Link";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,70 @@
|
||||
<script setup lang="ts"></script>
|
||||
<template>
|
||||
<div>
|
||||
<AppToast />
|
||||
<AppHeader />
|
||||
<main class="p-8 dark:bg-gray-800 dark:text-white bg-white text-gray-800 min-h-screen">
|
||||
<main>
|
||||
<slot></slot>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useItemStore } from "~~/stores/items";
|
||||
import { useLabelStore } from "~~/stores/labels";
|
||||
import { useLocationStore } from "~~/stores/locations";
|
||||
|
||||
/**
|
||||
* Store Provider Initialization
|
||||
*/
|
||||
|
||||
const labelStore = useLabelStore();
|
||||
const reLabel = /\/api\/v1\/labels\/.*/gm;
|
||||
const rmLabelStoreObserver = defineObserver("labelStore", {
|
||||
handler: r => {
|
||||
if (r.status === 201 || r.url.match(reLabel)) {
|
||||
labelStore.refresh();
|
||||
}
|
||||
console.debug("labelStore handler called by observer");
|
||||
},
|
||||
});
|
||||
|
||||
const locationStore = useLocationStore();
|
||||
const reLocation = /\/api\/v1\/locations\/.*/gm;
|
||||
const rmLocationStoreObserver = defineObserver("locationStore", {
|
||||
handler: r => {
|
||||
if (r.status === 201 || r.url.match(reLocation)) {
|
||||
locationStore.refresh();
|
||||
}
|
||||
console.debug("locationStore handler called by observer");
|
||||
},
|
||||
});
|
||||
|
||||
const itemStore = useItemStore();
|
||||
const reItem = /\/api\/v1\/items\/.*/gm;
|
||||
const rmItemStoreObserver = defineObserver("itemStore", {
|
||||
handler: r => {
|
||||
if (r.status === 201 || r.url.match(reItem)) {
|
||||
itemStore.refresh();
|
||||
}
|
||||
console.debug("itemStore handler called by observer");
|
||||
},
|
||||
});
|
||||
|
||||
const eventBus = useEventBus();
|
||||
eventBus.on(
|
||||
EventTypes.ClearStores,
|
||||
() => {
|
||||
labelStore.refresh();
|
||||
itemStore.refresh();
|
||||
locationStore.refresh();
|
||||
},
|
||||
"stores"
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
rmLabelStoreObserver();
|
||||
rmLocationStoreObserver();
|
||||
rmItemStoreObserver();
|
||||
eventBus.off(EventTypes.ClearStores, "stores");
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<AppToast />
|
||||
<AppHeader />
|
||||
<main>
|
||||
<slot></slot>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useItemStore } from "~~/stores/items";
|
||||
import { useLabelStore } from "~~/stores/labels";
|
||||
import { useLocationStore } from "~~/stores/locations";
|
||||
/**
|
||||
* Store Provider Initialization
|
||||
*/
|
||||
|
||||
const labelStore = useLabelStore();
|
||||
const reLabel = /\/api\/v1\/labels\/.*/gm;
|
||||
const rmLabelStoreObserver = defineObserver("labelStore", {
|
||||
handler: r => {
|
||||
if (r.status === 201 || r.url.match(reLabel)) {
|
||||
labelStore.refresh();
|
||||
}
|
||||
console.debug("labelStore handler called by observer");
|
||||
},
|
||||
});
|
||||
|
||||
const locationStore = useLocationStore();
|
||||
const reLocation = /\/api\/v1\/locations\/.*/gm;
|
||||
const rmLocationStoreObserver = defineObserver("locationStore", {
|
||||
handler: r => {
|
||||
if (r.status === 201 || r.url.match(reLocation)) {
|
||||
locationStore.refresh();
|
||||
}
|
||||
console.debug("locationStore handler called by observer");
|
||||
},
|
||||
});
|
||||
|
||||
const itemStore = useItemStore();
|
||||
const reItem = /\/api\/v1\/items\/.*/gm;
|
||||
const rmItemStoreObserver = defineObserver("itemStore", {
|
||||
handler: r => {
|
||||
if (r.status === 201 || r.url.match(reItem)) {
|
||||
itemStore.refresh();
|
||||
}
|
||||
console.debug("itemStore handler called by observer");
|
||||
},
|
||||
});
|
||||
|
||||
const eventBus = useEventBus();
|
||||
eventBus.on(
|
||||
EventTypes.ClearStores,
|
||||
() => {
|
||||
labelStore.refresh();
|
||||
itemStore.refresh();
|
||||
locationStore.refresh();
|
||||
},
|
||||
"stores"
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
rmLabelStoreObserver();
|
||||
rmLocationStoreObserver();
|
||||
rmItemStoreObserver();
|
||||
eventBus.off(EventTypes.ClearStores, "stores");
|
||||
});
|
||||
</script>
|
||||
@@ -2,11 +2,23 @@ import { faker } from "@faker-js/faker";
|
||||
import { expect } from "vitest";
|
||||
import { overrideParts } from "../../base/urls";
|
||||
import { PublicApi } from "../../public";
|
||||
import { LabelCreate, LocationCreate, UserRegistration } from "../../types/data-contracts";
|
||||
import { ItemField, LabelCreate, LocationCreate, UserRegistration } from "../../types/data-contracts";
|
||||
import * as config from "../../../../test/config";
|
||||
import { UserClient } from "../../user";
|
||||
import { Requests } from "../../../requests";
|
||||
|
||||
function itemField(id = null): ItemField {
|
||||
return {
|
||||
id,
|
||||
name: faker.lorem.word(),
|
||||
type: "text",
|
||||
textValue: faker.lorem.sentence(),
|
||||
booleanValue: false,
|
||||
numberValue: faker.datatype.number(),
|
||||
timeValue: null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a random user registration object that can be
|
||||
* used to signup a new user.
|
||||
@@ -72,6 +84,7 @@ export const factories = {
|
||||
user,
|
||||
location,
|
||||
label,
|
||||
itemField,
|
||||
client: {
|
||||
public: publicClient,
|
||||
user: userClient,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { factories } from "./factories";
|
||||
import { sharedUserClient } from "./test-utils";
|
||||
|
||||
describe("[GET] /api/v1/status", () => {
|
||||
test("server should respond", async () => {
|
||||
@@ -32,43 +31,4 @@ describe("first time user workflow (register, login, join group)", () => {
|
||||
expect(response.status).toBe(204);
|
||||
}
|
||||
});
|
||||
|
||||
test("user should be able to join create join token and have user signup", async () => {
|
||||
// Setup User 1 Token
|
||||
|
||||
const client = await sharedUserClient();
|
||||
const { data: user1 } = await client.user.self();
|
||||
|
||||
const { response, data } = await client.group.createInvitation({
|
||||
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
|
||||
uses: 1,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(data.token).toBeTruthy();
|
||||
|
||||
// Create User 2 with token
|
||||
|
||||
const duplicateUser = factories.user();
|
||||
duplicateUser.token = data.token;
|
||||
|
||||
const { response: registerResp } = await api.register(duplicateUser);
|
||||
expect(registerResp.status).toBe(204);
|
||||
|
||||
const { response: loginResp, data: loginData } = await api.login(duplicateUser.email, duplicateUser.password);
|
||||
expect(loginResp.status).toBe(200);
|
||||
|
||||
// Get Self and Assert
|
||||
|
||||
const client2 = factories.client.user(loginData.token);
|
||||
|
||||
const { data: user2 } = await client2.user.self();
|
||||
|
||||
user2.item.groupName = user1.item.groupName;
|
||||
|
||||
// Cleanup User 2
|
||||
|
||||
const { response: deleteResp } = await client2.user.delete();
|
||||
expect(deleteResp.status).toBe(204);
|
||||
});
|
||||
});
|
||||
|
||||
66
frontend/lib/api/__test__/user/group.test.ts
Normal file
66
frontend/lib/api/__test__/user/group.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { factories } from "../factories";
|
||||
import { sharedUserClient } from "../test-utils";
|
||||
|
||||
describe("first time user workflow (register, login, join group)", () => {
|
||||
test("user should be able to update group", async () => {
|
||||
const { client } = await factories.client.singleUse();
|
||||
|
||||
const name = faker.name.firstName();
|
||||
|
||||
const { response, data: group } = await client.group.update({
|
||||
name,
|
||||
currency: "eur",
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(group.name).toBe(name);
|
||||
});
|
||||
|
||||
test("user should be able to get own group", async () => {
|
||||
const { client } = await factories.client.singleUse();
|
||||
|
||||
const { response, data: group } = await client.group.get();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(group.name).toBeTruthy();
|
||||
expect(group.currency).toBe("USD");
|
||||
});
|
||||
|
||||
test("user should be able to join create join token and have user signup", async () => {
|
||||
const api = factories.client.public();
|
||||
|
||||
// Setup User 1 Token
|
||||
const client = await sharedUserClient();
|
||||
const { data: user1 } = await client.user.self();
|
||||
|
||||
const { response, data } = await client.group.createInvitation({
|
||||
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
|
||||
uses: 1,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(data.token).toBeTruthy();
|
||||
|
||||
// Create User 2 with token
|
||||
const duplicateUser = factories.user();
|
||||
duplicateUser.token = data.token;
|
||||
|
||||
const { response: registerResp } = await api.register(duplicateUser);
|
||||
expect(registerResp.status).toBe(204);
|
||||
|
||||
const { response: loginResp, data: loginData } = await api.login(duplicateUser.email, duplicateUser.password);
|
||||
expect(loginResp.status).toBe(200);
|
||||
|
||||
// Get Self and Assert
|
||||
const client2 = factories.client.user(loginData.token);
|
||||
const { data: user2 } = await client2.user.self();
|
||||
|
||||
user2.item.groupName = user1.item.groupName;
|
||||
|
||||
// Cleanup User 2
|
||||
const { response: deleteResp } = await client2.user.delete();
|
||||
expect(deleteResp.status).toBe(204);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,9 @@
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { LocationOut } from "../../types/data-contracts";
|
||||
import { ItemField, LocationOut } from "../../types/data-contracts";
|
||||
import { AttachmentTypes } from "../../types/non-generated";
|
||||
import { UserClient } from "../../user";
|
||||
import { factories } from "../factories";
|
||||
import { sharedUserClient } from "../test-utils";
|
||||
|
||||
describe("user should be able to create an item and add an attachment", () => {
|
||||
@@ -58,4 +60,57 @@ describe("user should be able to create an item and add an attachment", () => {
|
||||
api.items.delete(item.id);
|
||||
await cleanup();
|
||||
});
|
||||
|
||||
test("user should be able to create and delete fields on an item", async () => {
|
||||
const api = await sharedUserClient();
|
||||
const [location, cleanup] = await useLocation(api);
|
||||
|
||||
const { response, data: item } = await api.items.create({
|
||||
name: faker.vehicle.model(),
|
||||
labelIds: [],
|
||||
description: faker.lorem.paragraph(1),
|
||||
locationId: location.id,
|
||||
});
|
||||
expect(response.status).toBe(201);
|
||||
|
||||
const fields: ItemField[] = [
|
||||
factories.itemField(),
|
||||
factories.itemField(),
|
||||
factories.itemField(),
|
||||
factories.itemField(),
|
||||
];
|
||||
|
||||
// Add fields
|
||||
const itemUpdate = {
|
||||
...item,
|
||||
locationId: item.location.id,
|
||||
labelIds: item.labels.map(l => l.id),
|
||||
fields,
|
||||
};
|
||||
|
||||
const { response: updateResponse, data: item2 } = await api.items.update(item.id, itemUpdate);
|
||||
expect(updateResponse.status).toBe(200);
|
||||
|
||||
expect(item2.fields).toHaveLength(fields.length);
|
||||
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
expect(item2.fields[i].name).toBe(fields[i].name);
|
||||
expect(item2.fields[i].textValue).toBe(fields[i].textValue);
|
||||
expect(item2.fields[i].numberValue).toBe(fields[i].numberValue);
|
||||
}
|
||||
|
||||
itemUpdate.fields = [fields[0], fields[1]];
|
||||
|
||||
const { response: updateResponse2, data: item3 } = await api.items.update(item.id, itemUpdate);
|
||||
expect(updateResponse2.status).toBe(200);
|
||||
|
||||
expect(item3.fields).toHaveLength(2);
|
||||
for (let i = 0; i < item3.fields.length; i++) {
|
||||
expect(item3.fields[i].name).toBe(itemUpdate.fields[i].name);
|
||||
expect(item3.fields[i].textValue).toBe(itemUpdate.fields[i].textValue);
|
||||
expect(item3.fields[i].numberValue).toBe(itemUpdate.fields[i].numberValue);
|
||||
}
|
||||
|
||||
cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BaseAPI, route } from "../base";
|
||||
import { GroupInvitation, GroupInvitationCreate } from "../types/data-contracts";
|
||||
import { Group, GroupInvitation, GroupInvitationCreate, GroupUpdate } from "../types/data-contracts";
|
||||
|
||||
export class GroupApi extends BaseAPI {
|
||||
createInvitation(data: GroupInvitationCreate) {
|
||||
@@ -8,4 +8,17 @@ export class GroupApi extends BaseAPI {
|
||||
body: data,
|
||||
});
|
||||
}
|
||||
|
||||
update(data: GroupUpdate) {
|
||||
return this.http.put<GroupUpdate, Group>({
|
||||
url: route("/groups"),
|
||||
body: data,
|
||||
});
|
||||
}
|
||||
|
||||
get() {
|
||||
return this.http.get<Group>({
|
||||
url: route("/groups"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,19 @@ import {
|
||||
ItemSummary,
|
||||
ItemUpdate,
|
||||
} from "../types/data-contracts";
|
||||
import { AttachmentTypes } from "../types/non-generated";
|
||||
import { Results } from "./types";
|
||||
import { AttachmentTypes, PaginationResult } from "../types/non-generated";
|
||||
|
||||
export type ItemsQuery = {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
locations?: string[];
|
||||
labels?: string[];
|
||||
q?: string;
|
||||
};
|
||||
|
||||
export class ItemsApi extends BaseAPI {
|
||||
getAll() {
|
||||
return this.http.get<Results<ItemSummary>>({ url: route("/items") });
|
||||
getAll(q: ItemsQuery = {}) {
|
||||
return this.http.get<PaginationResult<ItemSummary>>({ url: route("/items", q) });
|
||||
}
|
||||
|
||||
create(item: ItemCreate) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BaseAPI, route } from "../base";
|
||||
import { LabelCreate, LabelOut } from "../types/data-contracts";
|
||||
import { Results } from "./types";
|
||||
import { Results } from "../types/non-generated";
|
||||
|
||||
export class LabelsApi extends BaseAPI {
|
||||
getAll() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BaseAPI, route } from "../base";
|
||||
import { LocationOutCount, LocationCreate, LocationOut } from "../types/data-contracts";
|
||||
import { Results } from "./types";
|
||||
import { Results } from "../types/non-generated";
|
||||
|
||||
export type LocationUpdate = LocationCreate;
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export type Results<T> = {
|
||||
items: T[];
|
||||
};
|
||||
@@ -16,6 +16,19 @@ export interface DocumentOut {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
createdAt: Date;
|
||||
currency: string;
|
||||
id: string;
|
||||
name: string;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface GroupUpdate {
|
||||
currency: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ItemAttachment {
|
||||
createdAt: Date;
|
||||
document: DocumentOut;
|
||||
@@ -38,10 +51,23 @@ export interface ItemCreate {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ItemField {
|
||||
booleanValue: boolean;
|
||||
id: string;
|
||||
name: string;
|
||||
numberValue: number;
|
||||
textValue: string;
|
||||
timeValue: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface ItemOut {
|
||||
attachments: ItemAttachment[];
|
||||
createdAt: Date;
|
||||
description: string;
|
||||
|
||||
/** Future */
|
||||
fields: ItemField[];
|
||||
id: string;
|
||||
insured: boolean;
|
||||
labels: LabelSummary[];
|
||||
@@ -95,6 +121,7 @@ export interface ItemSummary {
|
||||
|
||||
export interface ItemUpdate {
|
||||
description: string;
|
||||
fields: ItemField[];
|
||||
id: string;
|
||||
insured: boolean;
|
||||
labelIds: string[];
|
||||
@@ -187,6 +214,13 @@ export interface LocationSummary {
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface PaginationResultRepoItemSummary {
|
||||
items: ItemSummary[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface UserOut {
|
||||
email: string;
|
||||
groupId: string;
|
||||
@@ -227,6 +261,7 @@ export interface UserRegistration {
|
||||
|
||||
export interface ApiSummary {
|
||||
build: Build;
|
||||
demo: boolean;
|
||||
health: boolean;
|
||||
message: string;
|
||||
title: string;
|
||||
|
||||
@@ -3,8 +3,20 @@ export enum AttachmentTypes {
|
||||
Manual = "manual",
|
||||
Warranty = "warranty",
|
||||
Attachment = "attachment",
|
||||
Receipt = "receipt",
|
||||
}
|
||||
|
||||
export type Result<T> = {
|
||||
item: T;
|
||||
};
|
||||
|
||||
export type Results<T> = {
|
||||
items: T[];
|
||||
};
|
||||
|
||||
export interface PaginationResult<T> {
|
||||
items: T[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
35
frontend/lib/data/currency.ts
Normal file
35
frontend/lib/data/currency.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export type Codes = "USD" | "EUR" | "GBP" | "JPY";
|
||||
|
||||
export type Currency = {
|
||||
code: Codes;
|
||||
local: string;
|
||||
symbol: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const currencies: Currency[] = [
|
||||
{
|
||||
code: "USD",
|
||||
local: "en-US",
|
||||
symbol: "$",
|
||||
name: "US Dollar",
|
||||
},
|
||||
{
|
||||
code: "EUR",
|
||||
local: "de-DE",
|
||||
symbol: "€",
|
||||
name: "Euro",
|
||||
},
|
||||
{
|
||||
code: "GBP",
|
||||
local: "en-GB",
|
||||
symbol: "£",
|
||||
name: "British Pound",
|
||||
},
|
||||
{
|
||||
code: "JPY",
|
||||
local: "ja-JP",
|
||||
symbol: "¥",
|
||||
name: "Japanese Yen",
|
||||
},
|
||||
];
|
||||
150
frontend/lib/data/themes.ts
Normal file
150
frontend/lib/data/themes.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
export type DaisyTheme =
|
||||
| "light"
|
||||
| "dark"
|
||||
| "cupcake"
|
||||
| "bumblebee"
|
||||
| "emerald"
|
||||
| "corporate"
|
||||
| "synthwave"
|
||||
| "retro"
|
||||
| "cyberpunk"
|
||||
| "valentine"
|
||||
| "halloween"
|
||||
| "garden"
|
||||
| "forest"
|
||||
| "aqua"
|
||||
| "lofi"
|
||||
| "pastel"
|
||||
| "fantasy"
|
||||
| "wireframe"
|
||||
| "black"
|
||||
| "luxury"
|
||||
| "dracula"
|
||||
| "cmyk"
|
||||
| "autumn"
|
||||
| "business"
|
||||
| "acid"
|
||||
| "lemonade"
|
||||
| "night"
|
||||
| "coffee"
|
||||
| "winter";
|
||||
|
||||
export type ThemeOption = {
|
||||
label: string;
|
||||
value: DaisyTheme;
|
||||
};
|
||||
|
||||
export const themes: ThemeOption[] = [
|
||||
{
|
||||
label: "Garden",
|
||||
value: "garden",
|
||||
},
|
||||
{
|
||||
label: "Light",
|
||||
value: "light",
|
||||
},
|
||||
{
|
||||
label: "Cupcake",
|
||||
value: "cupcake",
|
||||
},
|
||||
{
|
||||
label: "Bumblebee",
|
||||
value: "bumblebee",
|
||||
},
|
||||
{
|
||||
label: "Emerald",
|
||||
value: "emerald",
|
||||
},
|
||||
{
|
||||
label: "Corporate",
|
||||
value: "corporate",
|
||||
},
|
||||
{
|
||||
label: "Synthwave",
|
||||
value: "synthwave",
|
||||
},
|
||||
{
|
||||
label: "Retro",
|
||||
value: "retro",
|
||||
},
|
||||
{
|
||||
label: "Cyberpunk",
|
||||
value: "cyberpunk",
|
||||
},
|
||||
{
|
||||
label: "Valentine",
|
||||
value: "valentine",
|
||||
},
|
||||
{
|
||||
label: "Halloween",
|
||||
value: "halloween",
|
||||
},
|
||||
{
|
||||
label: "Forest",
|
||||
value: "forest",
|
||||
},
|
||||
{
|
||||
label: "Aqua",
|
||||
value: "aqua",
|
||||
},
|
||||
{
|
||||
label: "Lofi",
|
||||
value: "lofi",
|
||||
},
|
||||
{
|
||||
label: "Pastel",
|
||||
value: "pastel",
|
||||
},
|
||||
{
|
||||
label: "Fantasy",
|
||||
value: "fantasy",
|
||||
},
|
||||
{
|
||||
label: "Wireframe",
|
||||
value: "wireframe",
|
||||
},
|
||||
{
|
||||
label: "Black",
|
||||
value: "black",
|
||||
},
|
||||
{
|
||||
label: "Luxury",
|
||||
value: "luxury",
|
||||
},
|
||||
{
|
||||
label: "Dracula",
|
||||
value: "dracula",
|
||||
},
|
||||
{
|
||||
label: "Cmyk",
|
||||
value: "cmyk",
|
||||
},
|
||||
{
|
||||
label: "Autumn",
|
||||
value: "autumn",
|
||||
},
|
||||
{
|
||||
label: "Business",
|
||||
value: "business",
|
||||
},
|
||||
{
|
||||
label: "Acid",
|
||||
value: "acid",
|
||||
},
|
||||
{
|
||||
label: "Lemonade",
|
||||
value: "lemonade",
|
||||
},
|
||||
{
|
||||
label: "Night",
|
||||
value: "night",
|
||||
},
|
||||
{
|
||||
label: "Coffee",
|
||||
value: "coffee",
|
||||
},
|
||||
{
|
||||
label: "Winter",
|
||||
value: "winter",
|
||||
},
|
||||
];
|
||||
15
frontend/middleware/auth.ts
Normal file
15
frontend/middleware/auth.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useAuthStore } from "~~/stores/auth";
|
||||
|
||||
export default defineNuxtRouteMiddleware(async () => {
|
||||
const auth = useAuthStore();
|
||||
const api = useUserApi();
|
||||
|
||||
if (!auth.self) {
|
||||
const { data, error } = await api.user.self();
|
||||
if (error) {
|
||||
navigateTo("/");
|
||||
}
|
||||
|
||||
auth.$patch({ self: data.item });
|
||||
}
|
||||
});
|
||||
@@ -5,8 +5,9 @@
|
||||
import { useLocationStore } from "~~/stores/locations";
|
||||
|
||||
definePageMeta({
|
||||
layout: "home",
|
||||
middleware: ["auth"],
|
||||
});
|
||||
|
||||
useHead({
|
||||
title: "Homebox | Home",
|
||||
});
|
||||
@@ -15,15 +16,6 @@
|
||||
|
||||
const auth = useAuthStore();
|
||||
|
||||
if (auth.self === null) {
|
||||
const { data, error } = await api.user.self();
|
||||
if (error) {
|
||||
navigateTo("/");
|
||||
}
|
||||
|
||||
auth.$patch({ self: data.item });
|
||||
}
|
||||
|
||||
const itemsStore = useItemStore();
|
||||
const items = computed(() => itemsStore.items);
|
||||
|
||||
@@ -157,13 +149,6 @@
|
||||
</BaseCard>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<BaseSectionHeader class="mb-5"> Labels </BaseSectionHeader>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<LabelChip v-for="label in labels" :key="label.id" size="lg" :label="label" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<BaseSectionHeader class="mb-5"> Storage Locations </BaseSectionHeader>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 card md:grid-cols-3 gap-4">
|
||||
@@ -172,9 +157,9 @@
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<BaseSectionHeader class="mb-5"> Items </BaseSectionHeader>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<ItemCard v-for="item in items" :key="item.id" :item="item" />
|
||||
<BaseSectionHeader class="mb-5"> Labels </BaseSectionHeader>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<LabelChip v-for="label in labels" :key="label.id" size="lg" :label="label" />
|
||||
</div>
|
||||
</section>
|
||||
</BaseContainer>
|
||||
|
||||
@@ -11,6 +11,23 @@
|
||||
const api = usePublicApi();
|
||||
const toast = useNotifier();
|
||||
|
||||
const { data: status } = useAsyncData(async () => {
|
||||
const { data } = await api.status();
|
||||
|
||||
if (data) {
|
||||
username.value = "demo@example.com";
|
||||
password.value = "demo";
|
||||
}
|
||||
return data;
|
||||
});
|
||||
|
||||
whenever(status, status => {
|
||||
if (status?.demo) {
|
||||
email.value = "demo@example.com";
|
||||
loginPassword.value = "demo";
|
||||
}
|
||||
});
|
||||
|
||||
const authStore = useAuthStore();
|
||||
if (!authStore.isTokenExpired) {
|
||||
navigateTo("/home");
|
||||
@@ -130,10 +147,10 @@
|
||||
<a href="https://twitter.com/haybytes" class="tooltip" data-tip="Follow The Developer" target="_blank">
|
||||
<Icon name="mdi-twitter" class="h-8 w-8" />
|
||||
</a>
|
||||
<a href="https://discord.gg/tuncmNrE4z" class="tooltip" data-tip="Join The Discord">
|
||||
<a href="https://discord.gg/tuncmNrE4z" class="tooltip" data-tip="Join The Discord" target="_blank">
|
||||
<Icon name="mdi-discord" class="h-8 w-8" />
|
||||
</a>
|
||||
<a href="https://hay-kot.github.io/homebox/" class="tooltip" data-tip="Read The Docs">
|
||||
<a href="https://hay-kot.github.io/homebox/" class="tooltip" data-tip="Read The Docs" target="_blank">
|
||||
<Icon name="mdi-folder" class="h-8 w-8" />
|
||||
</a>
|
||||
</div>
|
||||
@@ -178,6 +195,11 @@
|
||||
<Icon name="heroicons-user" class="mr-1 w-7 h-7" />
|
||||
Login
|
||||
</h2>
|
||||
<template v-if="status && status.demo">
|
||||
<p class="text-xs italic text-center">This is a demo instance</p>
|
||||
<p class="text-xs text-center"><b>Email</b> demo@example.com</p>
|
||||
<p class="text-xs text-center"><b>Password</b> demo</p>
|
||||
</template>
|
||||
<FormTextField v-model="email" label="Email" />
|
||||
<FormTextField v-model="loginPassword" label="Password" type="password" />
|
||||
<div class="card-actions justify-end mt-2">
|
||||
@@ -200,6 +222,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer v-if="status" class="absolute text-center w-full bottom-0 pb-4">
|
||||
<p class="text-center text-sm">Version: {{ status.build.version }} ~ Build: {{ status.build.commit }}</p>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { capitalize } from "~~/lib/strings";
|
||||
|
||||
definePageMeta({
|
||||
layout: "home",
|
||||
middleware: ["auth"],
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
@@ -30,6 +30,13 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (locations) {
|
||||
const location = locations.value.find(l => l.id === data.location.id);
|
||||
if (location) {
|
||||
data.location = location;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
onMounted(() => {
|
||||
@@ -111,12 +118,12 @@
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
label: "Purchased Price",
|
||||
label: "Purchase Price",
|
||||
ref: "purchasePrice",
|
||||
},
|
||||
{
|
||||
type: "date",
|
||||
label: "Purchased At",
|
||||
label: "Purchase Date",
|
||||
ref: "purchaseTime",
|
||||
},
|
||||
];
|
||||
@@ -183,6 +190,7 @@
|
||||
const dropAttachment = (files: File[] | null) => uploadAttachment(files, AttachmentTypes.Attachment);
|
||||
const dropWarranty = (files: File[] | null) => uploadAttachment(files, AttachmentTypes.Warranty);
|
||||
const dropManual = (files: File[] | null) => uploadAttachment(files, AttachmentTypes.Manual);
|
||||
const dropReceipt = (files: File[] | null) => uploadAttachment(files, AttachmentTypes.Receipt);
|
||||
|
||||
async function uploadAttachment(files: File[] | null, type: AttachmentTypes) {
|
||||
if (!files && files.length === 0) {
|
||||
@@ -226,6 +234,7 @@
|
||||
loading: false,
|
||||
|
||||
// Values
|
||||
obj: {},
|
||||
id: "",
|
||||
title: "",
|
||||
type: "",
|
||||
@@ -241,11 +250,13 @@
|
||||
editState.title = attachment.document.title;
|
||||
editState.type = attachment.type;
|
||||
editState.modal = true;
|
||||
|
||||
editState.obj = attachmentOpts.find(o => o.value === attachment.type);
|
||||
}
|
||||
|
||||
async function updateAttachment() {
|
||||
editState.loading = true;
|
||||
|
||||
console.log(editState.type);
|
||||
const { error, data } = await api.items.updateAttachment(itemId.value, editState.id, {
|
||||
title: editState.title,
|
||||
type: editState.type,
|
||||
@@ -267,6 +278,34 @@
|
||||
|
||||
toast.success("Attachment updated");
|
||||
}
|
||||
|
||||
// Custom Fields
|
||||
// const fieldTypes = [
|
||||
// {
|
||||
// name: "Text",
|
||||
// value: "text",
|
||||
// },
|
||||
// {
|
||||
// name: "Number",
|
||||
// value: "number",
|
||||
// },
|
||||
// {
|
||||
// name: "Boolean",
|
||||
// value: "boolean",
|
||||
// },
|
||||
// ];
|
||||
|
||||
function addField() {
|
||||
item.value.fields.push({
|
||||
id: null,
|
||||
name: "Field Name",
|
||||
type: "text",
|
||||
textValue: "",
|
||||
numberValue: 0,
|
||||
booleanValue: false,
|
||||
timeValue: null,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -275,7 +314,14 @@
|
||||
<template #title> Attachment Edit </template>
|
||||
|
||||
<FormTextField v-model="editState.title" label="Attachment Title" />
|
||||
<FormSelect v-model="editState.type" label="Attachment Type" value="value" name="text" :items="attachmentOpts" />
|
||||
<FormSelect
|
||||
v-model="editState.obj"
|
||||
v-model:value="editState.type"
|
||||
label="Attachment Type"
|
||||
value-key="value"
|
||||
name="text"
|
||||
:items="attachmentOpts"
|
||||
/>
|
||||
<div class="modal-action">
|
||||
<BaseButton :loading="editState.loading" @click="updateAttachment"> Update </BaseButton>
|
||||
</div>
|
||||
@@ -308,7 +354,7 @@
|
||||
</template>
|
||||
</BaseSectionHeader>
|
||||
<div class="px-5 mb-6 grid md:grid-cols-2 gap-4">
|
||||
<FormSelect v-model="item.location" label="Location" :items="locations ?? []" select-first />
|
||||
<FormSelect v-if="item" v-model="item.location" label="Location" :items="locations ?? []" />
|
||||
<FormMultiselect v-model="item.labels" label="Labels" :items="labels ?? []" />
|
||||
</div>
|
||||
|
||||
@@ -346,6 +392,31 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BaseCard>
|
||||
<template #title> Custom Fields </template>
|
||||
<div class="px-5 divide-y divide-gray-300 space-y-4">
|
||||
<div
|
||||
v-for="(field, idx) in item.fields"
|
||||
:key="`field-${idx}`"
|
||||
class="grid grid-cols-2 md:grid-cols-4 gap-2"
|
||||
>
|
||||
<!-- <FormSelect v-model:value="field.type" label="Field Type" :items="fieldTypes" value-key="value" /> -->
|
||||
<FormTextField v-model="field.name" label="Name" />
|
||||
<div class="flex items-end col-span-3">
|
||||
<FormTextField v-model="field.textValue" label="Value" />
|
||||
<div class="tooltip" data-tip="Delete">
|
||||
<button class="btn btn-sm btn-square mb-2 ml-2" @click="item.fields.splice(idx, 1)">
|
||||
<Icon name="mdi-delete" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-5 pb-4 mt-4 flex justify-end">
|
||||
<BaseButton size="sm" @click="addField"> Add </BaseButton>
|
||||
</div>
|
||||
</BaseCard>
|
||||
|
||||
<div
|
||||
v-if="!preferences.editorSimpleView"
|
||||
ref="attDropZone"
|
||||
@@ -361,6 +432,7 @@
|
||||
<DropZone @drop="dropWarranty"> Warranty </DropZone>
|
||||
<DropZone @drop="dropManual"> Manual </DropZone>
|
||||
<DropZone @drop="dropAttachment"> Attachment </DropZone>
|
||||
<DropZone @drop="dropReceipt"> Receipt </DropZone>
|
||||
</div>
|
||||
<button
|
||||
v-else
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { DateDetail, Detail } from "~~/components/global/DetailsSection/types";
|
||||
import { Detail, Details } from "~~/components/global/DetailsSection/types";
|
||||
import { ItemAttachment } from "~~/lib/api/types/data-contracts";
|
||||
|
||||
definePageMeta({
|
||||
layout: "home",
|
||||
middleware: ["auth"],
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
@@ -31,6 +31,7 @@
|
||||
attachments: ItemAttachment[];
|
||||
warranty: ItemAttachment[];
|
||||
manuals: ItemAttachment[];
|
||||
receipts: ItemAttachment[];
|
||||
};
|
||||
|
||||
const attachments = computed<FilteredAttachments>(() => {
|
||||
@@ -40,6 +41,7 @@
|
||||
attachments: [],
|
||||
manuals: [],
|
||||
warranty: [],
|
||||
receipts: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -51,6 +53,8 @@
|
||||
acc.warranty.push(attachment);
|
||||
} else if (attachment.type === "manual") {
|
||||
acc.manuals.push(attachment);
|
||||
} else if (attachment.type === "receipt") {
|
||||
acc.receipts.push(attachment);
|
||||
} else {
|
||||
acc.attachments.push(attachment);
|
||||
}
|
||||
@@ -61,6 +65,7 @@
|
||||
attachments: [] as ItemAttachment[],
|
||||
warranty: [] as ItemAttachment[],
|
||||
manuals: [] as ItemAttachment[],
|
||||
receipts: [] as ItemAttachment[],
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -76,7 +81,7 @@
|
||||
text: item.value?.serialNumber,
|
||||
},
|
||||
{
|
||||
name: "Mode Number",
|
||||
name: "Model Number",
|
||||
text: item.value?.modelNumber,
|
||||
},
|
||||
{
|
||||
@@ -91,6 +96,25 @@
|
||||
name: "Notes",
|
||||
text: item.value?.notes,
|
||||
},
|
||||
...item.value.fields.map(field => {
|
||||
/**
|
||||
* Support Special URL Syntax
|
||||
*/
|
||||
const url = maybeUrl(field.textValue);
|
||||
if (url.isUrl) {
|
||||
return {
|
||||
name: field.name,
|
||||
text: url.text,
|
||||
type: "link",
|
||||
href: url.url,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: field.name,
|
||||
text: field.textValue,
|
||||
};
|
||||
}),
|
||||
];
|
||||
});
|
||||
|
||||
@@ -103,7 +127,8 @@
|
||||
attachments.value.photos.length > 0 ||
|
||||
attachments.value.attachments.length > 0 ||
|
||||
attachments.value.warranty.length > 0 ||
|
||||
attachments.value.manuals.length > 0
|
||||
attachments.value.manuals.length > 0 ||
|
||||
attachments.value.receipts.length > 0
|
||||
);
|
||||
});
|
||||
|
||||
@@ -134,6 +159,10 @@
|
||||
push("Manuals");
|
||||
}
|
||||
|
||||
if (attachments.value.receipts.length > 0) {
|
||||
push("Receipts");
|
||||
}
|
||||
|
||||
return details;
|
||||
});
|
||||
|
||||
@@ -145,7 +174,7 @@
|
||||
});
|
||||
|
||||
const warrantyDetails = computed(() => {
|
||||
const details: (Detail | DateDetail)[] = [
|
||||
const details: Details = [
|
||||
{
|
||||
name: "Lifetime Warranty",
|
||||
text: item.value?.lifetimeWarranty ? "Yes" : "No",
|
||||
@@ -180,15 +209,16 @@
|
||||
return item.value?.purchaseFrom || item.value?.purchasePrice !== "0";
|
||||
});
|
||||
|
||||
const purchaseDetails = computed<Array<Detail | DateDetail>>(() => {
|
||||
const purchaseDetails = computed<Details>(() => {
|
||||
return [
|
||||
{
|
||||
name: "Purchase From",
|
||||
name: "Purchased From",
|
||||
text: item.value?.purchaseFrom || "",
|
||||
},
|
||||
{
|
||||
name: "Purchase Price",
|
||||
text: item.value?.purchasePrice ? fmtCurrency(item.value.purchasePrice) : "",
|
||||
text: item.value?.purchasePrice || "",
|
||||
type: "currency",
|
||||
},
|
||||
{
|
||||
name: "Purchase Date",
|
||||
@@ -205,7 +235,7 @@
|
||||
return item.value?.soldTo || item.value?.soldPrice !== "0";
|
||||
});
|
||||
|
||||
const soldDetails = computed<Array<Detail | DateDetail>>(() => {
|
||||
const soldDetails = computed<Details>(() => {
|
||||
return [
|
||||
{
|
||||
name: "Sold To",
|
||||
@@ -213,7 +243,8 @@
|
||||
},
|
||||
{
|
||||
name: "Sold Price",
|
||||
text: item.value?.soldPrice ? fmtCurrency(item.value.soldPrice) : "",
|
||||
text: item.value?.soldPrice || "",
|
||||
type: "currency",
|
||||
},
|
||||
{
|
||||
name: "Sold At",
|
||||
@@ -321,21 +352,28 @@
|
||||
:item-id="item.id"
|
||||
/>
|
||||
</template>
|
||||
<template #receipts>
|
||||
<ItemAttachmentsList
|
||||
v-if="attachments.receipts.length > 0"
|
||||
:attachments="attachments.receipts"
|
||||
:item-id="item.id"
|
||||
/>
|
||||
</template>
|
||||
</DetailsSection>
|
||||
</BaseCard>
|
||||
|
||||
<BaseCard v-if="showPurchase">
|
||||
<template #title> Purchase </template>
|
||||
<template #title> Purchase Details </template>
|
||||
<DetailsSection :details="purchaseDetails" />
|
||||
</BaseCard>
|
||||
|
||||
<BaseCard v-if="showWarranty">
|
||||
<template #title> Warranty </template>
|
||||
<template #title> Warranty Details </template>
|
||||
<DetailsSection :details="warrantyDetails" />
|
||||
</BaseCard>
|
||||
|
||||
<BaseCard v-if="showSold">
|
||||
<template #title> Sold </template>
|
||||
<template #title> Sold Details </template>
|
||||
<DetailsSection :details="soldDetails" />
|
||||
</BaseCard>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
layout: "home",
|
||||
middleware: ["auth"],
|
||||
});
|
||||
|
||||
const show = reactive({
|
||||
|
||||
110
frontend/pages/items.vue
Normal file
110
frontend/pages/items.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<script setup lang="ts">
|
||||
import { ItemSummary } from "~~/lib/api/types/data-contracts";
|
||||
import { useLabelStore } from "~~/stores/labels";
|
||||
import { useLocationStore } from "~~/stores/locations";
|
||||
|
||||
definePageMeta({
|
||||
middleware: ["auth"],
|
||||
});
|
||||
|
||||
useHead({
|
||||
title: "Homebox | Home",
|
||||
});
|
||||
|
||||
const api = useUserApi();
|
||||
|
||||
const query = ref("");
|
||||
const loading = useMinLoader(2000);
|
||||
const results = ref<ItemSummary[]>([]);
|
||||
|
||||
async function search() {
|
||||
loading.value = true;
|
||||
|
||||
const locations = selectedLocations.value.map(l => l.id);
|
||||
const labels = selectedLabels.value.map(l => l.id);
|
||||
|
||||
const { data, error } = await api.items.getAll({ q: query.value, locations, labels });
|
||||
if (error) {
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
results.value = data.items;
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
search();
|
||||
});
|
||||
|
||||
const locationsStore = useLocationStore();
|
||||
const locations = computed(() => locationsStore.locations);
|
||||
|
||||
const labelStore = useLabelStore();
|
||||
const labels = computed(() => labelStore.labels);
|
||||
|
||||
const advanced = ref(false);
|
||||
const selectedLocations = ref([]);
|
||||
const selectedLabels = ref([]);
|
||||
|
||||
watchEffect(() => {
|
||||
if (!advanced.value) {
|
||||
selectedLocations.value = [];
|
||||
selectedLabels.value = [];
|
||||
}
|
||||
});
|
||||
|
||||
watchDebounced(query, search, { debounce: 250, maxWait: 1000 });
|
||||
watchDebounced(selectedLocations, search, { debounce: 250, maxWait: 1000 });
|
||||
watchDebounced(selectedLabels, search, { debounce: 250, maxWait: 1000 });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseContainer class="mb-16">
|
||||
<FormTextField v-model="query" placeholder="Search" />
|
||||
<div class="flex mt-1">
|
||||
<label class="ml-auto label cursor-pointer">
|
||||
<input v-model="advanced" type="checkbox" class="toggle toggle-primary" />
|
||||
<span class="label-text text-neutral-content ml-2"> Filters </span>
|
||||
</label>
|
||||
</div>
|
||||
<BaseCard v-if="advanced" class="my-1 overflow-visible">
|
||||
<template #title> Filters </template>
|
||||
<template #subtitle>
|
||||
Location and label filters use the 'OR' operation. If more than one is selected only one will be required for a
|
||||
match
|
||||
</template>
|
||||
<div class="px-4 pb-4">
|
||||
<FormMultiselect v-model="selectedLabels" label="Labels" :items="labels ?? []" />
|
||||
<FormMultiselect v-model="selectedLocations" label="Locations" :items="locations ?? []" />
|
||||
</div>
|
||||
</BaseCard>
|
||||
<section class="mt-10">
|
||||
<BaseSectionHeader class="mb-5"> Items </BaseSectionHeader>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<TransitionGroup name="list">
|
||||
<ItemCard v-for="item in results" :key="item.id" :item="item" />
|
||||
</TransitionGroup>
|
||||
<div class="hidden first:inline text-xl">No Items Found</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseContainer>
|
||||
</template>
|
||||
|
||||
<style lang="css">
|
||||
.list-move,
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
|
||||
.list-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import type { DateDetail, Detail } from "~~/components/global/DetailsSection/types";
|
||||
import type { CustomDetail, Detail } from "~~/components/global/DetailsSection/types";
|
||||
|
||||
definePageMeta({
|
||||
layout: "home",
|
||||
middleware: ["auth"],
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
@@ -23,7 +23,7 @@
|
||||
return data;
|
||||
});
|
||||
|
||||
const details = computed<(Detail | DateDetail)[]>(() => {
|
||||
const details = computed<(Detail | CustomDetail)[]>(() => {
|
||||
const details = [
|
||||
{
|
||||
name: "Name",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { Detail, DateDetail } from "~~/components/global/DetailsSection/types";
|
||||
import { Detail, CustomDetail } from "~~/components/global/DetailsSection/types";
|
||||
|
||||
definePageMeta({
|
||||
layout: "home",
|
||||
middleware: ["auth"],
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
@@ -23,7 +23,7 @@
|
||||
return data;
|
||||
});
|
||||
|
||||
const details = computed<(Detail | DateDetail)[]>(() => {
|
||||
const details = computed<(Detail | CustomDetail)[]>(() => {
|
||||
const details = [
|
||||
{
|
||||
name: "Name",
|
||||
|
||||
@@ -1,137 +1,79 @@
|
||||
<script setup lang="ts">
|
||||
import { Detail } from "~~/components/global/DetailsSection/types";
|
||||
import { DaisyTheme } from "~~/composables/use-preferences";
|
||||
import { useAuthStore } from "~~/stores/auth";
|
||||
import { themes } from "~~/lib/data/themes";
|
||||
import { currencies, Currency } from "~~/lib/data/currency";
|
||||
|
||||
definePageMeta({
|
||||
layout: "home",
|
||||
middleware: ["auth"],
|
||||
});
|
||||
useHead({
|
||||
title: "Homebox | Profile",
|
||||
});
|
||||
|
||||
const api = useUserApi();
|
||||
const confirm = useConfirm();
|
||||
const notify = useNotifier();
|
||||
|
||||
// Currency Selection
|
||||
const currency = ref<Currency>(currencies[0]);
|
||||
|
||||
watch(currency, () => {
|
||||
if (group.value) {
|
||||
group.value.currency = currency.value.code;
|
||||
}
|
||||
|
||||
console.log(group.value);
|
||||
});
|
||||
|
||||
const currencyExample = computed(() => {
|
||||
const formatter = new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: currency.value ? currency.value.code : "USD",
|
||||
});
|
||||
|
||||
return formatter.format(1000);
|
||||
});
|
||||
|
||||
const { data: group } = useAsyncData(async () => {
|
||||
const { data } = await api.group.get();
|
||||
return data;
|
||||
});
|
||||
|
||||
// Sync Initial Currency
|
||||
watch(group, () => {
|
||||
if (group.value) {
|
||||
const found = currencies.find(c => c.code === group.value.currency);
|
||||
if (found) {
|
||||
currency.value = found;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function updateGroup() {
|
||||
const { data, error } = await api.group.update({
|
||||
name: group.value.name,
|
||||
currency: group.value.currency,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
notify.error("Failed to update group");
|
||||
return;
|
||||
}
|
||||
|
||||
group.value = data;
|
||||
notify.success("Group updated");
|
||||
}
|
||||
|
||||
const pubApi = usePublicApi();
|
||||
const { data: status } = useAsyncData(async () => {
|
||||
const { data } = await pubApi.status();
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
type ThemeOption = {
|
||||
label: string;
|
||||
value: DaisyTheme;
|
||||
};
|
||||
|
||||
const themes: ThemeOption[] = [
|
||||
{
|
||||
label: "Garden",
|
||||
value: "garden",
|
||||
},
|
||||
{
|
||||
label: "Light",
|
||||
value: "light",
|
||||
},
|
||||
{
|
||||
label: "Cupcake",
|
||||
value: "cupcake",
|
||||
},
|
||||
{
|
||||
label: "Bumblebee",
|
||||
value: "bumblebee",
|
||||
},
|
||||
{
|
||||
label: "Emerald",
|
||||
value: "emerald",
|
||||
},
|
||||
{
|
||||
label: "Corporate",
|
||||
value: "corporate",
|
||||
},
|
||||
{
|
||||
label: "Synthwave",
|
||||
value: "synthwave",
|
||||
},
|
||||
{
|
||||
label: "Retro",
|
||||
value: "retro",
|
||||
},
|
||||
{
|
||||
label: "Cyberpunk",
|
||||
value: "cyberpunk",
|
||||
},
|
||||
{
|
||||
label: "Valentine",
|
||||
value: "valentine",
|
||||
},
|
||||
{
|
||||
label: "Halloween",
|
||||
value: "halloween",
|
||||
},
|
||||
{
|
||||
label: "Forest",
|
||||
value: "forest",
|
||||
},
|
||||
{
|
||||
label: "Aqua",
|
||||
value: "aqua",
|
||||
},
|
||||
{
|
||||
label: "Lofi",
|
||||
value: "lofi",
|
||||
},
|
||||
{
|
||||
label: "Pastel",
|
||||
value: "pastel",
|
||||
},
|
||||
{
|
||||
label: "Fantasy",
|
||||
value: "fantasy",
|
||||
},
|
||||
{
|
||||
label: "Wireframe",
|
||||
value: "wireframe",
|
||||
},
|
||||
{
|
||||
label: "Black",
|
||||
value: "black",
|
||||
},
|
||||
{
|
||||
label: "Luxury",
|
||||
value: "luxury",
|
||||
},
|
||||
{
|
||||
label: "Dracula",
|
||||
value: "dracula",
|
||||
},
|
||||
{
|
||||
label: "Cmyk",
|
||||
value: "cmyk",
|
||||
},
|
||||
{
|
||||
label: "Autumn",
|
||||
value: "autumn",
|
||||
},
|
||||
{
|
||||
label: "Business",
|
||||
value: "business",
|
||||
},
|
||||
{
|
||||
label: "Acid",
|
||||
value: "acid",
|
||||
},
|
||||
{
|
||||
label: "Lemonade",
|
||||
value: "lemonade",
|
||||
},
|
||||
{
|
||||
label: "Night",
|
||||
value: "night",
|
||||
},
|
||||
{
|
||||
label: "Coffee",
|
||||
value: "coffee",
|
||||
},
|
||||
{
|
||||
label: "Winter",
|
||||
value: "winter",
|
||||
},
|
||||
];
|
||||
|
||||
const auth = useAuthStore();
|
||||
|
||||
const details = computed(() => {
|
||||
@@ -147,10 +89,6 @@
|
||||
] as Detail[];
|
||||
});
|
||||
|
||||
const api = useUserApi();
|
||||
const confirm = useConfirm();
|
||||
const notify = useNotifier();
|
||||
|
||||
async function deleteProfile() {
|
||||
const result = await confirm.open(
|
||||
"Are you sure you want to delete your account? If you are the last member in your group all your data will be deleted. This action cannot be undone."
|
||||
@@ -276,6 +214,27 @@
|
||||
</div>
|
||||
</BaseCard>
|
||||
|
||||
<BaseCard>
|
||||
<template #title>
|
||||
<BaseSectionHeader class="pb-0">
|
||||
<Icon name="mdi-accounts" class="mr-2 -mt-1 text-base-600" />
|
||||
<span class="text-base-600"> Group Settings </span>
|
||||
<template #description>
|
||||
Shared Group Settings. You may need to refresh your browser for some settings to apply.
|
||||
</template>
|
||||
</BaseSectionHeader>
|
||||
</template>
|
||||
|
||||
<div v-if="group" class="p-5 pt-0">
|
||||
<FormSelect v-model="currency" label="Currency Format" :items="currencies" />
|
||||
<p class="m-2 text-sm">Example: {{ currencyExample }}</p>
|
||||
|
||||
<div class="mt-4 flex justify-end">
|
||||
<BaseButton @click="updateGroup"> Update Group </BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
|
||||
<BaseCard>
|
||||
<template #title>
|
||||
<BaseSectionHeader>
|
||||
@@ -341,6 +300,9 @@
|
||||
</template>
|
||||
</BaseCard>
|
||||
</BaseContainer>
|
||||
<footer v-if="status" class="text-center w-full bottom-0 pb-4">
|
||||
<p class="text-center text-sm">Version: {{ status.build.version }} ~ Build: {{ status.build.commit }}</p>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ def date_types(*names: list[str]) -> dict[re.Pattern, str]:
|
||||
|
||||
|
||||
regex_replace: dict[re.Pattern, str] = {
|
||||
re.compile(r" PaginationResultRepo"): "PaginationResult",
|
||||
re.compile(r" Repo"): " ",
|
||||
re.compile(r" Services"): " ",
|
||||
re.compile(r" V1"): " ",
|
||||
|
||||
Reference in New Issue
Block a user