Refactor CMS and IAM modules: restructure presentation and command layers
- Removed HTTP adapter module from CMS infrastructure. - Updated CMS infrastructure module to exclude HTTP. - Introduced presentation layer in CMS with DTOs and handlers for REST API. - Added command types for CMS domain operations to encapsulate input data. - Created typed error handling for CMS presentation layer. - Implemented handlers for CMS REST API endpoints. - Removed HTTP DTOs and handlers from IAM infrastructure. - Introduced command types for IAM domain operations. - Created presentation layer in IAM with DTOs and handlers for OAuth flow. - Implemented typed error handling for IAM presentation layer.
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
//! Command types for IAM domain operations.
|
||||
//!
|
||||
//! Following the `NewXxx` / command pattern from clean architecture,
|
||||
//! these types encapsulate the input data for create/update operations
|
||||
//! on domain entities. They decouple presentation DTOs from the entity
|
||||
//! mutation surface and provide a clear boundary for validation.
|
||||
|
||||
/// Command to create a new session.
|
||||
///
|
||||
/// Carries only the data needed to construct a session entity — the
|
||||
/// service generates the UUID and timestamp internally.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NewSession {
|
||||
/// Human-readable session title.
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
impl From<String> for NewSession {
|
||||
fn from(title: String) -> Self {
|
||||
Self { title }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for NewSession {
|
||||
fn from(title: &str) -> Self {
|
||||
Self {
|
||||
title: title.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
//! - [`service`] — Trait definitions: `OAuthService`, `SessionService`
|
||||
//! - [`session`] — `Session` entity (IAM wrapper around `zesdex_entities::Session`)
|
||||
|
||||
pub mod commands;
|
||||
pub mod error;
|
||||
pub mod oauth;
|
||||
pub mod repository;
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
//! HTTP adapter layer for OAuth callback handling.
|
||||
//!
|
||||
//! # Sub-modules
|
||||
//!
|
||||
//! - [`dto`] — Request/response DTOs for the loopback endpoint
|
||||
//! - [`handlers`] — HTTP handler that validates state and extracts `?code=`
|
||||
|
||||
pub mod dto;
|
||||
pub mod handlers;
|
||||
@@ -5,12 +5,10 @@
|
||||
//!
|
||||
//! # Sub-modules
|
||||
//!
|
||||
//! - [`http`] — HTTP server, DTOs, handlers for the OAuth callback
|
||||
//! - [`oauth_loopback`] — Loopback HTTP server to receive the OAuth redirect
|
||||
//! - [`persistence`] — Filesystem-backed repositories (JSON + PID locks)
|
||||
//! - [`rng`] — System random token / UUID generation
|
||||
|
||||
pub mod http;
|
||||
pub mod oauth_loopback;
|
||||
pub mod persistence;
|
||||
pub mod rng;
|
||||
|
||||
@@ -12,3 +12,4 @@
|
||||
pub mod application;
|
||||
pub mod domain;
|
||||
pub mod infrastructure;
|
||||
pub mod presentation;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
//! Typed presentation-layer error type for the IAM crate.
|
||||
//!
|
||||
//! `AppError` replaces bare `anyhow::Result` in handler signatures with a
|
||||
//! structured enum that callers can match on for status-code selection
|
||||
//! and structured error responses.
|
||||
//!
|
||||
//! `From<ServiceError>` auto-converts domain errors so handler code uses
|
||||
//! the `?` operator throughout.
|
||||
//!
|
||||
//! # Variants
|
||||
//!
|
||||
//! - `BadRequest` — invalid input, validation failure, OAuth state mismatch
|
||||
//! - `NotFound` — resource (session, token) not found
|
||||
//! - `Conflict` — resource already exists (e.g. duplicate session)
|
||||
//! - `Internal` — unexpected errors translated to a generic message
|
||||
|
||||
use crate::domain::error::ServiceError;
|
||||
|
||||
/// Typed presentation-layer error.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AppError {
|
||||
/// The request was malformed or contained invalid data.
|
||||
#[error("Bad request: {0}")]
|
||||
BadRequest(String),
|
||||
/// The requested resource was not found.
|
||||
#[error("Not found: {0}")]
|
||||
NotFound(String),
|
||||
/// The request conflicts with the current state.
|
||||
#[error("Conflict: {0}")]
|
||||
Conflict(String),
|
||||
/// An unexpected internal error occurred.
|
||||
#[error("Internal error: {0}")]
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
impl From<ServiceError> for AppError {
|
||||
fn from(e: ServiceError) -> Self {
|
||||
match e {
|
||||
ServiceError::Repository(repo_err) => match repo_err {
|
||||
zesdex_utils::Error::NotFound(msg) => AppError::NotFound(msg),
|
||||
zesdex_utils::Error::Conflict(msg) => AppError::Conflict(msg),
|
||||
_ => AppError::Internal(repo_err.to_string()),
|
||||
},
|
||||
ServiceError::InvalidConfig(msg) => AppError::BadRequest(msg),
|
||||
ServiceError::StateMismatch => {
|
||||
AppError::BadRequest("OAuth state mismatch — possible CSRF attack".into())
|
||||
}
|
||||
ServiceError::OAuthProvider(msg) => AppError::Internal(msg),
|
||||
ServiceError::Other(msg) => AppError::Internal(msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
-9
@@ -19,24 +19,27 @@ use tracing::instrument;
|
||||
use zesdex_entities::domain::auth::SessionId;
|
||||
|
||||
use crate::domain::service::{OAuthService, SessionService};
|
||||
use crate::infrastructure::http::dto::{
|
||||
use crate::presentation::dto::{
|
||||
CreateSessionRequest, OAuthCompleteRequest, OAuthStartRequest, OAuthStartResponse,
|
||||
OAuthTokenResponse, SessionListResponse, SessionResponse,
|
||||
};
|
||||
use crate::presentation::error::AppError;
|
||||
|
||||
/// Handle a create-session request.
|
||||
#[instrument(skip(service), fields(title = %req.title))]
|
||||
pub fn handle_create_session<S: SessionService>(
|
||||
service: &S,
|
||||
req: CreateSessionRequest,
|
||||
) -> anyhow::Result<SessionResponse> {
|
||||
) -> Result<SessionResponse, AppError> {
|
||||
let session = service.create_session(&req.title)?;
|
||||
Ok(SessionResponse { session })
|
||||
}
|
||||
|
||||
/// Handle a list-sessions request.
|
||||
#[instrument(skip(service))]
|
||||
pub fn handle_list_sessions<S: SessionService>(service: &S) -> anyhow::Result<SessionListResponse> {
|
||||
pub fn handle_list_sessions<S: SessionService>(
|
||||
service: &S,
|
||||
) -> Result<SessionListResponse, AppError> {
|
||||
let sessions = service.list_all()?;
|
||||
let total = sessions.len();
|
||||
Ok(SessionListResponse { sessions, total })
|
||||
@@ -44,9 +47,12 @@ pub fn handle_list_sessions<S: SessionService>(service: &S) -> anyhow::Result<Se
|
||||
|
||||
/// Handle an archive-session request.
|
||||
#[instrument(skip(service), fields(session_id = %id))]
|
||||
pub fn handle_archive_session<S: SessionService>(service: &S, id: &str) -> anyhow::Result<()> {
|
||||
pub fn handle_archive_session<S: SessionService>(
|
||||
service: &S,
|
||||
id: &str,
|
||||
) -> Result<(), AppError> {
|
||||
let sid = SessionId::new(id)
|
||||
.map_err(|e| anyhow::anyhow!("invalid session id: {e}"))?;
|
||||
.map_err(|e| AppError::BadRequest(format!("invalid session id: {e}")))?;
|
||||
service.archive_session(sid)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -56,7 +62,7 @@ pub fn handle_archive_session<S: SessionService>(service: &S, id: &str) -> anyho
|
||||
pub fn handle_start_oauth<O: OAuthService>(
|
||||
service: &O,
|
||||
req: OAuthStartRequest,
|
||||
) -> anyhow::Result<OAuthStartResponse> {
|
||||
) -> Result<OAuthStartResponse, AppError> {
|
||||
let (auth_url, state) = service.start_flow(&req.config, &req.redirect_uri)?;
|
||||
Ok(OAuthStartResponse { auth_url, state })
|
||||
}
|
||||
@@ -66,16 +72,18 @@ pub fn handle_start_oauth<O: OAuthService>(
|
||||
pub fn handle_complete_oauth<O: OAuthService>(
|
||||
service: &O,
|
||||
req: OAuthCompleteRequest,
|
||||
) -> anyhow::Result<OAuthTokenResponse> {
|
||||
) -> Result<OAuthTokenResponse, AppError> {
|
||||
let token = service.complete_flow(&req.config, &req.redirect_uri, &req.code, &req.state)?;
|
||||
Ok(OAuthTokenResponse { token })
|
||||
}
|
||||
|
||||
/// Handle a get-token request.
|
||||
#[instrument(skip(service))]
|
||||
pub fn handle_get_token<O: OAuthService>(service: &O) -> anyhow::Result<OAuthTokenResponse> {
|
||||
pub fn handle_get_token<O: OAuthService>(
|
||||
service: &O,
|
||||
) -> Result<OAuthTokenResponse, AppError> {
|
||||
let token = service
|
||||
.get_token()?
|
||||
.ok_or_else(|| anyhow::anyhow!("no OAuth token stored"))?;
|
||||
.ok_or_else(|| AppError::NotFound("no OAuth token stored".into()))?;
|
||||
Ok(OAuthTokenResponse { token })
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//! HTTP presentation layer — handler functions and DTOs for the IAM crate.
|
||||
//!
|
||||
//! This is the outermost ring of the Clean Architecture onion. Handlers receive
|
||||
//! domain service trait references via generics and translate between
|
||||
//! request/response DTOs and domain types. They have **no dependency** on
|
||||
//! any HTTP framework — callers (IPC server, Axum, etc.) are responsible for
|
||||
//! mapping results into actual HTTP responses.
|
||||
//!
|
||||
//! # Sub-modules
|
||||
//!
|
||||
//! - [`dto`] — request/response DTO types (JSON serialisation)
|
||||
//! - [`handlers`] — handler functions that accept service trait refs + DTOs
|
||||
//! - [`error`] — typed presentation-layer error type
|
||||
//!
|
||||
//! # Dependency rule
|
||||
//!
|
||||
//! presentation → application → domain
|
||||
//! presentation may also depend on infrastructure for wiring/composition.
|
||||
|
||||
pub mod dto;
|
||||
pub mod error;
|
||||
pub mod handlers;
|
||||
|
||||
pub use dto::{
|
||||
CreateSessionRequest, OAuthCompleteRequest, OAuthStartRequest, OAuthStartResponse,
|
||||
OAuthTokenResponse, SessionListResponse, SessionResponse,
|
||||
};
|
||||
pub use error::AppError;
|
||||
pub use handlers::{
|
||||
handle_archive_session, handle_complete_oauth, handle_create_session, handle_get_token,
|
||||
handle_list_sessions, handle_start_oauth,
|
||||
};
|
||||
Reference in New Issue
Block a user