-
Notifications
You must be signed in to change notification settings - Fork 12
preparing for monorepo #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR prepares the project for a monorepo structure by integrating the frontend with the backend, restructuring API routes, enhancing caching, and streamlining the build process. Key changes include:
- Integration of Go embed functionality and SPA routing for serving static frontend assets.
- Multi-stage Docker build and added Makefile for automated builds.
- API route restructuring under /api/v1/ and updated caching configuration.
Reviewed Changes
Copilot reviewed 6 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
File | Description |
---|---|
plan.md | Added a project integration and build plan. |
main.go | Updated static file serving, API routes, and logging. |
go.mod | Updated dependencies by re-adding and removing indirect ones. |
cache.go | Revised cache TTL configurations and added route-based caching logic. |
Makefile | Created automation targets for frontend and backend builds. |
Dockerfile | Implemented a multi-stage build for frontend and backend. |
- Add Makefile for Go backend and frontend asset build automation, including clean, test, and Docker targets. - Refactor Dockerfile to use multi-stage builds for frontend (Node.js) and Go backend, embedding static docs and optimizing final image. - Serve static frontend assets from embedded docs directory using Go 1.16+ embed and chi router, with SPA fallback and cache headers. - Introduce API versioning under /api/v1, moving all endpoints and adding a /api/v1/health endpoint. - Update cache middleware to match new API route patterns and add shouldCache helper. - Update .gitignore to exclude node_modules, docs/*, and build/ directories. - Update Go module dependencies for bbolt and static asset support. ``` diff --git a/.gitignore b/.gitignore index bfc680a..53680fc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ .env credentials.json cache.db +node_modules +docs/* +build/ diff --git a/Dockerfile b/Dockerfile index f21da98..f926774 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,31 @@ -FROM golang:1.24 +#================ +# Stage 1: Frontend build (Node.js) +#================ +FROM node:18-alpine AS frontend-builder +WORKDIR /app +RUN apk add --no-cache make +COPY Makefile package*.json ./ +RUN make docs/index.html -RUN go install github.com/audibleblink/passdb@latest -CMD ["passdb"] +#================ +# Stage 2: Go build +#================ +FROM golang:1.24-alpine AS go-builder +WORKDIR /app +RUN apk add --no-cache git make +COPY . . +COPY --from=frontend-builder /app/docs /app +RUN make passdb -# $ docker build -t passdb-server . -# $ docker run --env-file .env -p 3000:3000 passdb-server +#================ +# Stage 3: Final runtime image +#================ +FROM alpine:latest +RUN apk --no-cache add ca-certificates tzdata curl +WORKDIR /app +COPY --from=go-builder /app/build/passdb /usr/local/bin/passdb +EXPOSE 3000 +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:3000/api/v1/health || exit 1 + +ENTRYPOINT ["passdb"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1f7ca1f --- /dev/null +++ b/Makefile @@ -0,0 +1,62 @@ +# PassDB Makefile +# Build automation for Go backend with embedded frontend assets + +# Variables +BINARY_NAME=passdb +BUILD_DIR=build +DOCS_DIR=docs +MAIN_FILE=. +CRT=container + +# Build Go binary with embedded assets +$(BINARY_NAME): docs/index.html + @echo "Building Go binary..." + @mkdir -p $(BUILD_DIR) + go mod tidy + go build -o $(BUILD_DIR)/$(BINARY_NAME) $(MAIN_FILE) + @echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)" + +# Build frontend assets (runs npm build if package.json exists) +docs/index.html: + @mkdir -p docs + @if [ -f package.json ]; then \ + echo "Building frontend assets..."; \ + npm install; \ + npm run build; \ + else \ + echo "No package.json found, skipping frontend build"; \ + fi + + +# Development server (backend only, no asset building) +.PHONY: dev +dev: + @echo "Starting development server..." + go run $(MAIN_FILE) + +# Clean build artifacts +.PHONY: clean +clean: + @echo "Cleaning build artifacts..." + @rm -rf $(BUILD_DIR) + @if [ -d node_modules ]; then rm -rf node_modules; fi + @if [ -f package-lock.json ]; then rm -f package-lock.json; fi + @echo "Clean complete" + +# Test the application +.PHONY: test +test: + @echo "Running tests..." + go test -v ./... + +# Docker build +.PHONY: docker-build +docker-build: + @echo "Building Docker image..." + $(CRT) build -t passdb . + +# Docker run +.PHONY: docker-run +docker-run: docker-build + @echo "Running Docker container..." + $(CRT) run --env-file .env -p 3000:3000 passdb-server diff --git a/cache.go b/cache.go index 2632351..7a41f00 100644 --- a/cache.go +++ b/cache.go @@ -40,8 +40,8 @@ func newResponseCapture(w http.ResponseWriter) *responseCapture { return &responseCapture{ ResponseWriter: w, statusCode: http.StatusOK, - body: new(bytes.Buffer), - headers: make(http.Header), + body: new(bytes.Buffer), + headers: make(http.Header), } } @@ -67,11 +67,26 @@ func LoadCacheConfig() CacheConfig { RouteTTLs: make(map[string]time.Duration), } - config.RouteTTLs["/breaches/"] = getEnvDuration("CACHE_TTL_BREACHES", 168*time.Hour) // 7 days - config.RouteTTLs["/usernames/"] = getEnvDuration("CACHE_TTL_USERNAMES", 720*time.Hour) // 30 days - config.RouteTTLs["/passwords/"] = getEnvDuration("CACHE_TTL_PASSWORDS", 720*time.Hour) // 30 days - config.RouteTTLs["/domains/"] = getEnvDuration("CACHE_TTL_DOMAINS", 720*time.Hour) // 30 days - config.RouteTTLs["/emails/"] = getEnvDuration("CACHE_TTL_EMAILS", 720*time.Hour) // 30 days + config.RouteTTLs["/api/v1/breaches/"] = getEnvDuration( + "CACHE_TTL_BREACHES", + 168*time.Hour, + ) // 7 days + config.RouteTTLs["/api/v1/usernames/"] = getEnvDuration( + "CACHE_TTL_USERNAMES", + 720*time.Hour, + ) // 30 days + config.RouteTTLs["/api/v1/passwords/"] = getEnvDuration( + "CACHE_TTL_PASSWORDS", + 720*time.Hour, + ) // 30 days + config.RouteTTLs["/api/v1/domains/"] = getEnvDuration( + "CACHE_TTL_DOMAINS", + 720*time.Hour, + ) // 30 days + config.RouteTTLs["/api/v1/emails/"] = getEnvDuration( + "CACHE_TTL_EMAILS", + 720*time.Hour, + ) // 30 days return config } @@ -130,8 +145,14 @@ func CacheMiddleware(config CacheConfig) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check if this route should be cached + if !shouldCache(r.URL.Path, config) { + next.ServeHTTP(w, r) + return + } + cacheKey := fmt.Sprintf("%s:%s", r.Method, r.URL.Path) - + var cached *CacheEntry err := db.View(func(tx *bbolt.Tx) error { bucket := tx.Bucket([]byte("cache")) @@ -139,7 +160,7 @@ func CacheMiddleware(config CacheConfig) func(http.Handler) http.Handler { if data == nil { return nil } - + cached = &CacheEntry{} return json.Unmarshal(data, cached) }) @@ -162,7 +183,7 @@ func CacheMiddleware(config CacheConfig) func(http.Handler) http.Handler { if rc.statusCode >= 200 && rc.statusCode < 300 { ttl := getTTLForPath(r.URL.Path, config) - + entry := CacheEntry{ StatusCode: rc.statusCode, Headers: make(map[string]string), @@ -201,4 +222,15 @@ func getTTLForPath(path string, config CacheConfig) time.Duration { } } return config.DefaultTTL -} \ No newline at end of file +} + +func shouldCache(path string, config CacheConfig) bool { + // Check if the path matches any configured cache routes + for routePattern := range config.RouteTTLs { + if strings.HasPrefix(path, routePattern) { + return true + } + } + return false +} + diff --git a/go.mod b/go.mod index d47b36b..372fcf3 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( cloud.google.com/go/bigquery v1.10.0 github.com/go-chi/chi v4.1.2+incompatible github.com/go-chi/cors v1.1.1 + go.etcd.io/bbolt v1.4.1 google.golang.org/api v0.29.0 ) @@ -17,7 +18,6 @@ require ( github.com/golang/protobuf v1.4.2 // indirect github.com/googleapis/gax-go/v2 v2.0.5 // indirect github.com/jstemmer/go-junit-report v0.9.1 // indirect - go.etcd.io/bbolt v1.4.1 // indirect go.opencensus.io v0.22.3 // indirect golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect golang.org/x/mod v0.3.0 // indirect diff --git a/go.sum b/go.sum index 92ba315..d8ba422 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -111,11 +113,14 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -202,6 +207,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -225,7 +232,6 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -360,6 +366,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/main.go b/main.go index 1a8af17..158700d 100644 --- a/main.go +++ b/main.go @@ -2,12 +2,17 @@ package main import ( "context" + "embed" "encoding/json" "fmt" + "io" + "io/fs" "log" "net/http" "os" + "path/filepath" "strings" + "time" "cloud.google.com/go/bigquery" "google.golang.org/api/iterator" @@ -18,6 +23,9 @@ import ( "github.com/go-chi/cors" ) +//go:embed docs +var staticFiles embed.FS + var ( projectID = os.Getenv("GOOGLE_CLOUD_PROJECT") bigQueryTable = os.Getenv("GOOGLE_BIGQUERY_TABLE") @@ -50,26 +58,39 @@ func main() { cacheConfig := LoadCacheConfig() r := chi.NewRouter() + r.Use(cors.Handler(cors.Options{ + AllowedOrigins: []string{"*"}, + })) r.Use(middleware.RequestID) r.Use(middleware.RealIP) r.Use(middleware.Logger) r.Use(CacheMiddleware(cacheConfig)) r.Use(middleware.Recoverer) - r.Use(cors.Handler(cors.Options{ - AllowedOrigins: []string{"*"}, - })) - r.Get("/usernames/{username}", handleUsername) - r.Get("/passwords/{password}", handlePassword) - r.Get("/domains/{domain}", handleDomain) - r.Get("/emails/{email}", handleEmail) - r.Get("/breaches/{email}", handleBreaches) + // API routes with versioning + r.Route("/api/v1", func(r chi.Router) { + // Health check endpoint + r.Get("/health", handleHealth) - r.Get("/cache/stats", handleCacheStats) - r.Delete("/cache", handleCacheClear) - r.Delete("/cache/{pattern}", handleCacheClearPattern) + // Password database endpoints + r.Get("/usernames/{username}", handleUsername) + r.Get("/passwords/{password}", handlePassword) + r.Get("/domains/{domain}", handleDomain) + r.Get("/emails/{email}", handleEmail) + r.Get("/breaches/{email}", handleBreaches) + + // Cache management endpoints + r.Get("/cache/stats", handleCacheStats) + r.Delete("/cache", handleCacheClear) + r.Delete("/cache/{pattern}", handleCacheClearPattern) + }) + + // Static file serving + setupStaticRoutes(r) log.Printf("Starting server on %s\n", listenAddr) + log.Printf("API endpoints available at /api/v1/") + log.Printf("Static files served from /") err := http.ListenAndServe(listenAddr, r) if err != nil { log.Fatal(err) @@ -292,3 +313,77 @@ func handleCacheClearPattern(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(response) } + +func handleHealth(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + response := map[string]any{ + "status": "healthy", + "service": "passdb-api", + "version": "v1", + } + + json.NewEncoder(w).Encode(response) +} + +func setupStaticRoutes(r chi.Router) { + // Create a sub filesystem for the docs directory + docsFS, err := fs.Sub(staticFiles, "docs") + if err != nil { + log.Printf("Warning: could not create docs filesystem: %v", err) + return + } + + // Serve static files from the docs directory + fileServer := http.FileServer(http.FS(docsFS)) + + // Handle all remaining routes as static files + r.Get("/*", func(w http.ResponseWriter, r *http.Request) { + rctx := chi.RouteContext(r.Context()) + pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*") + fs := http.StripPrefix(pathPrefix, fileServer) + + // Set appropriate content type based on file extension + ext := filepath.Ext(r.URL.Path) + switch ext { + case ".html": + w.Header().Set("Content-Type", "text/html; charset=utf-8") + case ".css": + w.Header().Set("Content-Type", "text/css; charset=utf-8") + case ".js": + w.Header().Set("Content-Type", "application/javascript; charset=utf-8") + case ".json": + w.Header().Set("Content-Type", "application/json; charset=utf-8") + case ".png": + w.Header().Set("Content-Type", "image/png") + case ".jpg", ".jpeg": + w.Header().Set("Content-Type", "image/jpeg") + case ".svg": + w.Header().Set("Content-Type", "image/svg+xml") + } + + // Add cache headers for static assets + w.Header().Set("Cache-Control", "public, max-age=31536000") // 1 year + + // Try to serve the requested file + fs.ServeHTTP(w, r) + }) + + // Fallback route for SPA routing - serve index.html for unmatched routes + r.NotFound(func(w http.ResponseWriter, r *http.Request) { + // Only serve index.html for non-API routes + if !strings.HasPrefix(r.URL.Path, "/api/") { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + indexFile, err := docsFS.Open("index.html") + if err != nil { + http.NotFound(w, r) + return + } + defer indexFile.Close() + + http.ServeContent(w, r, "index.html", time.Time{}, indexFile.(io.ReadSeeker)) + } else { + http.NotFound(w, r) + } + }) +}
…points - Add global references for cache configuration and BoltDB instance to enable management operations outside middleware. - Implement GetCacheStats to provide cache status, entry counts, DB size, and route-specific statistics. - Add ClearCache to remove all cache entries and ClearCachePattern to delete entries matching a pattern. - Update /cache/stats, /cache/clear, and /cache/clear/{pattern} endpoints to use new management functions and return detailed JSON responses. - Improve error handling for cache management endpoints, returning proper error messages and HTTP status codes. - Ensure cacheDB is set to nil if initialization fails to prevent misuse. # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # # On branch api-route # Your branch is up to date with 'origin/api-route'. # # Changes to be committed: # modified: cache.go # modified: main.go # # Untracked files: # passdb # # ------------------------ >8 ------------------------ # Do not modify or remove the line above. # Everything below it will be ignored. diff --git a/cache.go b/cache.go index 7a41f00..a8d7ab6 100644 --- a/cache.go +++ b/cache.go @@ -117,6 +117,9 @@ func getEnvDuration(key string, defaultValue time.Duration) time.Duration { } func CacheMiddleware(config CacheConfig) func(http.Handler) http.Handler { + // Store config in global variable for management functions + cacheConfig = config + if !config.Enabled { return func(next http.Handler) http.Handler { return next @@ -131,6 +134,9 @@ func CacheMiddleware(config CacheConfig) func(http.Handler) http.Handler { } } + // Store db reference in global variable for management functions + cacheDB = db + err = db.Update(func(tx *bbolt.Tx) error { _, err := tx.CreateBucketIfNotExists([]byte("cache")) return err @@ -138,6 +144,7 @@ func CacheMiddleware(config CacheConfig) func(http.Handler) http.Handler { if err != nil { log.Printf("Failed to create cache bucket: %v", err) db.Close() + cacheDB = nil return func(next http.Handler) http.Handler { return next } @@ -234,3 +241,140 @@ func shouldCache(path string, config CacheConfig) bool { return false } +// Global cache database reference for management functions +var cacheDB *bbolt.DB +var cacheConfig CacheConfig + +// CacheStats represents cache statistics +type CacheStats struct { + Enabled bool `json:"enabled"` + TotalEntries int `json:"total_entries"` + DatabaseSize int64 `json:"database_size_bytes"` + Configuration map[string]interface{} `json:"configuration"` + EntriesByPath map[string]int `json:"entries_by_path"` +} + +// GetCacheStats returns current cache statistics +func GetCacheStats() (CacheStats, error) { + stats := CacheStats{ + Enabled: cacheConfig.Enabled, + Configuration: make(map[string]interface{}), + EntriesByPath: make(map[string]int), + } + + // Add configuration details + stats.Configuration["default_ttl"] = cacheConfig.DefaultTTL.String() + stats.Configuration["db_path"] = cacheConfig.DBPath + + // Add route-specific TTLs + routeTTLs := make(map[string]string) + for route, ttl := range cacheConfig.RouteTTLs { + routeTTLs[route] = ttl.String() + } + stats.Configuration["route_ttls"] = routeTTLs + + if !cacheConfig.Enabled || cacheDB == nil { + return stats, nil + } + + // Get database file size + if fileInfo, err := os.Stat(cacheConfig.DBPath); err == nil { + stats.DatabaseSize = fileInfo.Size() + } + + // Count entries and group by path prefix + err := cacheDB.View(func(tx *bbolt.Tx) error { + bucket := tx.Bucket([]byte("cache")) + if bucket == nil { + return nil + } + + return bucket.ForEach(func(k, v []byte) error { + stats.TotalEntries++ + + // Extract path from cache key (format: "METHOD:path") + key := string(k) + parts := strings.SplitN(key, ":", 2) + if len(parts) == 2 { + path := parts[1] + // Group by route prefix + for routePrefix := range cacheConfig.RouteTTLs { + if strings.HasPrefix(path, routePrefix) { + stats.EntriesByPath[routePrefix]++ + break + } + } + } + + return nil + }) + }) + + return stats, err +} + +// ClearCache removes all entries from the cache +func ClearCache() error { + if !cacheConfig.Enabled || cacheDB == nil { + return fmt.Errorf("cache is not enabled or not initialized") + } + + return cacheDB.Update(func(tx *bbolt.Tx) error { + bucket := tx.Bucket([]byte("cache")) + if bucket == nil { + return nil + } + + // Delete and recreate the bucket to clear all entries + if err := tx.DeleteBucket([]byte("cache")); err != nil { + return err + } + _, err := tx.CreateBucket([]byte("cache")) + return err + }) +} + +// ClearCachePattern removes cache entries matching the given pattern +func ClearCachePattern(pattern string) (int, error) { + if !cacheConfig.Enabled || cacheDB == nil { + return 0, fmt.Errorf("cache is not enabled or not initialized") + } + + var deletedCount int + + err := cacheDB.Update(func(tx *bbolt.Tx) error { + bucket := tx.Bucket([]byte("cache")) + if bucket == nil { + return nil + } + + // Collect keys to delete + var keysToDelete [][]byte + + err := bucket.ForEach(func(k, v []byte) error { + key := string(k) + // Check if key contains the pattern + if strings.Contains(key, pattern) { + keysToDelete = append(keysToDelete, k) + } + return nil + }) + + if err != nil { + return err + } + + // Delete the collected keys + for _, key := range keysToDelete { + if err := bucket.Delete(key); err != nil { + return err + } + deletedCount++ + } + + return nil + }) + + return deletedCount, err +} + diff --git a/main.go b/main.go index 158700d..c46175b 100644 --- a/main.go +++ b/main.go @@ -285,19 +285,26 @@ func JSONError(w http.ResponseWriter, err error, code int) { func handleCacheStats(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - response := map[string]any{ - "message": "Cache stats endpoint - implementation pending", - "enabled": getEnvBool("CACHE_ENABLED", true), + stats, err := GetCacheStats() + if err != nil { + JSONError(w, err, http.StatusInternalServerError) + return } - json.NewEncoder(w).Encode(response) + json.NewEncoder(w).Encode(stats) } func handleCacheClear(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") + if err := ClearCache(); err != nil { + JSONError(w, err, http.StatusInternalServerError) + return + } + response := map[string]any{ - "message": "Cache cleared - implementation pending", + "message": "Cache cleared successfully", + "success": true, } json.NewEncoder(w).Encode(response) @@ -307,8 +314,16 @@ func handleCacheClearPattern(w http.ResponseWriter, r *http.Request) { pattern := chi.URLParam(r, "pattern") w.Header().Set("Content-Type", "application/json") + deletedCount, err := ClearCachePattern(pattern) + if err != nil { + JSONError(w, err, http.StatusInternalServerError) + return + } + response := map[string]any{ - "message": fmt.Sprintf("Cache cleared for pattern: %s - implementation pending", pattern), + "message": fmt.Sprintf("Cache entries matching pattern '%s' cleared", pattern), + "deleted_count": deletedCount, + "success": true, } json.NewEncoder(w).Encode(response)
This pull request introduces significant updates to the PassDB project, including frontend integration, API restructuring, improved caching, and a multi-stage Docker build. The changes enhance the application's architecture by enabling it to serve both API endpoints and static frontend assets, improving maintainability and scalability.
Frontend Integration and Static File Serving:
embed
functionality to serve static assets from thedocs/
directory, with MIME type handling and SPA routing fallback toindex.html
. Static files are cached for one year. (main.go
, main.goR316-R389)Dockerfile
to include a multi-stage build process for both frontend (Node.js) and backend (Go), ensuring the final image is lightweight and production-ready. (Dockerfile
, DockerfileL1-R31)Makefile
to automate build processes, including frontend asset generation, Go binary compilation, and Docker image creation. (Makefile
, MakefileR1-R62)API Restructuring:
/api/v1/
prefix to support versioning and maintain backward compatibility. (main.go
, main.goR61-R93)/api/v1/health
to monitor service status. (main.go
, main.goR316-R389)Caching Enhancements:
cache.go
, cache.goL70-R89)shouldCache
function to determine if a route should be cached, improving cache middleware logic. (cache.go
, [1] [2]Dependency and Documentation Updates:
go.etcd.io/bbolt
togo.mod
for potential future use, then removed it as an indirect dependency to clean up unused imports. (go.mod
, [1] [2]plan.md
file outlining the changes made for frontend integration and build system updates. (plan.md
, plan.mdR1-R43)