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:
asepharyana
2026-07-20 06:53:01 +07:00
parent e9a8e93c83
commit 714b4617dd
22 changed files with 465 additions and 175 deletions
+30
View File
@@ -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(),
}
}
}
+1
View File
@@ -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;
+1
View File
@@ -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),
}
}
}
@@ -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 })
}
+32
View File
@@ -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,
};