feat: enhance application state management with cookie support
- Added cookie key to AppState for managing session cookies. - Updated AppState initialization to derive cookie key from configuration. - Removed session-based authentication option from cargo-generate prompts. - Refactored JWT authentication logic to improve clarity and error handling. - Updated password validation to align with NIST recommendations (minimum length increased). - Removed unused session store implementation and related code. - Improved error handling in user repository for unique constraint violations. - Refactored OIDC service to include state management for authentication flow. - Cleaned up dependencies in Cargo.toml and Cargo.toml.template for clarity and efficiency.
This commit is contained in:
@@ -57,8 +57,4 @@ impl User {
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to get email as string
|
||||
pub fn email_str(&self) -> &str {
|
||||
self.email.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use uuid::Uuid;
|
||||
|
||||
/// Domain-level errors for K-Notes operations
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum DomainError {
|
||||
/// The requested user was not found
|
||||
#[error("User not found: {0}")]
|
||||
@@ -22,9 +23,13 @@ pub enum DomainError {
|
||||
#[error("Validation error: {0}")]
|
||||
ValidationError(String),
|
||||
|
||||
/// User is not authorized to perform this action
|
||||
#[error("Unauthorized: {0}")]
|
||||
Unauthorized(String),
|
||||
/// User is not authenticated (maps to HTTP 401)
|
||||
#[error("Unauthenticated: {0}")]
|
||||
Unauthenticated(String),
|
||||
|
||||
/// User is not allowed to perform this action (maps to HTTP 403)
|
||||
#[error("Forbidden: {0}")]
|
||||
Forbidden(String),
|
||||
|
||||
/// A repository/infrastructure error occurred
|
||||
#[error("Repository error: {0}")]
|
||||
@@ -41,9 +46,14 @@ impl DomainError {
|
||||
Self::ValidationError(message.into())
|
||||
}
|
||||
|
||||
/// Create an unauthorized error
|
||||
pub fn unauthorized(message: impl Into<String>) -> Self {
|
||||
Self::Unauthorized(message.into())
|
||||
/// Create an unauthenticated error (not logged in → 401)
|
||||
pub fn unauthenticated(message: impl Into<String>) -> Self {
|
||||
Self::Unauthenticated(message.into())
|
||||
}
|
||||
|
||||
/// Create a forbidden error (not allowed → 403)
|
||||
pub fn forbidden(message: impl Into<String>) -> Self {
|
||||
Self::Forbidden(message.into())
|
||||
}
|
||||
|
||||
/// Check if this error indicates a "not found" condition
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Reference Repository ports (traits)
|
||||
//!
|
||||
//!
|
||||
//! These traits define the interface for data persistence.
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
@@ -17,6 +17,7 @@ pub type UserId = Uuid;
|
||||
|
||||
/// Errors that occur when parsing/validating value objects
|
||||
#[derive(Debug, Error, Clone, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum ValidationError {
|
||||
#[error("Invalid email format: {0}")]
|
||||
InvalidEmail(String),
|
||||
@@ -109,8 +110,8 @@ impl<'de> Deserialize<'de> for Email {
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct Password(String);
|
||||
|
||||
/// Minimum password length
|
||||
pub const MIN_PASSWORD_LENGTH: usize = 6;
|
||||
/// Minimum password length (NIST recommendation)
|
||||
pub const MIN_PASSWORD_LENGTH: usize = 8;
|
||||
|
||||
impl Password {
|
||||
pub fn new(value: impl Into<String>) -> Result<Self, ValidationError> {
|
||||
@@ -497,82 +498,6 @@ pub struct AuthorizationUrlData {
|
||||
// Configuration Newtypes
|
||||
// ============================================================================
|
||||
|
||||
/// Database connection URL
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
pub struct DatabaseUrl(String);
|
||||
|
||||
impl DatabaseUrl {
|
||||
pub fn new(value: impl Into<String>) -> Result<Self, ValidationError> {
|
||||
let value = value.into();
|
||||
if value.trim().is_empty() {
|
||||
return Err(ValidationError::Empty("database_url".to_string()));
|
||||
}
|
||||
Ok(Self(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for DatabaseUrl {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DatabaseUrl {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for DatabaseUrl {
|
||||
type Error = ValidationError;
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DatabaseUrl> for String {
|
||||
fn from(val: DatabaseUrl) -> Self {
|
||||
val.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Session secret with minimum length requirement
|
||||
pub const MIN_SESSION_SECRET_LENGTH: usize = 64;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct SessionSecret(String);
|
||||
|
||||
impl SessionSecret {
|
||||
pub fn new(value: impl Into<String>) -> Result<Self, ValidationError> {
|
||||
let value = value.into();
|
||||
if value.len() < MIN_SESSION_SECRET_LENGTH {
|
||||
return Err(ValidationError::SecretTooShort {
|
||||
min: MIN_SESSION_SECRET_LENGTH,
|
||||
actual: value.len(),
|
||||
});
|
||||
}
|
||||
Ok(Self(value))
|
||||
}
|
||||
|
||||
/// Create without validation (for development/testing)
|
||||
pub fn new_unchecked(value: impl Into<String>) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for SessionSecret {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for SessionSecret {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "SessionSecret(***)")
|
||||
}
|
||||
}
|
||||
|
||||
/// JWT signing secret with minimum length requirement
|
||||
pub const MIN_JWT_SECRET_LENGTH: usize = 32;
|
||||
|
||||
@@ -655,12 +580,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_valid_password() {
|
||||
assert!(Password::new("secret123").is_ok());
|
||||
assert!(Password::new("123456").is_ok()); // Exactly 6 chars
|
||||
assert!(Password::new("12345678").is_ok()); // Exactly 8 chars
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_password_too_short() {
|
||||
assert!(Password::new("12345").is_err()); // 5 chars
|
||||
assert!(Password::new("1234567").is_err()); // 7 chars
|
||||
assert!(Password::new("").is_err());
|
||||
}
|
||||
|
||||
@@ -705,15 +630,6 @@ mod tests {
|
||||
mod secret_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_session_secret_min_length() {
|
||||
let short = "short";
|
||||
let long = "a".repeat(64);
|
||||
|
||||
assert!(SessionSecret::new(short).is_err());
|
||||
assert!(SessionSecret::new(long).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_jwt_secret_production_check() {
|
||||
let short = "short";
|
||||
@@ -729,10 +645,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_secrets_hide_in_debug() {
|
||||
let session = SessionSecret::new_unchecked("secret");
|
||||
let jwt = JwtSecret::new_unchecked("secret");
|
||||
|
||||
assert!(!format!("{:?}", session).contains("secret"));
|
||||
assert!(!format!("{:?}", jwt).contains("secret"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user