source_api_userAuth.bs

' needed for SignOut() and ServerInfo()
import "pkg:/source/api/ApiClient.bs"
import "pkg:/source/utils/misc.bs"
import "pkg:/source/utils/session.bs"

function getToken(username as string, password as string)
  json = GetApi().AuthenticateByName(username, password)
  return userDataFromAuthResult(json)
end function

' Turn a Jellyfin AuthenticationResult into the UserData node user.Login() expects,
' registering the user in `available_users` on the way.
'
' Shared by the two flows that mint a token — password sign-in (getToken above) and
' Quick Connect (loginRouter.onQuickConnectAuthenticated). Quick Connect used to build its
' own node AND then re-fetch the user via AboutMe(); the re-fetch was redundant,
' because AuthenticationResult.User and GET /Users/{id} return the identical UserDto
' (diffed against Jellyfin 10.11.11 — same 12 keys, differing only in LastLoginDate /
' LastActivityDate, neither of which this app reads).
'
' The Quick Connect flow guards the SAME payload one layer up, on the render thread
' (quickConnectHasSession in source/utils/quickConnect.bs), because this side has
' nowhere left to report a failure to. The two are deliberate duplicates rather
' than one shared helper — they sit on opposite threads and answer different
' questions ("can I build a node?" vs "can I tell the user?") — so if what counts
' as a usable session ever changes, change both.
'
' @param json - AuthenticationResult from AuthenticateByName / AuthenticateWithQuickConnect
' @returns a UserData node, or invalid when the response carries no usable session
function userDataFromAuthResult(json as dynamic) as dynamic
  if not isValid(json) then return invalid
  if not isValidAndNotEmpty(json.AccessToken) then return invalid
  if not isValid(json.User) then return invalid

  userdata = CreateObject("roSGNode", "UserData")
  userdata.json = json
  userdata.callFunc("saveToRegistry")

  return userdata
end function

function AboutMe(id = "" as string)
  if id = ""
    globalUser = m.global.user
    if isValid(globalUser.id) and globalUser.id <> ""
      id = globalUser.id
    else
      return invalid
    end if
  end if

  return GetApi().GetUser(id)
end function

' #666/#728: stop the ws:// remote-control receiver AND the socket child it published — ends the
' listener thread and releases the socket's Task thread. Split out of SignOut so it can be unit
' tested: SignOut's remaining body writes real (non-`test-`) registry sections and reaches for the
' router and the session, none of which a Rooibos suite can stand up, but this block is pure node
' manipulation. See tests/source/unit/api/stopRemoteControlReceiver.spec.bs.
'
' Parent first (so a receiver still winding down can't spawn a replacement socket), then the
' published child — STOP kills the receiver mid-loop, so its own closeSocket() teardown never runs.
' STOP is not a join: the receiver can still be mid-connectAndPump here, so a child created but not
' yet published to socketNode is missed. That window is bounded, not permanent — the vendored socket
' loop self-exits once its connection reaches CLOSED.
sub StopRemoteControlReceiver()
  remoteControlTask = m.global.remoteControlTask
  if not isValid(remoteControlTask) then return

  remoteControlTask.control = "STOP"
  ' Snapshot before dotting into it. Re-reading the field for the test and again for the write
  ' would race the receiver's own closeSocket(), which clears it — a dot on invalid crashes the
  ' caller. (Each dot on a render-thread-owned node is a separate rendezvous, too.)
  socketNode = remoteControlTask.socketNode
  if isValid(socketNode) then socketNode.control = "STOP"
  remoteControlTask.socketNode = invalid
end sub

sub SignOut(deleteSavedEntry = true as boolean)
  ' This is the single logout + server-switch chokepoint (a server switch runs SignOut(false) via
  ' replayRoute.performServerSwitch), so the socket never survives a session teardown.
  StopRemoteControlReceiver()

  if deleteSavedEntry
    unsetUserSetting("authToken")
    unsetUserSetting("username")
  end if
  unsetSetting("active_user")
  user.Logout()
  m.global.sceneManager.currentUser = ""
  ' getActiveView() resolves the routed view; guard isValid because sign-out via the Home menu
  ' tears the router down first (resetRouter → no active view).
  group = getActiveView()
  if isValid(group) then group.isOptionsAvailable = false
end sub

function AvailableUsers()
  users = parseJson(getSetting("available_users", "[]"))
  return users
end function

function ServerInfo()
  return server.Discover()
end function

function GetPublicUsers()
  return GetApi().GetPublicUsers()
end function