use crate::domain::models::*; /// Specification pattern for encapsulating business rules pub trait Specification { fn is_satisfied_by(&self, candidate: &T) -> bool; fn reason_for_failure(&self, candidate: &T) -> Option; } /// Composite specifications pub struct AndSpecification { left: Box>, right: Box>, } impl AndSpecification { pub fn new(left: Box>, right: Box>) -> Self { Self { left, right } } } impl Specification for AndSpecification { fn is_satisfied_by(&self, candidate: &T) -> bool { self.left.is_satisfied_by(candidate) && self.right.is_satisfied_by(candidate) } fn reason_for_failure(&self, candidate: &T) -> Option { if !self.left.is_satisfied_by(candidate) { self.left.reason_for_failure(candidate) } else if !self.right.is_satisfied_by(candidate) { self.right.reason_for_failure(candidate) } else { None } } } /// OAuth2 Client Specifications pub struct ActiveClientSpecification; impl Specification for ActiveClientSpecification { fn is_satisfied_by(&self, client: &OAuthClient) -> bool { client.is_active } fn reason_for_failure(&self, _client: &OAuthClient) -> Option { Some("Client is not active".to_string()) } } pub struct ValidRedirectUriSpecification { redirect_uri: String, } impl ValidRedirectUriSpecification { pub fn new(redirect_uri: String) -> Self { Self { redirect_uri } } } impl Specification for ValidRedirectUriSpecification { fn is_satisfied_by(&self, client: &OAuthClient) -> bool { client.redirect_uris.contains(&self.redirect_uri) } fn reason_for_failure(&self, _client: &OAuthClient) -> Option { Some(format!("Invalid redirect_uri: {}", self.redirect_uri)) } } pub struct SupportedScopesSpecification { requested_scopes: Vec, } impl SupportedScopesSpecification { pub fn new(requested_scopes: Vec) -> Self { Self { requested_scopes } } } impl Specification for SupportedScopesSpecification { fn is_satisfied_by(&self, client: &OAuthClient) -> bool { self.requested_scopes .iter() .all(|scope| client.scopes.contains(scope)) } fn reason_for_failure(&self, client: &OAuthClient) -> Option { let unsupported: Vec<_> = self .requested_scopes .iter() .filter(|scope| !client.scopes.contains(scope)) .cloned() .collect(); if !unsupported.is_empty() { Some(format!("Unsupported scopes: {}", unsupported.join(", "))) } else { None } } } /// Authorization Code Specifications pub struct UnusedAuthCodeSpecification; impl Specification for UnusedAuthCodeSpecification { fn is_satisfied_by(&self, code: &AuthorizationCode) -> bool { !code.is_used } fn reason_for_failure(&self, _code: &AuthorizationCode) -> Option { Some("Authorization code has already been used".to_string()) } } pub struct ValidAuthCodeSpecification; impl Specification for ValidAuthCodeSpecification { fn is_satisfied_by(&self, code: &AuthorizationCode) -> bool { chrono::Utc::now() < code.expires_at } fn reason_for_failure(&self, _code: &AuthorizationCode) -> Option { Some("Authorization code has expired".to_string()) } } pub struct MatchingClientSpecification { client_id: String, } impl MatchingClientSpecification { pub fn new(client_id: String) -> Self { Self { client_id } } } impl Specification for MatchingClientSpecification { fn is_satisfied_by(&self, code: &AuthorizationCode) -> bool { code.client_id == self.client_id } fn reason_for_failure(&self, _code: &AuthorizationCode) -> Option { Some("Client ID mismatch".to_string()) } } /// PKCE Specifications pub struct ValidPkceSpecification { code_verifier: String, } impl ValidPkceSpecification { pub fn new(code_verifier: String) -> Self { Self { code_verifier } } } impl Specification for ValidPkceSpecification { fn is_satisfied_by(&self, code: &AuthorizationCode) -> bool { if let Some(challenge) = &code.code_challenge { let method = code.code_challenge_method.as_deref().unwrap_or("plain"); crate::oauth::pkce::verify_code_challenge( &self.code_verifier, challenge, &crate::oauth::pkce::CodeChallengeMethod::from_str(method) .unwrap_or(crate::oauth::pkce::CodeChallengeMethod::Plain), ) .is_ok() } else { true // No PKCE required } } fn reason_for_failure(&self, _code: &AuthorizationCode) -> Option { Some("PKCE verification failed".to_string()) } } /// Access Token Specifications pub struct ValidTokenSpecification; impl Specification for ValidTokenSpecification { fn is_satisfied_by(&self, token: &AccessToken) -> bool { !token.is_revoked && chrono::Utc::now() < token.expires_at } fn reason_for_failure(&self, token: &AccessToken) -> Option { if token.is_revoked { Some("Token has been revoked".to_string()) } else if chrono::Utc::now() >= token.expires_at { Some("Token has expired".to_string()) } else { None } } } /// Helper trait for chaining specifications pub trait SpecificationExt: Specification + Sized + 'static { fn and(self, other: impl Specification + 'static) -> AndSpecification { AndSpecification::new(Box::new(self), Box::new(other)) } } impl + 'static> SpecificationExt for S {}