0.1.38 gateway
This commit is contained in:
@@ -3,10 +3,13 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ponzischeme89/memby/server/internal/adminevents"
|
||||
"github.com/ponzischeme89/memby/server/internal/cache"
|
||||
"github.com/ponzischeme89/memby/server/internal/emby"
|
||||
"github.com/ponzischeme89/memby/server/internal/store"
|
||||
@@ -82,10 +85,47 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
"username", req.Username, "device", req.DeviceName, "device_id", req.DeviceID,
|
||||
"reason", "emby refused the credentials",
|
||||
)
|
||||
// A refused attempt has no verified identity, so it carries the name that was
|
||||
// typed and no user id. It is recorded precisely because a run of these against
|
||||
// one name is the thing worth noticing, and nothing else in the gateway keeps it.
|
||||
s.recordLogin(r, store.LoginEvent{
|
||||
Username: req.Username,
|
||||
DeviceID: req.DeviceID,
|
||||
DeviceName: req.DeviceName,
|
||||
Success: false,
|
||||
Method: store.LoginMethodPassword,
|
||||
// Emby's reason is deliberately not carried through: it distinguishes
|
||||
// "no such user" from "wrong password", which is more than an operator's
|
||||
// console should restate about somebody else's failed attempt.
|
||||
FailureReason: "credentials refused",
|
||||
})
|
||||
s.publishAdmin(r.Context(), adminevents.Event{
|
||||
Type: adminevents.TypeLoginFailed,
|
||||
Severity: adminevents.SeverityWarning,
|
||||
Title: "Sign-in refused",
|
||||
Summary: fmt.Sprintf("%s was refused on %s",
|
||||
displayName(req.Username), displayName(req.DeviceName)),
|
||||
Actor: req.Username, Target: req.DeviceName,
|
||||
Link: "/admin/logins",
|
||||
Metadata: adminevents.Meta(map[string]any{
|
||||
"deviceId": req.DeviceID, "ip": requestClientIP(r),
|
||||
}),
|
||||
})
|
||||
writeError(w, http.StatusUnauthorized, "sign-in failed")
|
||||
return
|
||||
}
|
||||
|
||||
// Asked before the attempt is recorded, so this sign-in cannot answer for itself:
|
||||
// "new device registered" is only distinguishable from every later sign-in by the
|
||||
// same set if the history is consulted while it still predates this one.
|
||||
knownDevice, lookupErr := s.store.DeviceHasLoggedIn(r.Context(), auth.User.ID, req.DeviceID)
|
||||
if lookupErr != nil {
|
||||
s.loggerFor(r.Context()).Warn("device history lookup failed", "error", lookupErr)
|
||||
// Assume known. Announcing a device as new because a query failed is a claim; not
|
||||
// announcing one is a missed line.
|
||||
knownDevice = true
|
||||
}
|
||||
|
||||
token, err := newToken()
|
||||
if err != nil {
|
||||
s.log.Error("token generation failed", "error", err)
|
||||
@@ -143,6 +183,45 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
"replaced_session", len(created.ReplacedHash) > 0,
|
||||
)
|
||||
|
||||
address := requestClientIP(r)
|
||||
s.recordLogin(r, store.LoginEvent{
|
||||
EmbyUserID: sess.EmbyUserID, Username: sess.Username,
|
||||
DeviceID: sess.DeviceID, DeviceName: sess.DeviceName,
|
||||
ClientVersion: sess.ClientVersion, ClientProtocol: sess.ClientProtocol,
|
||||
Success: true, Method: store.LoginMethodPassword, NewDevice: !knownDevice,
|
||||
})
|
||||
// A television arriving for the first time and one signing in again are the same
|
||||
// request and different news, which is why they are different event types rather than
|
||||
// one type with a flag: an operator subscribing a Discord channel to new devices is
|
||||
// asking for the rare one, and would not want the other.
|
||||
if knownDevice {
|
||||
s.publishAdmin(r.Context(), adminevents.Event{
|
||||
Type: adminevents.TypeLogin,
|
||||
Title: "Signed in",
|
||||
Summary: fmt.Sprintf("%s signed in on %s",
|
||||
displayName(sess.Username), displayName(sess.DeviceName)),
|
||||
Actor: sess.Username, Target: sess.DeviceName,
|
||||
Link: "/admin/devices/" + url.PathEscape(sess.DeviceID),
|
||||
Metadata: adminevents.Meta(map[string]any{
|
||||
"deviceId": sess.DeviceID, "userId": sess.EmbyUserID,
|
||||
"ip": address, "version": sess.ClientVersion,
|
||||
}),
|
||||
})
|
||||
} else {
|
||||
s.publishAdmin(r.Context(), adminevents.Event{
|
||||
Type: adminevents.TypeDeviceRegistered,
|
||||
Title: "New device registered",
|
||||
Summary: fmt.Sprintf("%s signed in on %s for the first time",
|
||||
displayName(sess.Username), displayName(sess.DeviceName)),
|
||||
Actor: sess.Username, Target: sess.DeviceName,
|
||||
Link: "/admin/devices/" + url.PathEscape(sess.DeviceID),
|
||||
Metadata: adminevents.Meta(map[string]any{
|
||||
"deviceId": sess.DeviceID, "userId": sess.EmbyUserID,
|
||||
"ip": address, "version": sess.ClientVersion,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, loginResponse{
|
||||
Token: token, UserID: sess.EmbyUserID, Username: sess.Username, ServerID: sess.ServerID,
|
||||
})
|
||||
@@ -155,6 +234,15 @@ func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request, sess store
|
||||
_ = s.cache.Delete(r.Context(), cache.SessionKey(hexHash(sess.TokenHash)))
|
||||
_ = s.cache.InvalidateUser(r.Context(), sess.EmbyUserID)
|
||||
s.loggerFor(r.Context()).Info("signed out", "device_id", sess.DeviceID)
|
||||
s.publishAdmin(r.Context(), adminevents.Event{
|
||||
Type: adminevents.TypeLogout,
|
||||
Title: "Signed out",
|
||||
Summary: fmt.Sprintf("%s signed out on %s",
|
||||
displayName(sess.Username), displayName(sess.DeviceName)),
|
||||
Actor: sess.Username, Target: sess.DeviceName,
|
||||
Link: "/admin/devices/" + url.PathEscape(sess.DeviceID),
|
||||
Metadata: adminevents.Meta(map[string]any{"deviceId": sess.DeviceID}),
|
||||
})
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -212,6 +300,16 @@ func (s *Server) handleDeleteDevice(w http.ResponseWriter, r *http.Request, curr
|
||||
// A device disappearing from a household is worth a line: the next thing that TV
|
||||
// reports is a sign-in, and the two together explain each other.
|
||||
s.loggerFor(r.Context()).Info("device signed out remotely", "removed_device_id", deviceID)
|
||||
s.publishAdmin(r.Context(), adminevents.Event{
|
||||
Type: adminevents.TypeDeviceRemoved,
|
||||
Severity: adminevents.SeverityWarning,
|
||||
Title: "Device removed",
|
||||
Summary: fmt.Sprintf("%s removed a device from their account",
|
||||
displayName(current.Username)),
|
||||
Actor: current.Username, Target: deviceID,
|
||||
Link: "/admin/devices",
|
||||
Metadata: adminevents.Meta(map[string]any{"deviceId": deviceID}),
|
||||
})
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -289,5 +387,14 @@ func (s *Server) handleRenameDevice(w http.ResponseWriter, r *http.Request, curr
|
||||
s.loggerFor(r.Context()).Info("device renamed",
|
||||
"renamed_device_id", deviceID, "new_name", req.DeviceName,
|
||||
)
|
||||
s.publishAdmin(r.Context(), adminevents.Event{
|
||||
Type: adminevents.TypeDeviceRenamed,
|
||||
Title: "Device renamed",
|
||||
Summary: fmt.Sprintf("%s renamed a device to %s",
|
||||
displayName(current.Username), req.DeviceName),
|
||||
Actor: current.Username, Target: req.DeviceName,
|
||||
Link: "/admin/devices/" + url.PathEscape(deviceID),
|
||||
Metadata: adminevents.Meta(map[string]any{"deviceId": deviceID}),
|
||||
})
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user