Skip to main content

cortex/models/
session.rs

1// Copyright 2015-2025 Deyan Ginev. See the LICENSE
2// file at the top-level directory of this distribution.
3//
4// Licensed under the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>.
5// This file may not be copied, modified, or distributed
6// except according to those terms.
7
8//! Server-side admin sessions (`docs/archive/WEBAUTHN_DESIGN.md`). The browser cookie carries only
9//! a random opaque session id; this table holds the owner + absolute expiry. **Both** human sign-in
10//! paths — the admin **token** and a **passkey** — open a session, so the model is unified and the
11//! cookie no longer carries a credential. Sign-out deletes the row (real revocation).
12
13use chrono::{Duration, NaiveDateTime, Utc};
14use diesel::prelude::*;
15use diesel::result::Error;
16use rand::RngExt;
17use rand::distr::Alphanumeric;
18
19use crate::schema::sessions;
20
21/// How long a session is valid from creation. **Absolute** expiry — no per-request sliding write,
22/// so an authenticated request costs one indexed lookup and zero writes (performance).
23pub const SESSION_TTL_DAYS: i64 = 7;
24
25/// A server-side admin session.
26#[derive(Queryable, Identifiable, Debug, Clone)]
27#[diesel(table_name = sessions)]
28pub struct Session {
29  /// The opaque session id (the cookie value).
30  pub id: String,
31  /// The authenticated identity (the audit-log actor / token owner).
32  pub owner: String,
33  /// How the session was established: `token` or `passkey`.
34  pub method: String,
35  /// When the session was opened.
36  pub created_at: NaiveDateTime,
37  /// Absolute expiry; the session resolves only while `now < expires_at`.
38  pub expires_at: NaiveDateTime,
39}
40
41#[derive(Insertable)]
42#[diesel(table_name = sessions)]
43struct NewSession<'a> {
44  id: &'a str,
45  owner: &'a str,
46  method: &'a str,
47  expires_at: NaiveDateTime,
48}
49
50impl Session {
51  /// Opens a session for `owner` established via `method` (`token` | `passkey`) and returns the
52  /// opaque session id to place in the cookie. Prunes expired rows first (best-effort
53  /// housekeeping).
54  pub fn open(connection: &mut PgConnection, owner: &str, method: &str) -> Result<String, Error> {
55    let _ = Self::prune_expired(connection);
56    // 48 url-safe chars (~285 bits) — an unguessable bearer; the security rests on this randomness.
57    let id: String = rand::rng()
58      .sample_iter(&Alphanumeric)
59      .take(48)
60      .map(char::from)
61      .collect();
62    let expires_at = (Utc::now() + Duration::days(SESSION_TTL_DAYS)).naive_utc();
63    diesel::insert_into(sessions::table)
64      .values(NewSession {
65        id: &id,
66        owner,
67        method,
68        expires_at,
69      })
70      .execute(connection)?;
71    Ok(id)
72  }
73
74  /// Resolves a session id to its (unexpired) owner, or `None` if unknown/expired.
75  pub fn resolve_owner(connection: &mut PgConnection, id: &str) -> Option<String> {
76    use crate::schema::sessions::dsl;
77    dsl::sessions
78      .filter(dsl::id.eq(id))
79      .filter(dsl::expires_at.gt(Utc::now().naive_utc()))
80      .select(dsl::owner)
81      .first::<String>(connection)
82      .optional()
83      .ok()
84      .flatten()
85  }
86
87  /// Revokes (deletes) a single session — sign-out.
88  pub fn revoke(connection: &mut PgConnection, id: &str) -> Result<(), Error> {
89    use crate::schema::sessions::dsl;
90    diesel::delete(dsl::sessions.filter(dsl::id.eq(id)))
91      .execute(connection)
92      .map(|_| ())
93  }
94
95  /// Revokes every session for an owner (sign-out-everywhere / a revoked token).
96  pub fn revoke_all_for(connection: &mut PgConnection, owner: &str) -> Result<usize, Error> {
97    use crate::schema::sessions::dsl;
98    diesel::delete(dsl::sessions.filter(dsl::owner.eq(owner))).execute(connection)
99  }
100
101  /// Lists the live (unexpired) sessions, most-recent first — the admin "active sessions" view.
102  pub fn active(connection: &mut PgConnection) -> Result<Vec<Self>, Error> {
103    use crate::schema::sessions::dsl;
104    dsl::sessions
105      .filter(dsl::expires_at.gt(Utc::now().naive_utc()))
106      .order(dsl::created_at.desc())
107      .get_results(connection)
108  }
109
110  /// Deletes expired sessions (housekeeping); returns the number removed.
111  pub fn prune_expired(connection: &mut PgConnection) -> Result<usize, Error> {
112    use crate::schema::sessions::dsl;
113    diesel::delete(dsl::sessions.filter(dsl::expires_at.le(Utc::now().naive_utc())))
114      .execute(connection)
115  }
116}