Skip to content

v1.8: multi-user, API tokens, audit log - #337

Merged
demigodmode merged 33 commits into
mainfrom
release/1.8
Nov 8, 2026
Merged

demigodmode merged 33 commits into
mainfrom
release/1.8

Conversation

@demigodmode

Copy link
Copy Markdown
Owner

This is 1.8. The big one is multi-user: admin and member roles, the Account page, search-only API tokens for scripts, and an audit log. There's also a reset-password command for when you get locked out.

Some fixes went in along the way. Sign-in rate limiting could be bypassed with a fake X-Forwarded-For header, and the fallback session key was hardcoded. Those two are security fixes and have their own commit. The health check could hang forever when Meili stopped responding, EXIF exposure/lens/GPS was mostly missing, and --json output from the CLI could break when piped. Plus some document page polish: a pager under long text, the metadata card collapsed by default, the extension always shown, and readable task checkboxes.

Upgrading from 1.7 needs nothing if SESSION_SECRET is set. Without it, people get signed out once. The changelog has the full list.

closes #332, closes #333, closes #334, closes #335, closes #336

test_route_access fails if a new route forgets. Also HealthResponse was
stale, so fixed that while I was there.
Migration makes existing users admins. Members get 403 on admin routes
and only see source names, and the admin pages are hidden for them.
The token carries a session version now so sessions can be ended later.
They're signed with the same key as sessions, and get_current_user never
checked what a token was for, so a 60s download token was accepted as a
full login. Session tokens now say purpose=session and anything else gets
401. Tokens from before this have no purpose and still work.
New users get a temporary password and can't do anything until they
change it, except sign out. Changing your password ends your other
sessions and any download links you made. There's an Account page for
it, and login records last_login_at.
Run it with docker exec. It switches to the onesearch user first so it
doesn't leave root-owned sqlite files behind.
Change role, disable/enable, reset password, delete. You can't do any of
that to yourself, and the last active admin can't be removed. That check
runs under a write lock now, so two admins demoting each other at once
can't leave nobody in charge. begin_immediate moved to app/db/locks.py.
Make them on the Account page. They start with os_, only the hash is
kept, and they get 403 on anything that isn't search or opening a
document. Works with onesearch login --token as is.
The old one got clipped on the last row and dropped focus on close.
Records sign-ins (failed ones too, with the name tried and the IP) and
every user change. Entries are written in the same transaction as the
change and every text field is cut to fit, so a weird X-Forwarded-For
can't break a login.
Also the 1.8.0 upgrade notes, a locked-out section in troubleshooting,
and the changelog entry for the download link fix.
…ntries

Settings entries only list which keys changed. Old entries are deleted in
batches once a day and at startup; the number of days is a setting.
The rate limit keyed on the leftmost X-Forwarded-For entry, but nginx
appends to whatever the client sends, so every request could pick a
fresh bucket. Forwarding headers are only trusted from loopback now
(the bundled nginx) and the rightmost hop wins.

Without SESSION_SECRET, tokens were signed with a string that's sitting
in the source. Now a random key is generated on first start and kept
in session_secret next to the database. Installs that never set
SESSION_SECRET get signed out once on upgrade.
The meilisearch client had no timeout, and the async health route
called it directly, so a stuck Meili froze the whole event loop.
Requests now time out (5s for health, 30s otherwise) and health runs
in a thread.

Also in here, all found by the e2e run:
- a file that changes and then fails to index no longer matches its old text
- camera EXIF fields get read from the Exif sub-IFD, plus GPS from real files
- snippets are cut on visible text so a <mark> never gets chopped
Sources page shows reindex/delete errors instead of swallowing them,
Status shows Next scan for interval and default schedules, Clear
filters actually sticks after a reload, and the header buttons keep
their names on a phone. Result cards can be opened from the keyboard.
Pillow gives GPS coordinates back as IFDRational, which can't be
serialised, so the batch blew up and took the full reindex with it.
Convert them to floats, and check each document serialises before it
goes in the batch so one bad file only fails itself.
Revoking your only token used to drop it from the list, and the page
went back to "You haven't made any API tokens yet." Both lists (your
own and the admin view) now return every token with a status and
revoked_at, and the UI greys out revoked/expired rows with the date.
The header had Account, Admin, your name and Logout on search pages,
and Back to Search on admin pages with no Account at all. Now it's the
gear (Settings) and Logout everywhere. Inside, Account comes first in
the sidebar. Members get Account plus their browser-side Appearance and
Search settings, admins get Account plus everything they had before.

AdminLayout is renamed to SettingsLayout, and the docs say Settings
instead of Admin.
- Account lines up with the other settings pages instead of sitting in
  a narrow centred column, and the token table stops wrapping headers
- the gear goes to Account for everyone (it used to send admins to
  Sources, which didn't match the "Settings" label)
- Results per page gets a 20 option, since 20 is the default
- document dates don't wrap mid-value, and task lists lose the extra bullet
GFM renders them disabled and the browser greys disabled checkboxes
out, so done items looked faded. They're read-only now instead, in the
brand colour. Also lines up the password card with the token card on
Account.
Used to be top only, so you had to scroll back up after reading a page.
Pulled the pager into one TextPager component and render it top and
bottom; the bottom one scrolls you back to the start of the new page.

closes #333
Comment thread backend/app/admin_cli.py Dismissed
They start the backend without SESSION_SECRET, so on CI (no .env) it tried
to write the key to /app/data and blew up.
@demigodmode
demigodmode merged commit 7ce6d75 into main Nov 8, 2026
15 checks passed
@demigodmode
demigodmode deleted the release/1.8 branch November 8, 2026 03:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants