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:
2026-03-05 01:28:27 +01:00
parent c368293cd4
commit 9ca4eeddb4
25 changed files with 440 additions and 1340 deletions

View File

@@ -4,17 +4,13 @@ version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.100"
async-trait = "0.1.89"
chrono = { version = "0.4.42", features = ["serde"] }
email_address = "0.2"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.146"
thiserror = "2.0.17"
tracing = "0.1"
url = { version = "2.5", features = ["serde"] }
uuid = { version = "1.19.0", features = ["v4", "serde"] }
futures-core = "0.3"
[dev-dependencies]
tokio = { version = "1", features = ["rt", "macros"] }

View File

@@ -57,8 +57,4 @@ impl User {
}
}
/// Helper to get email as string
pub fn email_str(&self) -> &str {
self.email.as_ref()
}
}

View File

@@ -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

View File

@@ -1,5 +1,5 @@
//! Reference Repository ports (traits)
//!
//!
//! These traits define the interface for data persistence.
use async_trait::async_trait;

View File

@@ -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"));
}
}