AkurAI Build
Menu

Public repository / e20fdc20ba73ed38f462643569dcb30aa8cb2187

AkurAI-Drive / backend/src/session.rs

Latest change feb03c4c2e9a6b0e869a63e52329c938e68dc8e8 - backend: device-token auth, SigV4 presigning, and file API for the sync client by olibuijr

use axum::http::{HeaderMap, header};

use crate::db::with_db;

const SESSION_TTL_SECS: i64 = 30 * 24 * 60 * 60; // ~30 days

/// A live, non-expired session loaded from the `sessions` table.
pub struct Session {
    pub sub: String,
    pub email: String,
    pub groups: Vec<String>,
    pub tenant_id: Option<String>,
}

/// Read a named cookie value out of the request `Cookie` header.
pub fn get_cookie(headers: &HeaderMap, name: &str) -> Option<String> {
    let cookies = headers.get(header::COOKIE)?.to_str().ok()?;
    let prefix = format!("{name}=");
    for part in cookies.split(';') {
        let part = part.trim();
        if let Some(value) = part.strip_prefix(&prefix) {
            return Some(value.to_string());
        }
    }
    None
}

/// Persist a new session row and return its random id (to set as `drive_session`).
pub fn create_session(
    sub: &str,
    email: &str,
    groups: &[String],
    tenant_id: Option<&str>,
) -> String {
    let id = crate::util::token();
    let now = chrono::Utc::now().timestamp();
    let expires = now + SESSION_TTL_SECS;
    let groups_json = serde_json::to_string(groups).unwrap_or_else(|_| "[]".to_string());

    with_db(|conn| {
        conn.execute(
            "INSERT INTO sessions (id, sub, email, groups, tenant_id, created, expires)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
            rusqlite::params![id, sub, email, groups_json, tenant_id, now, expires],
        )
        .expect("failed to insert session");
    });

    id
}

/// Look up the current session from the `drive_session` cookie. Expired rows are
/// rejected (and treated as no session).
pub fn lookup_session(headers: &HeaderMap) -> Option<Session> {
    let id = get_cookie(headers, "drive_session")?;
    let now = chrono::Utc::now().timestamp();

    with_db(|conn| {
        conn.query_row(
            "SELECT sub, email, groups, tenant_id
             FROM sessions WHERE id = ?1 AND expires > ?2",
            rusqlite::params![id, now],
            |row| {
                let groups_json: String = row.get(2)?;
                Ok(Session {
                    sub: row.get(0)?,
                    email: row.get(1)?,
                    groups: serde_json::from_str(&groups_json).unwrap_or_default(),
                    tenant_id: row.get(3)?,
                })
            },
        )
        .ok()
    })
}

/// Delete a session row by id (used on logout).
pub fn delete_session(id: &str) {
    with_db(|conn| {
        conn.execute("DELETE FROM sessions WHERE id = ?1", rusqlite::params![id])
            .ok();
    });
}

/// Remember where to send the browser after a successful login, keyed on the OAuth
/// `state`. Used by `/auth/device` so it can resume once a session exists.
pub fn store_oauth_return(state: &str, return_to: &str) {
    let now = chrono::Utc::now().timestamp();
    with_db(|conn| {
        conn.execute(
            "INSERT OR REPLACE INTO oauth_states (state, return_to, created) VALUES (?1, ?2, ?3)",
            rusqlite::params![state, return_to, now],
        )
        .ok();
    });
}

/// Consume the stored `return_to` for an OAuth `state` (one-shot — the row is
/// deleted whether or not it existed).
pub fn take_oauth_return(state: &str) -> Option<String> {
    with_db(|conn| {
        let return_to: Option<String> = conn
            .query_row(
                "SELECT return_to FROM oauth_states WHERE state = ?1",
                rusqlite::params![state],
                |row| row.get(0),
            )
            .ok();
        conn.execute(
            "DELETE FROM oauth_states WHERE state = ?1",
            rusqlite::params![state],
        )
        .ok();
        return_to
    })
}