use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionInfo {
pub token: String,
pub user_id: String,
pub email: String,
pub created_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
impl SessionInfo {
pub fn new(user_id: impl Into<String>, email: impl Into<String>, expires_at: DateTime<Utc>) -> Self {
Self {
token: Uuid::new_v4().to_string(),
user_id: user_id.into(),
email: email.into(),
created_at: Utc::now(),
expires_at,
metadata: None,
}
}
pub fn with_token(
token: impl Into<String>,
user_id: impl Into<String>,
email: impl Into<String>,
expires_at: DateTime<Utc>,
) -> Self {
Self {
token: token.into(),
user_id: user_id.into(),
email: email.into(),
created_at: Utc::now(),
expires_at,
metadata: None,
}
}
pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
self.metadata = Some(metadata);
self
}
pub fn is_expired(&self) -> bool {
Utc::now() > self.expires_at
}
pub fn remaining_seconds(&self) -> i64 {
(self.expires_at - Utc::now()).num_seconds().max(0)
}
}
#[async_trait::async_trait]
pub trait SessionStore: Send + Sync {
async fn create(&self, session: &SessionInfo) -> Result<(), SessionStoreError>;
async fn get(&self, token: &str) -> Result<Option<SessionInfo>, SessionStoreError>;
async fn update(&self, session: &SessionInfo) -> Result<(), SessionStoreError>;
async fn delete(&self, token: &str) -> Result<(), SessionStoreError>;
async fn delete_user_sessions(&self, user_id: &str) -> Result<(), SessionStoreError>;
async fn cleanup_expired(&self) -> Result<u64, SessionStoreError>;
}
#[derive(Debug, thiserror::Error)]
pub enum SessionStoreError {
#[error("Session not found")]
NotFound,
#[error("Session expired")]
Expired,
#[error("Database error: {0}")]
Database(String),
#[error("Serialization error: {0}")]
Serialization(String),
#[error("Internal error: {0}")]
Internal(String),
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Duration;
#[test]
fn test_session_creation() {
let expires_at = Utc::now() + Duration::hours(1);
let session = SessionInfo::new("user_123", "user@example.com", expires_at);
assert_eq!(session.user_id, "user_123");
assert_eq!(session.email, "user@example.com");
assert!(!session.is_expired());
}
#[test]
fn test_session_expiration() {
let expires_at = Utc::now() - Duration::hours(1);
let session = SessionInfo::new("user_123", "user@example.com", expires_at);
assert!(session.is_expired());
assert_eq!(session.remaining_seconds(), 0);
}
#[test]
fn test_session_with_metadata() {
let expires_at = Utc::now() + Duration::hours(1);
let session = SessionInfo::new("user_123", "user@example.com", expires_at)
.with_metadata(serde_json::json!({"role": "admin"}));
assert!(session.metadata.is_some());
}
}