Files
zesdex/crates/zesdex-iam/src/infrastructure/persistence/session_repo.rs
T
asepharyana e9a8e93c83 Refactor session ID handling and improve error management
- Introduced `SessionId` newtype for validated session identifiers, ensuring safety against path traversal attacks.
- Updated session repository methods to accept `SessionId` instead of raw strings, enhancing type safety.
- Removed redundant error handling in repository methods by leveraging the new `Error` type from `zesdex_utils`.
- Simplified atomic JSON write operations by eliminating unnecessary error conversions.
- Enhanced integer casting with a new `CastOr` trait for safer narrowing conversions.
- Removed deprecated error handling code and consolidated error types across the codebase.
- Updated HTTP handlers to utilize the new session ID validation, improving overall robustness.
2026-07-20 06:39:30 +07:00

104 lines
3.9 KiB
Rust

//! Filesystem-backed `SessionRepository` implementation.
//!
//! Each session is stored as `<base_dir>/sessions/<id>/session.json`.
//! Writes use a write-then-rename + fsync pattern for crash safety.
//!
//! # Flow
//!
//! - **`list_sessions`** — enumerate `<base_dir>/sessions/` subdirectories,
//! attempt `load_session` on each (silently skipping failures).
//! - **`load_session`** — reads and deserialises `session.json`.
//! - **`save_session`** — creates session directory, writes JSON atomically.
//! - **`delete_session`** — removes the session directory.
//!
//! # Security
//!
//! Session IDs are validated at construction via [`SessionId::new`], so
//! directory-traversal attacks are prevented by the type system — no
//! per-method checks needed.
//!
//! # Components
//!
//! - `FileSystemSessionRepository` — stateless singleton implementing `SessionRepository`
use std::path::Path;
use tracing;
use zesdex_entities::domain::auth::SessionId;
use zesdex_utils::write_json_atomic;
use crate::domain::error::RepositoryError;
use crate::domain::repository::SessionRepository;
use crate::domain::session::Session;
/// Concrete filesystem session repository.
#[derive(Debug, Clone, Default)]
pub struct FileSystemSessionRepository;
impl FileSystemSessionRepository {
/// Create a new filesystem session repository.
pub fn new() -> Self {
FileSystemSessionRepository
}
}
impl SessionRepository for FileSystemSessionRepository {
fn list_sessions(&self, base_dir: &Path) -> Result<Vec<Session>, RepositoryError> {
let sessions_dir = base_dir.join("sessions");
let entries = match std::fs::read_dir(&sessions_dir) {
Ok(e) => e,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
tracing::warn!(path = %sessions_dir.display(), "sessions directory not found");
return Ok(Vec::new());
}
Err(e) => return Err(RepositoryError::Io(e)),
};
let mut sessions = Vec::new();
for entry in entries.flatten() {
if !entry.path().is_dir() {
continue;
}
let name = entry.file_name().to_string_lossy().to_string();
// Directory names from UUIDs are always valid session IDs.
if let Ok(sid) = SessionId::new(&name) {
if let Ok(session) = self.load_session(base_dir, &sid) {
sessions.push(session);
}
}
}
tracing::debug!(count = sessions.len(), "listed sessions");
Ok(sessions)
}
fn load_session(&self, base_dir: &Path, id: &SessionId) -> Result<Session, RepositoryError> {
let path = base_dir.join("sessions").join(id.as_str()).join("session.json");
if !path.exists() {
return Err(RepositoryError::NotFound(format!(
"session not found: {}",
id.as_str()
)));
}
tracing::debug!(session_id = %id, path = %path.display(), "loading session");
let data = std::fs::read_to_string(&path)?; // → RepositoryError
let session: Session = serde_json::from_str(&data)?; // → RepositoryError
Ok(session)
}
fn save_session(&self, base_dir: &Path, session: &Session) -> Result<(), RepositoryError> {
let dir = session.session_dir(base_dir);
std::fs::create_dir_all(&dir)?; // → RepositoryError
let path = dir.join("session.json");
tracing::debug!(session_id = %session.id, path = %path.display(), "saving session");
write_json_atomic(&path, session, None)?;
Ok(())
}
fn delete_session(&self, base_dir: &Path, id: &SessionId) -> Result<(), RepositoryError> {
let dir = base_dir.join("sessions").join(id.as_str());
tracing::debug!(session_id = %id, path = %dir.display(), "deleting session");
if dir.exists() {
std::fs::remove_dir_all(&dir)?; // → RepositoryError
}
Ok(())
}
}