0.2.45 - Advanced analytics, logout old versions

This commit is contained in:
ponzischeme89
2026-08-10 20:24:22 +12:00
parent 63f0768507
commit 56c1167382
32 changed files with 1125 additions and 452 deletions
+39 -3
View File
@@ -126,7 +126,7 @@ func (s *Server) Routes() http.Handler {
// once, without the gate ever touching health checks or the admin page.
v1 := http.NewServeMux()
v1.HandleFunc("POST /v1/auth/login", s.handleLogin)
v1.HandleFunc("POST /v1/auth/login", s.requireSupportedClient(s.handleLogin))
v1.Handle("POST /v1/auth/logout", s.authed(s.handleLogout))
v1.Handle("GET /v1/auth/session", s.authed(s.handleSession))
v1.Handle("GET /v1/auth/devices", s.authed(s.handleDevices))
@@ -196,6 +196,7 @@ func (s *Server) Routes() http.Handler {
v1.Handle("POST /v1/playback/{phase}", s.authed(s.handlePlaybackReport))
v1.Handle("POST /v1/analytics/rows", s.authed(s.handleRowAnalytics))
v1.Handle("POST /v1/analytics/events", s.authed(s.handleJourneyAnalytics))
v1.Handle("GET /v1/images/{itemId}/{imageType}", s.authed(s.handleImage))
@@ -204,8 +205,8 @@ func (s *Server) Routes() http.Handler {
mux.HandleFunc("GET /readyz", s.handleReady)
// Update policy is app-scoped, not user-scoped. Keep it outside authentication and
// maintenance so a fresh install, a signed-out TV, and a retired build can all learn
// whether the server requires an update without touching a viewer session.
mux.HandleFunc("GET /v1/update", s.handleUpdate)
// whether the server requires an update. A valid session enriches only its log context.
mux.Handle("GET /v1/update", s.identifyOptionalSession(http.HandlerFunc(s.handleUpdate)))
// Exact route outside the maintenance gate: signed-in clients poll this lightweight
// status even while every normal /v1 operation is deliberately unavailable.
mux.Handle("GET /v1/status", s.authed(s.handleServiceStatus))
@@ -230,6 +231,21 @@ func (s *Server) Routes() http.Handler {
type authedFunc func(http.ResponseWriter, *http.Request, store.Session)
// identifyOptionalSession gives public routes the viewer and television attached to a
// valid bearer token without turning authentication into a condition of access. The
// update check must remain reachable before sign-in, but an offer made to a signed-in
// client should still say whose session is affected in the logs.
func (s *Server) identifyOptionalSession(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if token := bearerToken(r); token != "" {
if sess, err := s.sessionFor(r.Context(), token); err == nil {
identify(r.Context(), sess)
}
}
next.ServeHTTP(w, r)
})
}
// authed resolves the bearer token to a session before running h.
//
// Images are also accepted with a `t=` query parameter: Coil builds plain URLs from the
@@ -253,6 +269,26 @@ func (s *Server) authed(h authedFunc) http.Handler {
}
sess = s.captureClientIdentity(r, sess)
identify(r.Context(), sess)
decision := s.updateDecision(r)
if mustRetireForUpdate(decision, clientVersion(r)) {
// Mirror an ordinary sign-out closely enough that this token cannot be restored
// from either database or Redis. The 401 is intentional: every supported client
// treats it as authoritative and removes the rejected local profile.
if err := s.store.DeleteSession(r.Context(), sess.TokenHash); err != nil {
s.loggerFor(r.Context()).Error("required-update session delete failed", "error", err)
}
_ = s.cache.Delete(r.Context(), cache.SessionKey(hexHash(sess.TokenHash)))
_ = s.cache.InvalidateUser(r.Context(), sess.EmbyUserID)
s.loggerFor(r.Context()).Info("signed out for required update",
"device_id", sess.DeviceID,
"from", clientLogValue(clientVersion(r)),
"minimum", forcedUpdateFloor,
"to", decision.Version,
)
w.Header().Set("X-Memby-Update-Required", decision.Version)
writeError(w, http.StatusUnauthorized, "Memby must be updated before signing in again")
return
}
h(w, r, sess)
})
}