Capture the preferred language from the browser view which accepts the
device code grant and store it on the grant, so it can later be used to
render a human-readable device name.
Add a new `for_clients(&[&Client])` method (and matching `clients()`
getter) on `OAuth2SessionFilter`. The existing single-client
`for_client(&Client)` is kept intact as GraphQL still uses it; the two
fields are independent and ANDed together when both happen to be set.
In the Postgres implementation the predicate uses sea-query's `is_in`,
which translates to a plain `column IN (...)`. As a side effect, an
empty client list matches no rows (sea-query emits `WHERE 1 = 2`),
which is the desired behaviour for an explicit "filter on this empty
set of clients" request.
Adds `UserFilter::with_active_compat_session(has: bool)` and its
PostgreSQL implementation (an `EXISTS` / `NOT EXISTS` subquery against
`compat_sessions` on `user_id` with `finished_at IS NULL`).
Also adds a partial index `compat_sessions (user_id) WHERE finished_at IS
NULL` so that on installations with many finished compat sessions the
existence/non-existence check stays cheap.
Adds `UserFilter::with_active_oauth2_session_for_any_of_clients` and its
PostgreSQL implementation (an `EXISTS` sub-query joining `oauth2_sessions`
on `user_id`, restricting to `finished_at IS NULL` and
`oauth2_client_id = ANY(...)`). The semantics are OR across the supplied
clients.
Also adds a partial composite index `oauth2_sessions (user_id,
oauth2_client_id) WHERE finished_at IS NULL` so that on installations with a
lot of churn we don't need to walk through finished sessions when answering
"does this user have an active session for any of these clients?". The
existing FK indexes can answer the query, but visit finished rows too.
Captures the raw query parameters from the downstream OAuth2 authorization
request, so they can later be referenced from templated upstream
authorization parameters. Existing call sites pass an empty map; a
follow-up commit will wire the downstream authorization handler to
to capture the real query parameters.
This adds three new scheduled cleanup jobs that clear the last_active_ip
field from sessions that have been inactive for more than 30 days:
- CleanupInactiveOAuth2SessionIpsJob
- CleanupInactiveCompatSessionIpsJob
- CleanupInactiveUserSessionIpsJob
This helps with data minimization by not retaining IP addresses longer
Implements hard deletion of user/browser sessions that have been finished for more than 30 days, but only after all child sessions are cleaned up.
User sessions can only be deleted when no child sessions exist, ensuring backchannel logout propagation continues to work correctly.
This includes sessions that were never completed, and sessions where
user_session was cleaned up. This is to avoid breaking features like
OIDC Backchannel Logout after 30 days.
Add scheduled cleanup job that removes old completed and failed queue
jobs after 30 days. Jobs are kept for debugging purposes.
Includes migration to change the next_attempt_id FK constraint from NO
ACTION to SET NULL, allowing cleanup of retry chains without breaking
foreign key constraints.
One caveat is that cleanup is based on their creation time, *not* when
they got completed/failed. This means that if the job takes a long time
(as in, several days) to get scheduled, it might get cleared as soon as
it runs. This is fine for now, we may want to revisit this if we start
scheduling jobs far in the future
Add two cleanup jobs scheduled hourly:
1. Upstream OAuth authorization sessions - removes sessions after 30 days
2. Orphaned upstream OAuth links - removes links after 7 days where user_id IS NULL. These are links created during upstream OAuth 2.0 login but never associated with a user
Add scheduled cleanup job that removes old user recovery sessions after
7 days. Runs hourly.
Implementation uses ULID cursor-based pagination with no additional
indexes needed. Child tickets cascade-delete automatically.