ZDDC/zddc/internal/handler/middleware.go
ZDDC ea385b5366 Initial commit
ZDDC — Zero Day Document Control. A file-naming convention plus five
single-file HTML tools (archive, transmittal, classifier, mdedit,
landing) and an optional Go HTTP server (zddc-server) with ACL and a
virtual archive index. Self-contained, offline-capable, dependency-free.

See README.md for an overview, AGENTS.md and ARCHITECTURE.md for the
build/release/architecture detail, bootstrap/README.md for the
two-level deployment install pattern, and zddc/README.md for the
HTTP server.
2026-04-27 11:05:47 -05:00

90 lines
2.4 KiB
Go

package handler
import (
"context"
"net/http"
"time"
"codeberg.org/VARASYS/ZDDC/zddc/internal/config"
"log/slog"
)
type contextKey string
// EmailKey is the context key for the authenticated user's email.
const EmailKey contextKey = "email"
// ACLMiddleware extracts the user email from the configured header and stores
// it in the request context. It does NOT enforce ACL itself — each handler
// performs its own ACL check via zddc.EffectivePolicy / zddc.AllowedWithChain.
func ACLMiddleware(cfg config.Config, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
email := r.Header.Get(cfg.EmailHeader)
ctx := context.WithValue(r.Context(), EmailKey, email)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// EmailFromContext extracts the user email from the request context.
func EmailFromContext(r *http.Request) string {
if v, ok := r.Context().Value(EmailKey).(string); ok {
return v
}
return ""
}
// responseWriter wraps http.ResponseWriter to capture status code and bytes written.
type responseWriter struct {
http.ResponseWriter
status int
bytes int
wrote bool
}
// WriteHeader records the status code and writes it to the underlying ResponseWriter.
func (rw *responseWriter) WriteHeader(code int) {
rw.status = code
rw.wrote = true
rw.ResponseWriter.WriteHeader(code)
}
// Write records the bytes written and writes to the underlying ResponseWriter.
func (rw *responseWriter) Write(b []byte) (int, error) {
n, err := rw.ResponseWriter.Write(b)
rw.bytes += n
return n, err
}
// AccessLogMiddleware logs a structured line per HTTP request after the response is written.
func AccessLogMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Capture request start time
start := time.Now()
// Wrap the ResponseWriter
wrapped := &responseWriter{ResponseWriter: w, status: 200}
// Serve the request
next.ServeHTTP(wrapped, r)
// Calculate duration
durationMs := int(time.Since(start).Milliseconds())
// Get email from context
email := EmailFromContext(r)
if email == "" {
email = "anonymous"
}
// Log access
slog.Info("access",
"ts", start.Format(time.RFC3339),
"email", email,
"method", r.Method,
"path", r.URL.Path,
"status", wrapped.status,
"bytes", wrapped.bytes,
"duration_ms", durationMs,
)
})
}