feat(auth): sessions expire server-side; password change and reset end other sessions (M462 #4978)

Sessions had no server-side expiry: only the web cookie's 30-day Max-Age
limited them, and a bearer token (Android) lived until revoked by hand.
GetSessionByTokenHash and ListSessionsForUser now ignore sessions idle for
30 days or older than a year, and the GC worker deletes them hourly.

A password change was a plain UPDATE, so a session opened with the old
password survived it. Now:
- self-service change signs out every other device and keeps this one;
- reset by email ends every session the account has;
- an admin reset ends the target's sessions (keeping the admin's own when
  they reset themselves).

The success copy on web and Android says the other devices were signed out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 08:32:39 -04:00
co-authored by Claude Opus 5.5
parent 719dc62b0d
commit 755f997b0d
11 changed files with 262 additions and 7 deletions
@@ -57,7 +57,7 @@ class PasswordViewModel @Inject constructor(
try {
repository.changePassword(current = s.current, next = s.next)
internal.update {
PasswordUiState(message = "Password changed.")
PasswordUiState(message = "Password changed. Your other devices have been signed out.")
}
} catch (
@Suppress("TooGenericExceptionCaught") e: Throwable,