use std::time::SystemTime;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SessionId(Uuid);
impl SessionId {
#[must_use]
pub fn new() -> Self {
Self(Uuid::now_v7())
}
#[must_use]
pub fn from_uuid(value: Uuid) -> Self {
Self(value)
}
#[must_use]
pub fn into_uuid(self) -> Uuid {
self.0
}
}
impl Default for SessionId {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SessionFamilyId(Uuid);
impl SessionFamilyId {
#[must_use]
pub fn new() -> Self {
Self(Uuid::now_v7())
}
#[must_use]
pub fn from_uuid(value: Uuid) -> Self {
Self(value)
}
#[must_use]
pub fn into_uuid(self) -> Uuid {
self.0
}
}
impl Default for SessionFamilyId {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Session {
pub session_id: SessionId,
pub family_id: SessionFamilyId,
pub subject_id: String,
pub created_at: SystemTime,
pub expires_at: SystemTime,
pub last_seen_at: Option<SystemTime>,
pub revoked: bool,
}
impl Session {
#[must_use]
pub fn new(
family_id: SessionFamilyId,
subject_id: impl Into<String>,
created_at: SystemTime,
expires_at: SystemTime,
) -> Self {
Self {
session_id: SessionId::new(),
family_id,
subject_id: subject_id.into(),
created_at,
expires_at,
last_seen_at: None,
revoked: false,
}
}
#[must_use]
pub fn touched(mut self, last_seen_at: SystemTime) -> Self {
self.last_seen_at = Some(last_seen_at);
self
}
#[must_use]
pub fn revoked(mut self) -> Self {
self.revoked = true;
self
}
#[must_use]
pub fn is_active_at(&self, now: SystemTime) -> bool {
!self.revoked && self.expires_at > now
}
#[must_use]
pub fn is_expired_at(&self, now: SystemTime) -> bool {
self.expires_at <= now
}
}
pub type SessionRecord = Session;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionFamilyRecord {
pub family_id: SessionFamilyId,
pub subject_id: String,
pub created_at: SystemTime,
pub revoked: bool,
}
impl SessionFamilyRecord {
#[must_use]
pub fn new(
family_id: SessionFamilyId,
subject_id: impl Into<String>,
created_at: SystemTime,
) -> Self {
Self {
family_id,
subject_id: subject_id.into(),
created_at,
revoked: false,
}
}
#[must_use]
pub fn revoked(mut self) -> Self {
self.revoked = true;
self
}
#[must_use]
pub fn is_active(&self) -> bool {
!self.revoked
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionRefreshRecord {
pub session_id: SessionId,
pub family_id: SessionFamilyId,
pub expires_at: SystemTime,
pub revoked: bool,
}
impl SessionRefreshRecord {
#[must_use]
pub fn new(session_id: SessionId, family_id: SessionFamilyId, expires_at: SystemTime) -> Self {
Self {
session_id,
family_id,
expires_at,
revoked: false,
}
}
#[must_use]
pub fn revoked(mut self) -> Self {
self.revoked = true;
self
}
#[must_use]
pub fn is_active_at(&self, now: SystemTime) -> bool {
!self.revoked && self.expires_at > now
}
#[must_use]
pub fn is_expired_at(&self, now: SystemTime) -> bool {
self.expires_at <= now
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionLookup {
pub session: SessionRecord,
pub family: SessionFamilyRecord,
pub refresh: SessionRefreshRecord,
}
impl SessionLookup {
#[must_use]
pub fn new(
session: SessionRecord,
family: SessionFamilyRecord,
refresh: SessionRefreshRecord,
) -> Self {
Self {
session,
family,
refresh,
}
}
#[must_use]
pub fn is_active_at(&self, now: SystemTime) -> bool {
self.session.is_active_at(now) && self.family.is_active() && self.refresh.is_active_at(now)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SessionTouch {
pub session_id: SessionId,
pub last_seen_at: SystemTime,
}
impl SessionTouch {
#[must_use]
pub fn new(session_id: SessionId, last_seen_at: SystemTime) -> Self {
Self {
session_id,
last_seen_at,
}
}
}
#[cfg(test)]
mod tests {
use super::{
Session, SessionFamilyId, SessionFamilyRecord, SessionLookup, SessionRefreshRecord,
SessionTouch,
};
use std::time::{Duration, SystemTime};
#[test]
fn new_session_is_active_and_not_revoked() {
let now = SystemTime::UNIX_EPOCH + Duration::from_secs(100);
let session = Session::new(
SessionFamilyId::new(),
"user-123",
now,
now + Duration::from_secs(60),
);
assert!(!session.revoked);
assert!(session.last_seen_at.is_none());
assert!(session.is_active_at(now));
assert!(!session.is_expired_at(now));
}
#[test]
fn touched_session_updates_last_seen() {
let now = SystemTime::UNIX_EPOCH + Duration::from_secs(100);
let touched_at = now + Duration::from_secs(10);
let session = Session::new(
SessionFamilyId::new(),
"user-123",
now,
now + Duration::from_secs(60),
)
.touched(touched_at);
assert_eq!(session.last_seen_at, Some(touched_at));
}
#[test]
fn revoked_session_is_not_active() {
let now = SystemTime::UNIX_EPOCH + Duration::from_secs(100);
let session = Session::new(
SessionFamilyId::new(),
"user-123",
now,
now + Duration::from_secs(60),
)
.revoked();
assert!(session.revoked);
assert!(!session.is_active_at(now));
}
#[test]
fn expired_session_reports_expired() {
let now = SystemTime::UNIX_EPOCH + Duration::from_secs(100);
let session = Session::new(
SessionFamilyId::new(),
"user-123",
now,
now + Duration::from_secs(1),
);
assert!(session.is_expired_at(now + Duration::from_secs(1)));
assert!(!session.is_active_at(now + Duration::from_secs(1)));
}
#[test]
fn family_record_reports_activity_state() {
let now = SystemTime::UNIX_EPOCH + Duration::from_secs(100);
let active_family = SessionFamilyRecord::new(SessionFamilyId::new(), "user-123", now);
let revoked_family = active_family.clone().revoked();
assert!(active_family.is_active());
assert!(!revoked_family.is_active());
}
#[test]
fn refresh_record_reports_activity_state() {
let now = SystemTime::UNIX_EPOCH + Duration::from_secs(100);
let refresh = SessionRefreshRecord::new(
super::SessionId::new(),
SessionFamilyId::new(),
now + Duration::from_secs(60),
);
let revoked = refresh.clone().revoked();
assert!(refresh.is_active_at(now));
assert!(!refresh.is_expired_at(now));
assert!(!revoked.is_active_at(now));
}
#[test]
fn lookup_is_active_only_when_all_components_are_active() {
let now = SystemTime::UNIX_EPOCH + Duration::from_secs(100);
let family = SessionFamilyRecord::new(SessionFamilyId::new(), "user-123", now);
let session = Session::new(
family.family_id,
"user-123",
now,
now + Duration::from_secs(60),
);
let refresh = SessionRefreshRecord::new(
session.session_id,
family.family_id,
now + Duration::from_secs(60),
);
let lookup = SessionLookup::new(session.clone(), family.clone(), refresh.clone());
let revoked_lookup = SessionLookup::new(session.revoked(), family, refresh);
assert!(lookup.is_active_at(now));
assert!(!revoked_lookup.is_active_at(now));
}
#[test]
fn session_touch_captures_target_and_timestamp() {
let now = SystemTime::UNIX_EPOCH + Duration::from_secs(100);
let touch = SessionTouch::new(super::SessionId::new(), now);
assert_eq!(touch.last_seen_at, now);
}
}