Skip to content

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
.env
credentials.json
cache.db
node_modules
docs/*
build/
34 changes: 29 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
62 changes: 62 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
198 changes: 187 additions & 11 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}

Expand All @@ -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
}
Expand Down Expand Up @@ -102,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
Expand All @@ -116,30 +134,40 @@ 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
})
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
}
}

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"))
data := bucket.Get([]byte(cacheKey))
if data == nil {
return nil
}

cached = &CacheEntry{}
return json.Unmarshal(data, cached)
})
Expand All @@ -162,7 +190,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),
Expand Down Expand Up @@ -201,4 +229,152 @@ func getTTLForPath(path string, config CacheConfig) time.Duration {
}
}
return config.DefaultTTL
}
}

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
}

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

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand All @@ -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
Expand Down
Loading