Publish current app and server
This commit is contained in:
+95
-55
@@ -2,9 +2,9 @@ package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ponzischeme89/memby/server/internal/cache"
|
||||
"github.com/ponzischeme89/memby/server/internal/emby"
|
||||
@@ -19,22 +19,22 @@ type loginRequest struct {
|
||||
}
|
||||
|
||||
type loginResponse struct {
|
||||
Token string `json:"token"`
|
||||
UserID string `json:"userId"`
|
||||
Username string `json:"username"`
|
||||
ServerID string `json:"serverId"`
|
||||
ActiveClients int `json:"activeClients,omitempty"`
|
||||
MaxClientsPerUser int `json:"maxClientsPerUser"`
|
||||
Token string `json:"token"`
|
||||
UserID string `json:"userId"`
|
||||
Username string `json:"username"`
|
||||
ServerID string `json:"serverId"`
|
||||
}
|
||||
|
||||
type authPolicyResponse struct {
|
||||
MaxClientsPerUser int `json:"maxClientsPerUser"`
|
||||
type deviceSessionResponse struct {
|
||||
DeviceID string `json:"deviceId"`
|
||||
DeviceName string `json:"deviceName"`
|
||||
ClientVersion string `json:"clientVersion,omitempty"`
|
||||
LastSeenAt time.Time `json:"lastSeenAt"`
|
||||
Current bool `json:"current"`
|
||||
}
|
||||
|
||||
func (s *Server) handleAuthPolicy(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(w, http.StatusOK, authPolicyResponse{
|
||||
MaxClientsPerUser: s.cfg.MaxClientsPerUser,
|
||||
})
|
||||
type renameDeviceRequest struct {
|
||||
DeviceName string `json:"deviceName"`
|
||||
}
|
||||
|
||||
// handleLogin exchanges Emby credentials for a gateway token.
|
||||
@@ -86,42 +86,21 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
sess := store.Session{
|
||||
TokenHash: hashToken(token),
|
||||
EmbyUserID: auth.User.ID,
|
||||
EmbyToken: auth.AccessToken,
|
||||
Username: auth.User.Name,
|
||||
ServerID: auth.ServerID,
|
||||
DeviceID: req.DeviceID,
|
||||
DeviceName: req.DeviceName,
|
||||
ClientVersion: clientVersion(r),
|
||||
ClientProtocol: clientProtocol(r),
|
||||
TokenHash: hashToken(token),
|
||||
EmbyUserID: auth.User.ID,
|
||||
EmbyToken: auth.AccessToken,
|
||||
Username: auth.User.Name,
|
||||
ServerID: auth.ServerID,
|
||||
DeviceID: req.DeviceID,
|
||||
DeviceName: req.DeviceName,
|
||||
ClientVersion: clientVersion(r),
|
||||
ClientProtocol: clientProtocol(r),
|
||||
ClientCapabilities: clientCapabilities(r),
|
||||
}
|
||||
if sess.Username == "" {
|
||||
sess.Username = req.Username
|
||||
}
|
||||
replacedHash, activeClients, err := s.store.CreateSession(
|
||||
r.Context(), sess, s.cfg.MaxClientsPerUser,
|
||||
)
|
||||
if errors.Is(err, store.ErrDeviceLimit) {
|
||||
if revokeErr := s.emby.Logout(r.Context(), emby.Credentials{
|
||||
UserID: auth.User.ID, Token: auth.AccessToken,
|
||||
DeviceID: req.DeviceID, DeviceName: req.DeviceName,
|
||||
}); revokeErr != nil {
|
||||
s.log.Warn("could not retire refused emby session", "error", revokeErr)
|
||||
}
|
||||
s.log.Warn("device allowance reached",
|
||||
"username", sess.Username,
|
||||
"active_clients", activeClients,
|
||||
"max_clients", s.cfg.MaxClientsPerUser,
|
||||
)
|
||||
writeJSON(w, http.StatusConflict, map[string]any{
|
||||
"error": "device_limit_reached",
|
||||
"message": "This account has reached its Memby device allowance.",
|
||||
"activeClients": activeClients,
|
||||
"maxClientsPerUser": s.cfg.MaxClientsPerUser,
|
||||
})
|
||||
return
|
||||
}
|
||||
replacedHash, err := s.store.CreateSession(r.Context(), sess)
|
||||
if err != nil {
|
||||
_ = s.emby.Logout(r.Context(), emby.Credentials{
|
||||
UserID: auth.User.ID, Token: auth.AccessToken,
|
||||
@@ -140,12 +119,7 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, loginResponse{
|
||||
Token: token,
|
||||
UserID: sess.EmbyUserID,
|
||||
Username: sess.Username,
|
||||
ServerID: sess.ServerID,
|
||||
ActiveClients: activeClients,
|
||||
MaxClientsPerUser: s.cfg.MaxClientsPerUser,
|
||||
Token: token, UserID: sess.EmbyUserID, Username: sess.Username, ServerID: sess.ServerID,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -161,9 +135,75 @@ func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request, sess store
|
||||
// handleSession lets the TV confirm a stored token is still good before rendering.
|
||||
func (s *Server) handleSession(w http.ResponseWriter, _ *http.Request, sess store.Session) {
|
||||
writeJSON(w, http.StatusOK, loginResponse{
|
||||
UserID: sess.EmbyUserID,
|
||||
Username: sess.Username,
|
||||
ServerID: sess.ServerID,
|
||||
MaxClientsPerUser: s.cfg.MaxClientsPerUser,
|
||||
UserID: sess.EmbyUserID, Username: sess.Username, ServerID: sess.ServerID,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleDevices(w http.ResponseWriter, r *http.Request, current store.Session) {
|
||||
sessions, err := s.store.SessionsForUser(r.Context(), current.EmbyUserID)
|
||||
if err != nil {
|
||||
s.log.Error("device list failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "could not list devices")
|
||||
return
|
||||
}
|
||||
devices := make([]deviceSessionResponse, 0, len(sessions))
|
||||
for _, sess := range sessions {
|
||||
devices = append(devices, deviceSessionResponse{
|
||||
DeviceID: sess.DeviceID, DeviceName: sess.DeviceName,
|
||||
ClientVersion: sess.ClientVersion, LastSeenAt: sess.LastSeenAt,
|
||||
Current: string(sess.TokenHash) == string(current.TokenHash),
|
||||
})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"devices": devices})
|
||||
}
|
||||
|
||||
func (s *Server) handleDeleteDevice(w http.ResponseWriter, r *http.Request, current store.Session) {
|
||||
deviceID := strings.TrimSpace(r.PathValue("deviceID"))
|
||||
if deviceID == "" {
|
||||
writeError(w, http.StatusBadRequest, "device id is required")
|
||||
return
|
||||
}
|
||||
if deviceID == current.DeviceID {
|
||||
writeError(w, http.StatusBadRequest, "sign out to remove the current device")
|
||||
return
|
||||
}
|
||||
tokenHash, err := s.store.DeleteUserDevice(r.Context(), current.EmbyUserID, deviceID)
|
||||
if err == store.ErrNotFound {
|
||||
writeError(w, http.StatusNotFound, "device not found")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
s.log.Error("device revoke failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "could not remove device")
|
||||
return
|
||||
}
|
||||
_ = s.cache.Delete(r.Context(), cache.SessionKey(hexHash(tokenHash)))
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s *Server) handleRenameDevice(w http.ResponseWriter, r *http.Request, current store.Session) {
|
||||
deviceID := strings.TrimSpace(r.PathValue("deviceID"))
|
||||
var req renameDeviceRequest
|
||||
if deviceID == "" || json.NewDecoder(http.MaxBytesReader(w, r.Body, 2<<10)).Decode(&req) != nil {
|
||||
writeError(w, http.StatusBadRequest, "device id and name are required")
|
||||
return
|
||||
}
|
||||
req.DeviceName = strings.TrimSpace(req.DeviceName)
|
||||
if req.DeviceName == "" {
|
||||
writeError(w, http.StatusBadRequest, "device name is required")
|
||||
return
|
||||
}
|
||||
if len([]rune(req.DeviceName)) > 80 {
|
||||
writeError(w, http.StatusBadRequest, "device name is too long")
|
||||
return
|
||||
}
|
||||
if err := s.store.RenameUserDevice(r.Context(), current.EmbyUserID, deviceID, req.DeviceName); err == store.ErrNotFound {
|
||||
writeError(w, http.StatusNotFound, "device not found")
|
||||
return
|
||||
} else if err != nil {
|
||||
s.log.Error("device rename failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "could not rename device")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user