mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-21 21:33:02 +01:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efd7069fe4 | ||
|
|
dd349aa98e | ||
|
|
607b06d2f2 |
@@ -2,6 +2,7 @@ version: "3"
|
||||
|
||||
env:
|
||||
HBOX_STORAGE_SQLITE_URL: .data/homebox.db?_fk=1
|
||||
HBOX_OPTIONS_ALLOW_REGISTRATION: true
|
||||
UNSAFE_DISABLE_PASSWORD_PROJECTION: "yes_i_am_sure"
|
||||
tasks:
|
||||
setup:
|
||||
|
||||
@@ -44,12 +44,13 @@ type (
|
||||
}
|
||||
|
||||
ApiSummary struct {
|
||||
Healthy bool `json:"health"`
|
||||
Versions []string `json:"versions"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Build Build `json:"build"`
|
||||
Demo bool `json:"demo"`
|
||||
Healthy bool `json:"health"`
|
||||
Versions []string `json:"versions"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Build Build `json:"build"`
|
||||
Demo bool `json:"demo"`
|
||||
AllowRegistration bool `json:"allowRegistration"`
|
||||
}
|
||||
)
|
||||
|
||||
@@ -82,11 +83,12 @@ func NewControllerV1(svc *services.AllServices, repos *repo.AllRepos, options ..
|
||||
func (ctrl *V1Controller) HandleBase(ready ReadyFunc, build Build) server.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
return server.Respond(w, http.StatusOK, ApiSummary{
|
||||
Healthy: ready(),
|
||||
Title: "Go API Template",
|
||||
Message: "Welcome to the Go API Template Application!",
|
||||
Build: build,
|
||||
Demo: ctrl.isDemo,
|
||||
Healthy: ready(),
|
||||
Title: "Go API Template",
|
||||
Message: "Welcome to the Go API Template Application!",
|
||||
Build: build,
|
||||
Demo: ctrl.isDemo,
|
||||
AllowRegistration: ctrl.allowRegistration,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -28,7 +29,7 @@ func (ctrl *V1Controller) HandleUserRegistration() server.HandlerFunc {
|
||||
}
|
||||
|
||||
if !ctrl.allowRegistration && regData.GroupToken == "" {
|
||||
return validate.NewRequestError(nil, http.StatusForbidden)
|
||||
return validate.NewRequestError(fmt.Errorf("user registration disabled"), http.StatusForbidden)
|
||||
}
|
||||
|
||||
_, err := ctrl.svc.User.RegisterUser(r.Context(), regData)
|
||||
|
||||
@@ -1983,6 +1983,7 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
},
|
||||
"warrantyExpires": {
|
||||
"description": "Sold",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
@@ -2424,6 +2425,9 @@ const docTemplate = `{
|
||||
"v1.ApiSummary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allowRegistration": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"build": {
|
||||
"$ref": "#/definitions/v1.Build"
|
||||
},
|
||||
|
||||
@@ -1975,6 +1975,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"warrantyExpires": {
|
||||
"description": "Sold",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
@@ -2416,6 +2417,9 @@
|
||||
"v1.ApiSummary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allowRegistration": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"build": {
|
||||
"$ref": "#/definitions/v1.Build"
|
||||
},
|
||||
|
||||
@@ -275,6 +275,7 @@ definitions:
|
||||
warrantyDetails:
|
||||
type: string
|
||||
warrantyExpires:
|
||||
description: Sold
|
||||
type: string
|
||||
type: object
|
||||
repo.LabelCreate:
|
||||
@@ -563,6 +564,8 @@ definitions:
|
||||
type: object
|
||||
v1.ApiSummary:
|
||||
properties:
|
||||
allowRegistration:
|
||||
type: boolean
|
||||
build:
|
||||
$ref: '#/definitions/v1.Build'
|
||||
demo:
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hay-kot/homebox/backend/internal/data/repo"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/types"
|
||||
)
|
||||
|
||||
func determineSeparator(data []byte) (rune, error) {
|
||||
@@ -62,15 +62,6 @@ func parseFloat(s string) float64 {
|
||||
return f
|
||||
}
|
||||
|
||||
func parseDate(s string) time.Time {
|
||||
if s == "" {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
p, _ := time.Parse("01/02/2006", s)
|
||||
return p
|
||||
}
|
||||
|
||||
func parseBool(s string) bool {
|
||||
switch strings.ToLower(s) {
|
||||
case "true", "yes", "1":
|
||||
@@ -92,6 +83,7 @@ type csvRow struct {
|
||||
}
|
||||
|
||||
func newCsvRow(row []string) csvRow {
|
||||
|
||||
return csvRow{
|
||||
Location: row[1],
|
||||
LabelStr: row[2],
|
||||
@@ -109,13 +101,13 @@ func newCsvRow(row []string) csvRow {
|
||||
Manufacturer: row[9],
|
||||
Notes: row[10],
|
||||
PurchaseFrom: row[11],
|
||||
PurchaseTime: parseDate(row[13]),
|
||||
PurchaseTime: types.DateFromString(row[13]),
|
||||
LifetimeWarranty: parseBool(row[14]),
|
||||
WarrantyExpires: parseDate(row[15]),
|
||||
WarrantyExpires: types.DateFromString(row[15]),
|
||||
WarrantyDetails: row[16],
|
||||
SoldTo: row[17],
|
||||
SoldPrice: parseFloat(row[18]),
|
||||
SoldTime: parseDate(row[19]),
|
||||
SoldTime: types.DateFromString(row[19]),
|
||||
SoldNotes: row[20],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -50,9 +50,9 @@ func Test_CorrectDateParsing(t *testing.T) {
|
||||
entity := newCsvRow(record)
|
||||
expected := expected[i-1]
|
||||
|
||||
assert.Equal(t, expected, entity.Item.PurchaseTime, fmt.Sprintf("Failed on row %d", i))
|
||||
assert.Equal(t, expected, entity.Item.WarrantyExpires, fmt.Sprintf("Failed on row %d", i))
|
||||
assert.Equal(t, expected, entity.Item.SoldTime, fmt.Sprintf("Failed on row %d", i))
|
||||
assert.Equal(t, expected, entity.Item.PurchaseTime.Time(), fmt.Sprintf("Failed on row %d", i))
|
||||
assert.Equal(t, expected, entity.Item.WarrantyExpires.Time(), fmt.Sprintf("Failed on row %d", i))
|
||||
assert.Equal(t, expected, entity.Item.SoldTime.Time(), fmt.Sprintf("Failed on row %d", i))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent/label"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent/location"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent/predicate"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/types"
|
||||
)
|
||||
|
||||
type ItemsRepository struct {
|
||||
@@ -78,20 +79,20 @@ type (
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
|
||||
// Warranty
|
||||
LifetimeWarranty bool `json:"lifetimeWarranty"`
|
||||
WarrantyExpires time.Time `json:"warrantyExpires"`
|
||||
WarrantyDetails string `json:"warrantyDetails"`
|
||||
LifetimeWarranty bool `json:"lifetimeWarranty"`
|
||||
WarrantyExpires types.Date `json:"warrantyExpires"`
|
||||
WarrantyDetails string `json:"warrantyDetails"`
|
||||
|
||||
// Purchase
|
||||
PurchaseTime time.Time `json:"purchaseTime"`
|
||||
PurchaseFrom string `json:"purchaseFrom"`
|
||||
PurchasePrice float64 `json:"purchasePrice,string"`
|
||||
PurchaseTime types.Date `json:"purchaseTime"`
|
||||
PurchaseFrom string `json:"purchaseFrom"`
|
||||
PurchasePrice float64 `json:"purchasePrice,string"`
|
||||
|
||||
// Sold
|
||||
SoldTime time.Time `json:"soldTime"`
|
||||
SoldTo string `json:"soldTo"`
|
||||
SoldPrice float64 `json:"soldPrice,string"`
|
||||
SoldNotes string `json:"soldNotes"`
|
||||
SoldTime types.Date `json:"soldTime"`
|
||||
SoldTo string `json:"soldTo"`
|
||||
SoldPrice float64 `json:"soldPrice,string"`
|
||||
SoldNotes string `json:"soldNotes"`
|
||||
|
||||
// Extras
|
||||
Notes string `json:"notes"`
|
||||
@@ -126,19 +127,19 @@ type (
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
|
||||
// Warranty
|
||||
LifetimeWarranty bool `json:"lifetimeWarranty"`
|
||||
WarrantyExpires time.Time `json:"warrantyExpires"`
|
||||
WarrantyDetails string `json:"warrantyDetails"`
|
||||
LifetimeWarranty bool `json:"lifetimeWarranty"`
|
||||
WarrantyExpires types.Date `json:"warrantyExpires"`
|
||||
WarrantyDetails string `json:"warrantyDetails"`
|
||||
|
||||
// Purchase
|
||||
PurchaseTime time.Time `json:"purchaseTime"`
|
||||
PurchaseFrom string `json:"purchaseFrom"`
|
||||
PurchaseTime types.Date `json:"purchaseTime"`
|
||||
PurchaseFrom string `json:"purchaseFrom"`
|
||||
|
||||
// Sold
|
||||
SoldTime time.Time `json:"soldTime"`
|
||||
SoldTo string `json:"soldTo"`
|
||||
SoldPrice float64 `json:"soldPrice,string"`
|
||||
SoldNotes string `json:"soldNotes"`
|
||||
SoldTime types.Date `json:"soldTime"`
|
||||
SoldTo string `json:"soldTo"`
|
||||
SoldPrice float64 `json:"soldPrice,string"`
|
||||
SoldNotes string `json:"soldNotes"`
|
||||
|
||||
// Extras
|
||||
Notes string `json:"notes"`
|
||||
@@ -232,7 +233,7 @@ func mapItemOut(item *ent.Item) ItemOut {
|
||||
AssetID: AssetID(item.AssetID),
|
||||
ItemSummary: mapItemSummary(item),
|
||||
LifetimeWarranty: item.LifetimeWarranty,
|
||||
WarrantyExpires: item.WarrantyExpires,
|
||||
WarrantyExpires: types.DateFromTime(item.WarrantyExpires),
|
||||
WarrantyDetails: item.WarrantyDetails,
|
||||
|
||||
// Identification
|
||||
@@ -241,11 +242,11 @@ func mapItemOut(item *ent.Item) ItemOut {
|
||||
Manufacturer: item.Manufacturer,
|
||||
|
||||
// Purchase
|
||||
PurchaseTime: item.PurchaseTime,
|
||||
PurchaseTime: types.DateFromTime(item.PurchaseTime),
|
||||
PurchaseFrom: item.PurchaseFrom,
|
||||
|
||||
// Sold
|
||||
SoldTime: item.SoldTime,
|
||||
SoldTime: types.DateFromTime(item.SoldTime),
|
||||
SoldTo: item.SoldTo,
|
||||
SoldPrice: item.SoldPrice,
|
||||
SoldNotes: item.SoldNotes,
|
||||
@@ -526,17 +527,17 @@ func (e *ItemsRepository) UpdateByGroup(ctx context.Context, GID uuid.UUID, data
|
||||
SetModelNumber(data.ModelNumber).
|
||||
SetManufacturer(data.Manufacturer).
|
||||
SetArchived(data.Archived).
|
||||
SetPurchaseTime(data.PurchaseTime).
|
||||
SetPurchaseTime(data.PurchaseTime.Time()).
|
||||
SetPurchaseFrom(data.PurchaseFrom).
|
||||
SetPurchasePrice(data.PurchasePrice).
|
||||
SetSoldTime(data.SoldTime).
|
||||
SetSoldTime(data.SoldTime.Time()).
|
||||
SetSoldTo(data.SoldTo).
|
||||
SetSoldPrice(data.SoldPrice).
|
||||
SetSoldNotes(data.SoldNotes).
|
||||
SetNotes(data.Notes).
|
||||
SetLifetimeWarranty(data.LifetimeWarranty).
|
||||
SetInsured(data.Insured).
|
||||
SetWarrantyExpires(data.WarrantyExpires).
|
||||
SetWarrantyExpires(data.WarrantyExpires.Time()).
|
||||
SetWarrantyDetails(data.WarrantyDetails).
|
||||
SetQuantity(data.Quantity).
|
||||
SetAssetID(int(data.AssetID))
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -237,15 +238,15 @@ func TestItemsRepository_Update(t *testing.T) {
|
||||
LabelIDs: nil,
|
||||
ModelNumber: fk.Str(10),
|
||||
Manufacturer: fk.Str(10),
|
||||
PurchaseTime: time.Now(),
|
||||
PurchaseTime: types.DateFromTime(time.Now()),
|
||||
PurchaseFrom: fk.Str(10),
|
||||
PurchasePrice: 300.99,
|
||||
SoldTime: time.Now(),
|
||||
SoldTime: types.DateFromTime(time.Now()),
|
||||
SoldTo: fk.Str(10),
|
||||
SoldPrice: 300.99,
|
||||
SoldNotes: fk.Str(10),
|
||||
Notes: fk.Str(10),
|
||||
WarrantyExpires: time.Now(),
|
||||
WarrantyExpires: types.DateFromTime(time.Now()),
|
||||
WarrantyDetails: fk.Str(10),
|
||||
LifetimeWarranty: true,
|
||||
}
|
||||
|
||||
88
backend/internal/data/types/date.go
Normal file
88
backend/internal/data/types/date.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
// Date is a custom type that implements the MarshalJSON interface
|
||||
// that applies date only formatting to the time.Time fields in order
|
||||
// to avoid common time and timezone pitfalls when working with Times.
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// "2019-01-01" -> time.Time{2019-01-01 00:00:00 +0000 UTC}
|
||||
// "2019-01-01T21:10:30Z" -> time.Time{2019-01-01 00:00:00 +0000 UTC}
|
||||
// "2019-01-01T21:10:30+01:00" -> time.Time{2019-01-01 00:00:00 +0000 UTC}
|
||||
type Date time.Time
|
||||
|
||||
func (d Date) Time() time.Time {
|
||||
return time.Time(d)
|
||||
}
|
||||
|
||||
// DateFromTime returns a Date type from a time.Time type by stripping
|
||||
// the time and timezone information.
|
||||
func DateFromTime(t time.Time) Date {
|
||||
dateOnlyTime := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC)
|
||||
return Date(dateOnlyTime)
|
||||
}
|
||||
|
||||
// DateFromString returns a Date type from a string by parsing the
|
||||
// string into a time.Time type and then stripping the time and
|
||||
// timezone information.
|
||||
//
|
||||
// Errors are ignored and an empty Date is returned.
|
||||
func DateFromString(s string) Date {
|
||||
if s == "" {
|
||||
return Date{}
|
||||
}
|
||||
|
||||
t, err := time.Parse("2006-01-02", s)
|
||||
if err != nil {
|
||||
// TODO: Remove - used by legacy importer
|
||||
t, err = time.Parse("01/02/2006", s)
|
||||
|
||||
if err != nil {
|
||||
return Date{}
|
||||
}
|
||||
}
|
||||
|
||||
return DateFromTime(t)
|
||||
}
|
||||
|
||||
func (d Date) String() string {
|
||||
if time.Time(d).IsZero() {
|
||||
return ""
|
||||
}
|
||||
|
||||
return time.Time(d).Format("2006-01-02")
|
||||
}
|
||||
|
||||
func (d Date) MarshalJSON() ([]byte, error) {
|
||||
if time.Time(d).IsZero() {
|
||||
return []byte(`""`), nil
|
||||
}
|
||||
|
||||
return []byte(`"` + d.String() + `"`), nil
|
||||
}
|
||||
|
||||
func (d *Date) UnmarshalJSON(data []byte) error {
|
||||
str := string(data)
|
||||
if str == `""` {
|
||||
*d = Date{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try YYYY-MM-DD format
|
||||
var t time.Time
|
||||
t, err := time.Parse("2006-01-02", str)
|
||||
if err != nil {
|
||||
// Try default interface
|
||||
err = t.UnmarshalJSON(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// strip the time and timezone information
|
||||
*d = DateFromTime(t)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -27,7 +27,7 @@
|
||||
<li
|
||||
:class="[
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9 duration-75 ease-in-out transition-colors',
|
||||
active ? 'bg-primary text-white' : 'text-gray-900',
|
||||
active ? 'bg-primary text-primary-content' : 'text-base-content',
|
||||
]"
|
||||
>
|
||||
<slot name="display" v-bind="{ item: item, selected, active }">
|
||||
|
||||
@@ -16,9 +16,11 @@ export function hasKey(obj: object, key: string): obj is Required<BaseApiType> {
|
||||
export function parseDate<T>(obj: T, keys: Array<keyof T> = []): T {
|
||||
const result = { ...obj };
|
||||
[...keys, "createdAt", "updatedAt"].forEach(key => {
|
||||
// @ts-ignore - TS doesn't know that we're checking for the key above
|
||||
// @ts-expect-error - TS doesn't know that we're checking for the key above
|
||||
if (hasKey(result, key)) {
|
||||
if (result[key] === ZERO_DATE) {
|
||||
const value = result[key] as string;
|
||||
|
||||
if (value === undefined || value === "" || value.startsWith(ZERO_DATE)) {
|
||||
const dt = new Date();
|
||||
dt.setFullYear(1);
|
||||
|
||||
@@ -26,11 +28,33 @@ export function parseDate<T>(obj: T, keys: Array<keyof T> = []): T {
|
||||
return;
|
||||
}
|
||||
|
||||
// transform string to ensure dates are parsed as UTC dates instead of
|
||||
// localized time stamps
|
||||
const asStr = result[key] as string;
|
||||
const cleaned = asStr.replaceAll("-", "/").split("T")[0];
|
||||
result[key] = new Date(cleaned);
|
||||
// Possible Formats
|
||||
// Date Only: YYYY-MM-DD
|
||||
// Timestamp: 0001-01-01T00:00:00Z
|
||||
|
||||
// Parse timestamps with default date
|
||||
if (value.includes("T")) {
|
||||
result[key] = new Date(value);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse dates with default time
|
||||
const split = value.split("-");
|
||||
|
||||
if (split.length !== 3) {
|
||||
console.log(`Invalid date format: ${value}`);
|
||||
throw new Error(`Invalid date format: ${value}`);
|
||||
}
|
||||
|
||||
const [year, month, day] = split;
|
||||
|
||||
const dt = new Date();
|
||||
|
||||
dt.setFullYear(parseInt(year, 10));
|
||||
dt.setMonth(parseInt(month, 10) - 1);
|
||||
dt.setDate(parseInt(day, 10));
|
||||
|
||||
result[key] = dt;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -335,6 +335,7 @@ export interface ActionAmountResult {
|
||||
}
|
||||
|
||||
export interface ApiSummary {
|
||||
allowRegistration: boolean;
|
||||
build: Build;
|
||||
demo: boolean;
|
||||
health: boolean;
|
||||
|
||||
@@ -101,7 +101,6 @@
|
||||
|
||||
toast.success("Logged in successfully");
|
||||
|
||||
// @ts-expect-error - expires is either a date or a string, need to figure out store typing
|
||||
authStore.$patch({
|
||||
token: data.token,
|
||||
expires: data.expiresAt,
|
||||
@@ -214,11 +213,13 @@
|
||||
</Transition>
|
||||
<div class="text-center mt-6">
|
||||
<button
|
||||
class="text-base-content text-lg hover:bg-primary hover:text-primary-content px-3 py-1 rounded-xl transition-colors duration-200"
|
||||
v-if="status && status.allowRegistration"
|
||||
class="btn text-base-content text-lg hover:bg-primary hover:text-primary-content transition-colors duration-200"
|
||||
@click="() => toggleLogin()"
|
||||
>
|
||||
{{ registerForm ? "Already a User? Login" : "Not a User? Register" }}
|
||||
{{ registerForm ? "Login" : "Register" }}
|
||||
</button>
|
||||
<p v-else class="text-base-content italic text-sm">Registration Disabled</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user