Skip to content

← All posts

Release

Existing accounts can be taken over from a media server

Media serverAccounts

New

  • Existing accounts can be taken over from a media server. Whoever already runs Plex, Jellyfin or Emby already has the people. Until now an account came into being only when somebody signed in for the first time, and with Jellyfin and Emby it did not come into being at all: neither knows an e-mail address for an account, and without one Nexview cannot tell a new person from somebody who has had an account for years. Onboarding thirty people meant creating thirty accounts by hand, setting thirty passwords and passing them on, after which each person still had to link the server in their own profile. Nexview now asks the server for its accounts and shows them as a list. Nothing is preselected, and for every row the decision is the operator's: a new account, or a link to one that already exists. Linking is never the default, because a wrong link hands whoever controls that server identity the way into somebody else's Nexview account, and that must not happen by clicking past it. What the import writes is the account and the link, in one transaction. The link is the actual work: without it there would be an account nobody can get into. Three things it deliberately does not do. It does not guess who belongs to whom: across providers there is no reliable signal, Plex knows an e-mail address while Jellyfin and Emby know none, and the same person is often named differently on two servers. It does not offer a role, so an import creates plain users and never approvers or administrators. And it does not carry quotas over to an account that is merely linked, because those were set deliberately once. For newly created accounts the limits can be set for the batch: movies, shows and storage, each as house default, unlimited or a fixed number, plus a tick that creates them inactive. The three-valued form is the one used everywhere else, and for the same reason as in 0.26.2: 0 means "may request nothing", which is not the same as "house default".
  • The list says which accounts already exist, and under which name. An entry in the assignment offers every Nexview account that has no link for this provider - not "no link at all". Someone who is already on Plex has to be offered at the Jellyfin import, or a second account is created for the same human, and Nexview cannot merge two accounts. Each entry names what it already hangs on, "jamie (Plex)" rather than "jamie", because on the second import that is the only hint there is.

Fixed

  • The block list was only visible for Plex. When an account is deleted, its media-server identity lands on a block list so that it cannot simply create itself again at the next sign-in. The list was shown for Plex only, with a reason that was correct until this release: with Jellyfin and Emby no account could come into being anyway, so a block there could not achieve anything. The import makes that assumption false. The list is now shown for all three. Reported after exactly this sequence: take over, delete, take over again, "access blocked" - and the list holding the entry was hidden. A blocked identity is refused by the import, and the row says so with a badge. Ticking it anyway and confirming once lifts the block rather than bypassing it: a block left standing would mean account and link exist while the sign-in is still refused, which is precisely the state this feature is meant to prevent. Lifting a block is written to the log.

Security

  • The watcher over the owner account stopped at a folder boundary. It checks from two sides: the route table, and the source. The source side read app/routers in full but looked outside it only for writes that go through the query layer, never for the ordinary one. Two lines in services/tickets.py were enough: a second administrator sent the owner a perfectly normal ticket, and came out of it as the only administrator left. All 2,491 tests stayed green. Both sides now use one and the same detection, over the whole backend. Fifteen places outside the routers came to light and were each examined by hand; none of them was a hole. Three are false alarms worth keeping: email, username and parent_id exist on more than one table, and a setattr can hit any object at all, so the watcher reports them rather than guess. Two of the exemptions carry a warning for whoever reads them next. children.aendern is harmless only because ChildUpdate is a closed model of six fields; give it extra="allow" some day and it becomes a write to every column of the user table. And unlocking child accounts from a ticket is harmless only because it grants a permission and never takes one away.
  • And its other half hung on seven hand-written field names. A request can name a foreign account through a field in its body, and the watcher knew seven names it might carry. Four of them (konto_id, kind_id, benutzer_id, benutzer_ids) do not exist anywhere in the schema and never matched a thing; eight that do exist were missing, among them parent_id, for_child_id and approved_by. Exactly the kind of list this watcher exists to make unnecessary. The names now come from the database itself: every column with a foreign key to users.id, plus the plural form. That still leaves the case a name cannot cover, a field called ziel or empfaenger, so a third check looks for the grab itself, db.get(User, ...) in the handler. The watcher sees 17 addresses where it saw 8.
  • secret.key was readable by everyone. Measured in the published image: -rw-r--r--, next to the database, in the directory an operator mounts from outside. It is the key the stored Radarr, Sonarr, TMDB and mail credentials are encrypted with. It is now created with 0600, and tightened to it on every start, so an installation that has been running for weeks gets there too, not only a fresh one. Honest about the size of it: this is hardening, not a wall. Whoever can read /data can read nexview.db anyway. It helps in the case the encryption was built for, where the database travels somewhere without the directory around it. The backups inside the data directory stay plain SQLite files. Only the download is encrypted, and the README and the interface now say so instead of implying otherwise.

Under the hood

  • An age filter could be lost from a list without a sound. The age check itself is well tested and was never the problem; the problem was that nothing asked whether every list still calls it. Taking the filter out of the favourites list let a twelve year old see the FSK 18 title, and not one of 2,491 tests noticed. There is now a watcher that measures the result rather than the intention: an account limited to twelve, a catalogue of three titles, and every title-serving address called once. Two assertions per address, and the second one is what makes it work: the blocked title must be absent, and the allowed one must be present. Without that, an address answering with an error would have passed without ever serving anything. The first attempt was a source scan and failed its own counter-check, because the call to the filter stays in the code and an if in front of it only makes it unreachable.
  • A renamed column loses its data without a word. Nexview has no numbered migrations; the start path compares the models with the file on disk and adds what is missing. That covers the common case and is well tested. It does not cover renaming, and it did not say so. Rename a column in the model, start as if updating: a new empty column appears, the values stay in the old one, the application shows a blank, and nothing is logged. The test suite cannot see it either, because every test starts from a fresh database. tests/schema_abdruck.json puts that in the diff, and the start now names every column the database still has and the model does not. It changes nothing on its own: deleting a column automatically is precisely the move that costs data.
  • Two click paths are now tested in a real browser. Search, open, request, find it in the list; and a child wishes for something while a parent releases it. Both assert the outcome rather than that something is visible.