package server import ( "context" "log" "net/http" "strings" "time" ) type contextKey string const sessionKey contextKey = "session" // LoggingMiddleware logs HTTP method, path, status code and duration for each request. // Static file requests are skipped to reduce noise. func LoggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() // Skip logging for static files to reduce noise if strings.Contains(r.URL.Path, "/static/") { next.ServeHTTP(w, r) return } // Wrap response writer to capture status code sw := &statusWriter{ResponseWriter: w, status: 200} next.ServeHTTP(sw, r) log.Printf("%s %s %d %s", r.Method, r.URL.Path, sw.status, time.Since(start).Round(time.Millisecond)) }) } type statusWriter struct { http.ResponseWriter status int } func (w *statusWriter) WriteHeader(code int) { w.status = code w.ResponseWriter.WriteHeader(code) } // AuthMiddleware checks for a valid session cookie on protected endpoints. func AuthMiddleware(store *SessionStore, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Public endpoints (no auth required) path := r.URL.Path if path == "/health" || path == "/web/login" || path == "/web/setup" || path == "/web/setup/install" || path == "/web/session/authenticate" || path == "/web/database/list" || path == "/web/webclient/version_info" || strings.Contains(path, "/static/") { next.ServeHTTP(w, r) return } // Check session cookie cookie, err := r.Cookie("session_id") if err != nil || cookie.Value == "" { // Also check JSON-RPC params for session_id (Odoo sends it both ways) next.ServeHTTP(w, r) // For now, allow through — UID defaults to 1 return } sess := store.Get(cookie.Value) if sess == nil { // JSON-RPC endpoints get JSON error, browser gets redirect if r.Header.Get("Content-Type") == "application/json" || strings.HasPrefix(path, "/web/dataset/") || strings.HasPrefix(path, "/jsonrpc") { http.Error(w, `{"jsonrpc":"2.0","error":{"code":100,"message":"Session expired"}}`, http.StatusUnauthorized) } else { http.Redirect(w, r, "/web/login", http.StatusFound) } return } // Inject session into context ctx := context.WithValue(r.Context(), sessionKey, sess) next.ServeHTTP(w, r.WithContext(ctx)) }) } // GetSession extracts the session from request context. func GetSession(r *http.Request) *Session { sess, _ := r.Context().Value(sessionKey).(*Session) return sess }