2025-03-20 09:22:31 +01:00
|
|
|
use crate::common::errors::DomainError;
|
2026-02-14 01:29:34 +01:00
|
|
|
use crate::domain::entities::session::Session;
|
2026-03-07 14:59:32 +01:00
|
|
|
use uuid::Uuid;
|
2025-03-20 09:22:31 +01:00
|
|
|
|
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
|
|
|
pub enum SessionRepositoryError {
|
2026-02-12 09:41:25 +01:00
|
|
|
#[error("Session not found: {0}")]
|
2025-03-20 09:22:31 +01:00
|
|
|
NotFound(String),
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
#[error("Database error: {0}")]
|
2025-03-20 09:22:31 +01:00
|
|
|
DatabaseError(String),
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
#[error("Timeout error: {0}")]
|
2025-03-20 09:22:31 +01:00
|
|
|
Timeout(String),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub type SessionRepositoryResult<T> = Result<T, SessionRepositoryError>;
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Conversion from SessionRepositoryError to DomainError
|
2025-03-20 09:22:31 +01:00
|
|
|
impl From<SessionRepositoryError> for DomainError {
|
|
|
|
|
fn from(err: SessionRepositoryError) -> Self {
|
|
|
|
|
match err {
|
2026-02-14 01:29:34 +01:00
|
|
|
SessionRepositoryError::NotFound(msg) => DomainError::not_found("Session", msg),
|
2025-03-20 09:22:31 +01:00
|
|
|
SessionRepositoryError::DatabaseError(msg) => {
|
|
|
|
|
DomainError::internal_error("Database", msg)
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
|
|
|
|
SessionRepositoryError::Timeout(msg) => DomainError::timeout("Database", msg),
|
2025-03-20 09:22:31 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub trait SessionRepository: Send + Sync + 'static {
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Creates a new session
|
2025-03-20 09:22:31 +01:00
|
|
|
async fn create_session(&self, session: Session) -> SessionRepositoryResult<Session>;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Gets a session by ID
|
2026-03-07 14:59:32 +01:00
|
|
|
async fn get_session_by_id(&self, id: Uuid) -> SessionRepositoryResult<Session>;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Gets a session by refresh token
|
2026-02-14 01:29:34 +01:00
|
|
|
async fn get_session_by_refresh_token(
|
|
|
|
|
&self,
|
|
|
|
|
refresh_token: &str,
|
|
|
|
|
) -> SessionRepositoryResult<Session>;
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Gets all sessions for a user
|
2026-03-07 14:59:32 +01:00
|
|
|
async fn get_sessions_by_user_id(&self, user_id: Uuid)
|
2026-02-14 01:29:34 +01:00
|
|
|
-> SessionRepositoryResult<Vec<Session>>;
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Revokes a specific session
|
2026-03-07 14:59:32 +01:00
|
|
|
async fn revoke_session(&self, session_id: Uuid) -> SessionRepositoryResult<()>;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Revokes all sessions for a user
|
2026-03-07 14:59:32 +01:00
|
|
|
async fn revoke_all_user_sessions(&self, user_id: Uuid) -> SessionRepositoryResult<u64>;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-08-05 23:35:05 +02:00
|
|
|
/// Revokes every session for `user_id` EXCEPT the one identified by
|
|
|
|
|
/// `keep_session_id`. Classic "password change" pattern: log the
|
|
|
|
|
/// user out from every OTHER device, but keep the current device's
|
|
|
|
|
/// session alive so the SPA can complete follow-up work (e.g. OPAQUE
|
|
|
|
|
/// envelope re-registration) without a session-death race.
|
|
|
|
|
///
|
|
|
|
|
/// Returns the count of revoked rows (excluding the kept one).
|
|
|
|
|
/// If `keep_session_id` doesn't belong to `user_id` (defensive),
|
|
|
|
|
/// the WHERE clause still matches nothing to revoke on that row —
|
|
|
|
|
/// no cross-user side effect.
|
|
|
|
|
async fn revoke_other_user_sessions(
|
|
|
|
|
&self,
|
|
|
|
|
user_id: Uuid,
|
|
|
|
|
keep_session_id: Uuid,
|
|
|
|
|
) -> SessionRepositoryResult<u64>;
|
|
|
|
|
|
2026-05-07 09:30:09 +02:00
|
|
|
/// Revokes all sessions in a token family (theft response)
|
|
|
|
|
async fn revoke_session_family(&self, family_id: Uuid) -> SessionRepositoryResult<u64>;
|
|
|
|
|
|
2026-08-03 08:00:13 +02:00
|
|
|
/// Revokes every OxiCloud session whose OIDC sid claim matches.
|
|
|
|
|
///
|
|
|
|
|
/// Used by the Back-Channel Logout handler when the IdP sends a
|
|
|
|
|
/// logout_token with a `sid` — this is the per-device path and
|
|
|
|
|
/// matches (in the typical case) exactly one session row. Returns
|
|
|
|
|
/// user IDs of every affected session so the caller can dispatch
|
|
|
|
|
/// per-user lifecycle hooks.
|
|
|
|
|
async fn revoke_sessions_by_oidc_sid(&self, sid: &str) -> SessionRepositoryResult<Vec<Uuid>>;
|
|
|
|
|
|
|
|
|
|
/// Revokes every session belonging to the user identified by
|
|
|
|
|
/// `(oidc_provider, oidc_subject)`.
|
|
|
|
|
///
|
|
|
|
|
/// Fallback path for the Back-Channel Logout handler when the IdP
|
|
|
|
|
/// omits `sid` from the logout_token — coarser than sid-based
|
|
|
|
|
/// revocation (kills the user's other devices too). Returns the
|
|
|
|
|
/// user id of the affected account, or `None` if no matching user.
|
|
|
|
|
async fn revoke_user_sessions_by_oidc_subject(
|
|
|
|
|
&self,
|
|
|
|
|
oidc_provider: &str,
|
|
|
|
|
oidc_subject: &str,
|
|
|
|
|
) -> SessionRepositoryResult<Option<Uuid>>;
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Deletes expired sessions
|
2025-03-20 09:22:31 +01:00
|
|
|
async fn delete_expired_sessions(&self) -> SessionRepositoryResult<u64>;
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|