Compare commits

...

3 Commits

Author SHA1 Message Date
Hayden
efd7069fe4 feat: hide registration button when disabled (#287)
* add allow registration to API Summary

* code gen

* use env for troubleshooting

* disable registration toggle based on backend
2023-02-15 08:58:38 -09:00
Hayden
dd349aa98e fix #285 (#286) 2023-02-15 08:52:13 -09:00
Hayden
607b06d2f2 fix: date and datetime regression (#282)
* use custom types.Date implementation

* fix user registration bug

* remove sanity check

* fix datetime bug
2023-02-15 08:40:35 -09:00
15 changed files with 190 additions and 67 deletions

View File

@@ -2,6 +2,7 @@ version: "3"
env: env:
HBOX_STORAGE_SQLITE_URL: .data/homebox.db?_fk=1 HBOX_STORAGE_SQLITE_URL: .data/homebox.db?_fk=1
HBOX_OPTIONS_ALLOW_REGISTRATION: true
UNSAFE_DISABLE_PASSWORD_PROJECTION: "yes_i_am_sure" UNSAFE_DISABLE_PASSWORD_PROJECTION: "yes_i_am_sure"
tasks: tasks:
setup: setup:

View File

@@ -50,6 +50,7 @@ type (
Message string `json:"message"` Message string `json:"message"`
Build Build `json:"build"` Build Build `json:"build"`
Demo bool `json:"demo"` Demo bool `json:"demo"`
AllowRegistration bool `json:"allowRegistration"`
} }
) )
@@ -87,6 +88,7 @@ func (ctrl *V1Controller) HandleBase(ready ReadyFunc, build Build) server.Handle
Message: "Welcome to the Go API Template Application!", Message: "Welcome to the Go API Template Application!",
Build: build, Build: build,
Demo: ctrl.isDemo, Demo: ctrl.isDemo,
AllowRegistration: ctrl.allowRegistration,
}) })
} }
} }

View File

@@ -1,6 +1,7 @@
package v1 package v1
import ( import (
"fmt"
"net/http" "net/http"
"github.com/google/uuid" "github.com/google/uuid"
@@ -28,7 +29,7 @@ func (ctrl *V1Controller) HandleUserRegistration() server.HandlerFunc {
} }
if !ctrl.allowRegistration && regData.GroupToken == "" { 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) _, err := ctrl.svc.User.RegisterUser(r.Context(), regData)

View File

@@ -1983,6 +1983,7 @@ const docTemplate = `{
"type": "string" "type": "string"
}, },
"warrantyExpires": { "warrantyExpires": {
"description": "Sold",
"type": "string" "type": "string"
} }
} }
@@ -2424,6 +2425,9 @@ const docTemplate = `{
"v1.ApiSummary": { "v1.ApiSummary": {
"type": "object", "type": "object",
"properties": { "properties": {
"allowRegistration": {
"type": "boolean"
},
"build": { "build": {
"$ref": "#/definitions/v1.Build" "$ref": "#/definitions/v1.Build"
}, },

View File

@@ -1975,6 +1975,7 @@
"type": "string" "type": "string"
}, },
"warrantyExpires": { "warrantyExpires": {
"description": "Sold",
"type": "string" "type": "string"
} }
} }
@@ -2416,6 +2417,9 @@
"v1.ApiSummary": { "v1.ApiSummary": {
"type": "object", "type": "object",
"properties": { "properties": {
"allowRegistration": {
"type": "boolean"
},
"build": { "build": {
"$ref": "#/definitions/v1.Build" "$ref": "#/definitions/v1.Build"
}, },

View File

@@ -275,6 +275,7 @@ definitions:
warrantyDetails: warrantyDetails:
type: string type: string
warrantyExpires: warrantyExpires:
description: Sold
type: string type: string
type: object type: object
repo.LabelCreate: repo.LabelCreate:
@@ -563,6 +564,8 @@ definitions:
type: object type: object
v1.ApiSummary: v1.ApiSummary:
properties: properties:
allowRegistration:
type: boolean
build: build:
$ref: '#/definitions/v1.Build' $ref: '#/definitions/v1.Build'
demo: demo:

View File

@@ -7,9 +7,9 @@ import (
"io" "io"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/data/types"
) )
func determineSeparator(data []byte) (rune, error) { func determineSeparator(data []byte) (rune, error) {
@@ -62,15 +62,6 @@ func parseFloat(s string) float64 {
return f 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 { func parseBool(s string) bool {
switch strings.ToLower(s) { switch strings.ToLower(s) {
case "true", "yes", "1": case "true", "yes", "1":
@@ -92,6 +83,7 @@ type csvRow struct {
} }
func newCsvRow(row []string) csvRow { func newCsvRow(row []string) csvRow {
return csvRow{ return csvRow{
Location: row[1], Location: row[1],
LabelStr: row[2], LabelStr: row[2],
@@ -109,13 +101,13 @@ func newCsvRow(row []string) csvRow {
Manufacturer: row[9], Manufacturer: row[9],
Notes: row[10], Notes: row[10],
PurchaseFrom: row[11], PurchaseFrom: row[11],
PurchaseTime: parseDate(row[13]), PurchaseTime: types.DateFromString(row[13]),
LifetimeWarranty: parseBool(row[14]), LifetimeWarranty: parseBool(row[14]),
WarrantyExpires: parseDate(row[15]), WarrantyExpires: types.DateFromString(row[15]),
WarrantyDetails: row[16], WarrantyDetails: row[16],
SoldTo: row[17], SoldTo: row[17],
SoldPrice: parseFloat(row[18]), SoldPrice: parseFloat(row[18]),
SoldTime: parseDate(row[19]), SoldTime: types.DateFromString(row[19]),
SoldNotes: row[20], SoldNotes: row[20],
}, },
} }

View File

@@ -50,9 +50,9 @@ func Test_CorrectDateParsing(t *testing.T) {
entity := newCsvRow(record) entity := newCsvRow(record)
expected := expected[i-1] expected := expected[i-1]
assert.Equal(t, expected, entity.Item.PurchaseTime, 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, 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, fmt.Sprintf("Failed on row %d", i)) assert.Equal(t, expected, entity.Item.SoldTime.Time(), fmt.Sprintf("Failed on row %d", i))
} }
} }

View File

@@ -13,6 +13,7 @@ import (
"github.com/hay-kot/homebox/backend/internal/data/ent/label" "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/location"
"github.com/hay-kot/homebox/backend/internal/data/ent/predicate" "github.com/hay-kot/homebox/backend/internal/data/ent/predicate"
"github.com/hay-kot/homebox/backend/internal/data/types"
) )
type ItemsRepository struct { type ItemsRepository struct {
@@ -79,16 +80,16 @@ type (
// Warranty // Warranty
LifetimeWarranty bool `json:"lifetimeWarranty"` LifetimeWarranty bool `json:"lifetimeWarranty"`
WarrantyExpires time.Time `json:"warrantyExpires"` WarrantyExpires types.Date `json:"warrantyExpires"`
WarrantyDetails string `json:"warrantyDetails"` WarrantyDetails string `json:"warrantyDetails"`
// Purchase // Purchase
PurchaseTime time.Time `json:"purchaseTime"` PurchaseTime types.Date `json:"purchaseTime"`
PurchaseFrom string `json:"purchaseFrom"` PurchaseFrom string `json:"purchaseFrom"`
PurchasePrice float64 `json:"purchasePrice,string"` PurchasePrice float64 `json:"purchasePrice,string"`
// Sold // Sold
SoldTime time.Time `json:"soldTime"` SoldTime types.Date `json:"soldTime"`
SoldTo string `json:"soldTo"` SoldTo string `json:"soldTo"`
SoldPrice float64 `json:"soldPrice,string"` SoldPrice float64 `json:"soldPrice,string"`
SoldNotes string `json:"soldNotes"` SoldNotes string `json:"soldNotes"`
@@ -127,15 +128,15 @@ type (
// Warranty // Warranty
LifetimeWarranty bool `json:"lifetimeWarranty"` LifetimeWarranty bool `json:"lifetimeWarranty"`
WarrantyExpires time.Time `json:"warrantyExpires"` WarrantyExpires types.Date `json:"warrantyExpires"`
WarrantyDetails string `json:"warrantyDetails"` WarrantyDetails string `json:"warrantyDetails"`
// Purchase // Purchase
PurchaseTime time.Time `json:"purchaseTime"` PurchaseTime types.Date `json:"purchaseTime"`
PurchaseFrom string `json:"purchaseFrom"` PurchaseFrom string `json:"purchaseFrom"`
// Sold // Sold
SoldTime time.Time `json:"soldTime"` SoldTime types.Date `json:"soldTime"`
SoldTo string `json:"soldTo"` SoldTo string `json:"soldTo"`
SoldPrice float64 `json:"soldPrice,string"` SoldPrice float64 `json:"soldPrice,string"`
SoldNotes string `json:"soldNotes"` SoldNotes string `json:"soldNotes"`
@@ -232,7 +233,7 @@ func mapItemOut(item *ent.Item) ItemOut {
AssetID: AssetID(item.AssetID), AssetID: AssetID(item.AssetID),
ItemSummary: mapItemSummary(item), ItemSummary: mapItemSummary(item),
LifetimeWarranty: item.LifetimeWarranty, LifetimeWarranty: item.LifetimeWarranty,
WarrantyExpires: item.WarrantyExpires, WarrantyExpires: types.DateFromTime(item.WarrantyExpires),
WarrantyDetails: item.WarrantyDetails, WarrantyDetails: item.WarrantyDetails,
// Identification // Identification
@@ -241,11 +242,11 @@ func mapItemOut(item *ent.Item) ItemOut {
Manufacturer: item.Manufacturer, Manufacturer: item.Manufacturer,
// Purchase // Purchase
PurchaseTime: item.PurchaseTime, PurchaseTime: types.DateFromTime(item.PurchaseTime),
PurchaseFrom: item.PurchaseFrom, PurchaseFrom: item.PurchaseFrom,
// Sold // Sold
SoldTime: item.SoldTime, SoldTime: types.DateFromTime(item.SoldTime),
SoldTo: item.SoldTo, SoldTo: item.SoldTo,
SoldPrice: item.SoldPrice, SoldPrice: item.SoldPrice,
SoldNotes: item.SoldNotes, SoldNotes: item.SoldNotes,
@@ -526,17 +527,17 @@ func (e *ItemsRepository) UpdateByGroup(ctx context.Context, GID uuid.UUID, data
SetModelNumber(data.ModelNumber). SetModelNumber(data.ModelNumber).
SetManufacturer(data.Manufacturer). SetManufacturer(data.Manufacturer).
SetArchived(data.Archived). SetArchived(data.Archived).
SetPurchaseTime(data.PurchaseTime). SetPurchaseTime(data.PurchaseTime.Time()).
SetPurchaseFrom(data.PurchaseFrom). SetPurchaseFrom(data.PurchaseFrom).
SetPurchasePrice(data.PurchasePrice). SetPurchasePrice(data.PurchasePrice).
SetSoldTime(data.SoldTime). SetSoldTime(data.SoldTime.Time()).
SetSoldTo(data.SoldTo). SetSoldTo(data.SoldTo).
SetSoldPrice(data.SoldPrice). SetSoldPrice(data.SoldPrice).
SetSoldNotes(data.SoldNotes). SetSoldNotes(data.SoldNotes).
SetNotes(data.Notes). SetNotes(data.Notes).
SetLifetimeWarranty(data.LifetimeWarranty). SetLifetimeWarranty(data.LifetimeWarranty).
SetInsured(data.Insured). SetInsured(data.Insured).
SetWarrantyExpires(data.WarrantyExpires). SetWarrantyExpires(data.WarrantyExpires.Time()).
SetWarrantyDetails(data.WarrantyDetails). SetWarrantyDetails(data.WarrantyDetails).
SetQuantity(data.Quantity). SetQuantity(data.Quantity).
SetAssetID(int(data.AssetID)) SetAssetID(int(data.AssetID))

View File

@@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/data/types"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -237,15 +238,15 @@ func TestItemsRepository_Update(t *testing.T) {
LabelIDs: nil, LabelIDs: nil,
ModelNumber: fk.Str(10), ModelNumber: fk.Str(10),
Manufacturer: fk.Str(10), Manufacturer: fk.Str(10),
PurchaseTime: time.Now(), PurchaseTime: types.DateFromTime(time.Now()),
PurchaseFrom: fk.Str(10), PurchaseFrom: fk.Str(10),
PurchasePrice: 300.99, PurchasePrice: 300.99,
SoldTime: time.Now(), SoldTime: types.DateFromTime(time.Now()),
SoldTo: fk.Str(10), SoldTo: fk.Str(10),
SoldPrice: 300.99, SoldPrice: 300.99,
SoldNotes: fk.Str(10), SoldNotes: fk.Str(10),
Notes: fk.Str(10), Notes: fk.Str(10),
WarrantyExpires: time.Now(), WarrantyExpires: types.DateFromTime(time.Now()),
WarrantyDetails: fk.Str(10), WarrantyDetails: fk.Str(10),
LifetimeWarranty: true, LifetimeWarranty: true,
} }

View 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
}

View File

@@ -27,7 +27,7 @@
<li <li
:class="[ :class="[
'relative cursor-default select-none py-2 pl-3 pr-9 duration-75 ease-in-out transition-colors', '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 }"> <slot name="display" v-bind="{ item: item, selected, active }">

View File

@@ -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 { export function parseDate<T>(obj: T, keys: Array<keyof T> = []): T {
const result = { ...obj }; const result = { ...obj };
[...keys, "createdAt", "updatedAt"].forEach(key => { [...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 (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(); const dt = new Date();
dt.setFullYear(1); dt.setFullYear(1);
@@ -26,11 +28,33 @@ export function parseDate<T>(obj: T, keys: Array<keyof T> = []): T {
return; return;
} }
// transform string to ensure dates are parsed as UTC dates instead of // Possible Formats
// localized time stamps // Date Only: YYYY-MM-DD
const asStr = result[key] as string; // Timestamp: 0001-01-01T00:00:00Z
const cleaned = asStr.replaceAll("-", "/").split("T")[0];
result[key] = new Date(cleaned); // 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;
} }
}); });

View File

@@ -335,6 +335,7 @@ export interface ActionAmountResult {
} }
export interface ApiSummary { export interface ApiSummary {
allowRegistration: boolean;
build: Build; build: Build;
demo: boolean; demo: boolean;
health: boolean; health: boolean;

View File

@@ -101,7 +101,6 @@
toast.success("Logged in successfully"); toast.success("Logged in successfully");
// @ts-expect-error - expires is either a date or a string, need to figure out store typing
authStore.$patch({ authStore.$patch({
token: data.token, token: data.token,
expires: data.expiresAt, expires: data.expiresAt,
@@ -214,11 +213,13 @@
</Transition> </Transition>
<div class="text-center mt-6"> <div class="text-center mt-6">
<button <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()" @click="() => toggleLogin()"
> >
{{ registerForm ? "Already a User? Login" : "Not a User? Register" }} {{ registerForm ? "Login" : "Register" }}
</button> </button>
<p v-else class="text-base-content italic text-sm">Registration Disabled</p>
</div> </div>
</div> </div>
</div> </div>