feat(v1.1.8): app_user_sessions table + repo with sliding TTL
App-user session storage (migration 0027) mirrors admin_sessions but
adds three things v1.1.8 needs:
* app_id column + FK cascade — every v1.1+ table starts with app_id
so cross-app isolation is bright at the SQL layer (lookup keys
off the hash only, but defense-in-depth: a leaked row's session
still scopes to its app on every read).
* absolute_expires_at — hard cap on the sliding window (default 30d
via PICLOUD_APP_USER_SESSION_ABSOLUTE_HOURS). Beyond this the
user must re-login regardless of recent activity.
* revoked_at — explicit revocation by token (logout) or per-user
(admin revoke-sessions button, password reset). Lookups reject
revoked rows immediately so revocation takes effect before the
weekly GC sweep runs.
The repo's gc() uses FOR UPDATE SKIP LOCKED matching the dead-letter
and abandoned-executions sweep patterns; the GC wiring lands in a
later commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
35
crates/manager-core/migrations/0027_app_user_sessions.sql
Normal file
35
crates/manager-core/migrations/0027_app_user_sessions.sql
Normal file
@@ -0,0 +1,35 @@
|
||||
-- v1.1.8 User Management — app-user sessions.
|
||||
--
|
||||
-- Distinct from `admin_sessions`. Mirror the schema shape (token_hash
|
||||
-- PK, user FK cascading) but add:
|
||||
--
|
||||
-- * app_id — every v1.1+ data-plane table starts with the app_id
|
||||
-- FK so cross-app isolation is bright at the SQL level
|
||||
-- and ON DELETE CASCADE wipes sessions when an app is
|
||||
-- deleted (in addition to the user-FK cascade).
|
||||
-- * absolute_expires_at — hard cap on the sliding window. The
|
||||
-- application caps new_expires_at at this value on each
|
||||
-- touch; beyond it, force re-login.
|
||||
-- * revoked_at — explicit revocation (admin "revoke all sessions"
|
||||
-- button, password reset, logout). The lookup query
|
||||
-- rejects revoked rows so a revoked session is dead
|
||||
-- immediately, before the GC sweep runs.
|
||||
--
|
||||
-- `token_hash` stores SHA-256(raw_token) as hex; the raw lives only in
|
||||
-- the script's return value from `users::login` / `users::accept_invite`
|
||||
-- and the realtime subscriber's Authorization header.
|
||||
|
||||
CREATE TABLE app_user_sessions (
|
||||
token_hash TEXT PRIMARY KEY,
|
||||
app_id UUID NOT NULL REFERENCES apps(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES app_users(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_used_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
absolute_expires_at TIMESTAMPTZ NOT NULL,
|
||||
revoked_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX idx_app_user_sessions_user ON app_user_sessions (app_id, user_id);
|
||||
CREATE INDEX idx_app_user_sessions_expiry
|
||||
ON app_user_sessions (expires_at) WHERE revoked_at IS NULL;
|
||||
Reference in New Issue
Block a user