Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/partial-frontend.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ jobs:
with:
fetch-depth: 0

- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
with:
node-version: lts/*

- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
with:
version: 10
Expand Down
22 changes: 14 additions & 8 deletions backend/app/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,14 +226,7 @@ func run(cfg *config.Config) error {
app.db = c
app.repos = repo.New(c, app.bus, cfg.Storage, cfg.Database.PubSubConnString, cfg.Thumbnail)

// Attachment-key escaping in fileblob only flattens paths on Windows
// (where os.PathSeparator is "\"), so the legacy-path rename is a Windows-
// only concern; skip the disk scan everywhere else.
if runtime.GOOS == "windows" {
if err := app.repos.Attachments.MigrateLegacyFlatPaths(); err != nil {
log.Error().Err(err).Msg("failed to migrate legacy attachment file paths")
}
}
migrateLegacyAttachmentPaths(app)

app.services = services.New(
app.repos,
Expand Down Expand Up @@ -356,6 +349,19 @@ func run(cfg *config.Config) error {
return runner.Start(context.Background())
}

func migrateLegacyAttachmentPaths(app *app) {
// Attachment-key escaping in fileblob only flattens paths on Windows
// (where os.PathSeparator is "\"), so the legacy-path rename is a Windows-
// only concern; skip the disk scan everywhere else.
if runtime.GOOS != "windows" {
return
}

if err := app.repos.Attachments.MigrateLegacyFlatPaths(); err != nil {
log.Error().Err(err).Msg("failed to migrate legacy attachment file paths")
}
}

// ensureAssetIDs assigns asset IDs to any entities that don't have one,
// covering locations that were migrated from the old schema.
func ensureAssetIDs(app *app) {
Expand Down
47 changes: 28 additions & 19 deletions backend/app/api/middleware_ratelimit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import (
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
)

const proxyRemoteAddr = "10.0.0.1:1234"
const (
proxyRemoteAddr = "10.0.0.1:1234"
directConnectionTestName = "DirectConnection"
proxyXRealIPTestName = "ProxyXRealIP"
rateLimitClientIP = "192.168.1.1"
)

func TestSimpleRateLimiter(t *testing.T) {
type testCase struct {
Expand All @@ -21,12 +26,12 @@ func TestSimpleRateLimiter(t *testing.T) {

tests := []testCase{
{
name: "DirectConnection",
name: directConnectionTestName,
trustProxy: false,
setupReq: func(r *http.Request, ip string) { r.RemoteAddr = ip + ":1234" },
},
{
name: "ProxyXRealIP",
name: proxyXRealIPTestName,
trustProxy: true,
setupReq: func(r *http.Request, ip string) {
r.RemoteAddr = proxyRemoteAddr // Proxy IP
Expand All @@ -48,7 +53,7 @@ func TestSimpleRateLimiter(t *testing.T) {
// Create a rate limiter that allows 3 requests per 10 seconds
limiter := newSimpleRateLimiter(3, 10*time.Second, tc.trustProxy)
t.Cleanup(func() { limiter.Stop() })
clientIP := "192.168.1.1"
clientIP := rateLimitClientIP

// Helper to get IP
getIP := func(ip string) string {
Expand Down Expand Up @@ -96,12 +101,12 @@ func TestSimpleRateLimiterRefill(t *testing.T) {

tests := []testCase{
{
name: "DirectConnection",
name: directConnectionTestName,
trustProxy: false,
setupReq: func(r *http.Request, ip string) { r.RemoteAddr = ip + ":1234" },
},
{
name: "ProxyXRealIP",
name: proxyXRealIPTestName,
trustProxy: true,
setupReq: func(r *http.Request, ip string) {
r.RemoteAddr = proxyRemoteAddr // Proxy IP
Expand All @@ -115,7 +120,7 @@ func TestSimpleRateLimiterRefill(t *testing.T) {
// Create a rate limiter that allows 2 requests per 100ms
limiter := newSimpleRateLimiter(2, 100*time.Millisecond, tc.trustProxy)
t.Cleanup(func() { limiter.Stop() })
clientIP := "192.168.1.1"
clientIP := rateLimitClientIP

// Helper to get IP
getIP := func(ip string) string {
Expand Down Expand Up @@ -157,12 +162,12 @@ func TestSimpleRateLimiterConcurrent(t *testing.T) {

tests := []testCase{
{
name: "DirectConnection",
name: directConnectionTestName,
trustProxy: false,
setupReq: func(r *http.Request, ip string) { r.RemoteAddr = ip + ":1234" },
},
{
name: "ProxyXRealIP",
name: proxyXRealIPTestName,
trustProxy: true,
setupReq: func(r *http.Request, ip string) {
r.RemoteAddr = proxyRemoteAddr // Proxy IP
Expand All @@ -175,7 +180,7 @@ func TestSimpleRateLimiterConcurrent(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
limiter := newSimpleRateLimiter(10, time.Second, tc.trustProxy)
t.Cleanup(func() { limiter.Stop() })
clientIP := "192.168.1.1"
clientIP := rateLimitClientIP

// Helper to get IP
getIP := func(ip string) string {
Expand Down Expand Up @@ -222,12 +227,12 @@ func TestSimpleRateLimiterCleanup(t *testing.T) {

tests := []testCase{
{
name: "DirectConnection",
name: directConnectionTestName,
trustProxy: false,
setupReq: func(r *http.Request, ip string) { r.RemoteAddr = ip + ":1234" },
},
{
name: "ProxyXRealIP",
name: proxyXRealIPTestName,
trustProxy: true,
setupReq: func(r *http.Request, ip string) {
r.RemoteAddr = proxyRemoteAddr // Proxy IP
Expand All @@ -250,7 +255,7 @@ func TestSimpleRateLimiterCleanup(t *testing.T) {
}

// Add entries for multiple IPs
ips := []string{"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4", "192.168.1.5"}
ips := []string{rateLimitClientIP, "192.168.1.2", "192.168.1.3", "192.168.1.4", "192.168.1.5"}
for _, ip := range ips {
limiter.allow(getIP(ip))
}
Expand Down Expand Up @@ -291,12 +296,12 @@ func TestSimpleRateLimiterCleanupPreservesActive(t *testing.T) {

tests := []testCase{
{
name: "DirectConnection",
name: directConnectionTestName,
trustProxy: false,
setupReq: func(r *http.Request, ip string) { r.RemoteAddr = ip + ":1234" },
},
{
name: "ProxyXRealIP",
name: proxyXRealIPTestName,
trustProxy: true,
setupReq: func(r *http.Request, ip string) {
r.RemoteAddr = proxyRemoteAddr // Proxy IP
Expand All @@ -317,7 +322,7 @@ func TestSimpleRateLimiterCleanupPreservesActive(t *testing.T) {
return limiter.getClientIP(req, limiter.trustProxy)
}

activeIP := "192.168.1.1"
activeIP := rateLimitClientIP
staleIP := "192.168.1.2"

// Create a stale entry
Expand Down Expand Up @@ -420,7 +425,11 @@ func TestAuthRateLimiterCleanupPreservesLocked(t *testing.T) {

lockedKey := "locked"
staleKey := "stale"
now := limiter.nowFn()
now := time.Now()
currentTime := now
limiter.nowFn = func() time.Time {
return currentTime
}

// Create a locked entry (exceed max attempts)
for i := 0; i < cfg.MaxAttempts+1; i++ {
Expand All @@ -430,8 +439,8 @@ func TestAuthRateLimiterCleanupPreservesLocked(t *testing.T) {
// Create a stale entry
limiter.record(staleKey, now, false)

// Wait for entries to be outside the window but locked entry still locked
time.Sleep(100 * time.Millisecond)
// Move outside the window while keeping the lockout active.
currentTime = now.Add(100 * time.Millisecond)

// Trigger cleanup
limiter.cleanup()
Expand Down
13 changes: 10 additions & 3 deletions backend/internal/core/services/reporting/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ var (
ErrMissingRequiredHeaders = errors.New("missing required headers `HB.location` or `HB.name`")
)

const (
homeboxFieldHeaderPrefix = "HB.field."
homeboxHeaderPrefix = "HB."
homeboxLocationHeader = "HB.location"
homeboxNameHeader = "HB.name"
)

// determineSeparator determines the separator used in the CSV file
// It returns the separator as a rune and an error if it could not be determined
//
Expand Down Expand Up @@ -80,16 +87,16 @@ func parseHeaders(headers []string) (hbHeaders map[string]int, fieldHeaders []st
hbHeaders = map[string]int{} // initialize map

for col, h := range headers {
if strings.HasPrefix(h, "HB.field.") {
if strings.HasPrefix(h, homeboxFieldHeaderPrefix) {
fieldHeaders = append(fieldHeaders, h)
}

if strings.HasPrefix(h, "HB.") {
if strings.HasPrefix(h, homeboxHeaderPrefix) {
hbHeaders[h] = col
}
}

required := []string{"HB.location", "HB.name"}
required := []string{homeboxLocationHeader, homeboxNameHeader}
if !lo.EveryBy(required, func(h string) bool {
return lo.HasKey(hbHeaders, h)
}) {
Expand Down
32 changes: 19 additions & 13 deletions backend/internal/core/services/reporting/io_sheet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ var (
customTypesImportCSV []byte
)

const (
homeboxField1Header = homeboxFieldHeaderPrefix + "1"
homeboxField2Header = homeboxFieldHeaderPrefix + "2"
homeboxField3Header = homeboxFieldHeaderPrefix + "3"
)

func TestSheet_Read(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -130,27 +136,27 @@ func Test_parseHeaders(t *testing.T) {
},
{
name: "field headers only",
rawHeaders: []string{"HB.location", "HB.name", "HB.field.1", "HB.field.2", "HB.field.3"},
rawHeaders: []string{homeboxLocationHeader, homeboxNameHeader, homeboxField1Header, homeboxField2Header, homeboxField3Header},
wantHbHeaders: map[string]int{
"HB.location": 0,
"HB.name": 1,
"HB.field.1": 2,
"HB.field.2": 3,
"HB.field.3": 4,
homeboxLocationHeader: 0,
homeboxNameHeader: 1,
homeboxField1Header: 2,
homeboxField2Header: 3,
homeboxField3Header: 4,
},
wantFieldHeaders: []string{"HB.field.1", "HB.field.2", "HB.field.3"},
wantFieldHeaders: []string{homeboxField1Header, homeboxField2Header, homeboxField3Header},
wantErr: false,
},
{
name: "mixed headers",
rawHeaders: []string{"Header 1", "HB.name", "Header 2", "HB.field.2", "Header 3", "HB.field.3", "HB.location"},
rawHeaders: []string{"Header 1", homeboxNameHeader, "Header 2", homeboxField2Header, "Header 3", homeboxField3Header, homeboxLocationHeader},
wantHbHeaders: map[string]int{
"HB.name": 1,
"HB.field.2": 3,
"HB.field.3": 5,
"HB.location": 6,
homeboxNameHeader: 1,
homeboxField2Header: 3,
homeboxField3Header: 5,
homeboxLocationHeader: 6,
},
wantFieldHeaders: []string{"HB.field.2", "HB.field.3"},
wantFieldHeaders: []string{homeboxField2Header, homeboxField3Header},
wantErr: false,
},
}
Expand Down
Loading
Loading