- Improved auto-generated list/form/search views with priority fields, two-column form layout, statusbar widget, notebook for O2M fields - Enhanced fields_get with currency_field, compute, related metadata - Fixed session handling: handleSessionInfo/handleSessionCheck use real session from cookie instead of hardcoded values - Added read_progress_bar and activity_format RPC stubs - Improved bootstrap translations with lang_parameters - Added "contacts" to session modules list Server starts successfully: 14 modules, 93 models, 378 XML templates, 503 JS modules transpiled — all from local frontend/ directory. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
91 lines
2.6 KiB
Go
91 lines
2.6 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/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
|
|
}
|