Files
goodie/pkg/server/middleware.go
Marc cc823d3310 Fix navbar: SVG logos, Settings menu, Logout handler
- Image handler now returns SVG placeholders for company logo and user
  avatars instead of broken 1x1 PNG (fixes yellow border in navbar)
- Supports both query-param (?model=&field=) and path-style URLs
  (/web/image/res.partner/2/avatar_128)
- Added Settings app menu with Users & Technical sub-menus
- Added actions for Settings (company form), Users list, Sequences
- Added /web/session/logout handler that clears session + cookie
- Added logout to middleware whitelist

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 01:31:37 +02:00

90 lines
2.5 KiB
Go

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/session/authenticate" ||
path == "/web/session/logout" ||
strings.HasPrefix(path, "/web/database/") ||
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
}