From 2d754c5f21d32b55bfafd37541504c314ffb4e7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A2=A8=E9=9B=AA?= Date: Mon, 17 Aug 2026 08:44:24 +0800 Subject: [PATCH 1/3] fix(build): embed fresh frontend assets in all build targets --- Makefile | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 1fd7757..aaba363 100644 --- a/Makefile +++ b/Makefile @@ -7,22 +7,28 @@ GIT_COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown) BUILD_TIME ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ) LDFLAGS := -s -w -X main.version=$(VERSION) -X main.gitCommit=$(GIT_COMMIT) -X main.buildTime=$(BUILD_TIME) -.PHONY: build build-linux build-linux-static release clean +.PHONY: build frontend-assets build-linux build-linux-static release clean -build: - mkdir -p $(DIST_DIR) +# Build the web UI and copy the complete Vite output into the directory used +# by go:embed. Every binary target depends on this so standalone Linux builds +# never ship with stale or missing frontend assets. +frontend-assets: cd $(FRONTEND_DIR) && npm install && npm run build rm -rf $(BACKEND_DIR)/cmd/server/web/dist + mkdir -p $(BACKEND_DIR)/cmd/server/web cp -R $(FRONTEND_DIR)/dist $(BACKEND_DIR)/cmd/server/web/dist + +build: frontend-assets + mkdir -p $(DIST_DIR) cd $(BACKEND_DIR) && go build -trimpath -ldflags "$(LDFLAGS)" -o ../$(DIST_DIR)/$(APP_NAME) ./cmd/server -build-linux: +build-linux: frontend-assets mkdir -p $(DIST_DIR) cd $(BACKEND_DIR) && GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -trimpath -ldflags "$(LDFLAGS)" -o ../$(DIST_DIR)/$(APP_NAME)-linux-amd64 ./cmd/server cd $(BACKEND_DIR) && GOOS=linux GOARCH=arm64 CGO_ENABLED=1 go build -trimpath -ldflags "$(LDFLAGS)" -o ../$(DIST_DIR)/$(APP_NAME)-linux-arm64 ./cmd/server cd $(BACKEND_DIR) && GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 go build -trimpath -ldflags "$(LDFLAGS)" -o ../$(DIST_DIR)/$(APP_NAME)-linux-armv7 ./cmd/server -build-linux-static: +build-linux-static: frontend-assets mkdir -p $(DIST_DIR) cd $(BACKEND_DIR) && GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -tags sqlite_modernc -trimpath -ldflags "$(LDFLAGS)" -o ../$(DIST_DIR)/$(APP_NAME)-linux-amd64-static ./cmd/server cd $(BACKEND_DIR) && GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -tags sqlite_modernc -trimpath -ldflags "$(LDFLAGS)" -o ../$(DIST_DIR)/$(APP_NAME)-linux-arm64-static ./cmd/server From 0c5cd3d924a3154342e7661ee87496eab505b099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A2=A8=E9=9B=AA?= Date: Mon, 17 Aug 2026 08:44:31 +0800 Subject: [PATCH 2/3] fix(auth): format bearer token parser and claim metadata --- backend/internal/auth/jwt.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/backend/internal/auth/jwt.go b/backend/internal/auth/jwt.go index 5db8d7f..258376c 100644 --- a/backend/internal/auth/jwt.go +++ b/backend/internal/auth/jwt.go @@ -11,10 +11,10 @@ import ( // JWTClaims represents the authenticated administrator identity. type JWTClaims struct { - UserID uint `json:"user_id"` - Username string `json:"username"` - Role string `json:"role"` - SessionVersion uint + UserID uint `json:"user_id"` + Username string `json:"username"` + Role string `json:"role"` + SessionVersion uint `json:"session_version"` ExpiresAt time.Time `json:"expires_at"` } @@ -51,7 +51,7 @@ func ParseToken(secret, tokenString string) (*JWTClaims, error) { return []byte(secret), nil }, jwt.WithValidMethods([]string{"HS256"})) if err != nil { - return nil, fmt.Errorf("invalid or expired token") + return nil, errors.New("invalid or expired token") } claims, ok := token.Claims.(jwt.MapClaims) if !ok || !token.Valid { @@ -84,9 +84,9 @@ func ParseToken(secret, tokenString string) (*JWTClaims, error) { // TokenFromRequest extracts the Bearer token from the Authorization header. func TokenFromRequest(authHeader string) string { -const prefix = "Bearer " -if strings.HasPrefix(authHeader, prefix) { -return strings.TrimSpace(authHeader[len(prefix):]) -} -return "" + const prefix = "Bearer " + if strings.HasPrefix(authHeader, prefix) { + return strings.TrimSpace(authHeader[len(prefix):]) + } + return "" } From 1591273ad9d2442344e595360dba53e3a369a7ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A2=A8=E9=9B=AA?= Date: Mon, 17 Aug 2026 08:44:38 +0800 Subject: [PATCH 3/3] test(auth): cover bearer token extraction and JWT round trip --- backend/internal/auth/jwt_test.go | 45 +++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 backend/internal/auth/jwt_test.go diff --git a/backend/internal/auth/jwt_test.go b/backend/internal/auth/jwt_test.go new file mode 100644 index 0000000..e06dce3 --- /dev/null +++ b/backend/internal/auth/jwt_test.go @@ -0,0 +1,45 @@ +package auth + +import ( + "testing" + "time" +) + +func TestTokenFromRequest(t *testing.T) { + tests := []struct { + name string + header string + want string + }{ + {name: "bearer", header: "Bearer abc.def.ghi", want: "abc.def.ghi"}, + {name: "bearer with surrounding whitespace", header: "Bearer abc", want: "abc"}, + {name: "wrong scheme", header: "Basic abc", want: ""}, + {name: "empty", header: "", want: ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := TokenFromRequest(tt.header); got != tt.want { + t.Fatalf("TokenFromRequest(%q) = %q, want %q", tt.header, got, tt.want) + } + }) + } +} + +func TestJWTTokenRoundTrip(t *testing.T) { + secret := "test-secret" + token, expiresAt, err := GenerateToken(secret, 42, "admin", "admin", 7, time.Hour) + if err != nil { + t.Fatalf("GenerateToken() error = %v", err) + } + if expiresAt.Before(time.Now()) { + t.Fatal("generated token is already expired") + } + + claims, err := ParseToken(secret, token) + if err != nil { + t.Fatalf("ParseToken() error = %v", err) + } + if claims.UserID != 42 || claims.Username != "admin" || claims.Role != "admin" || claims.SessionVersion != 7 { + t.Fatalf("unexpected claims: %+v", claims) + } +}