feat: auth hardening + codebase quality sweep
Refresh tokens: RefreshToken entity, PostgresRefreshTokenRepository, login returns refresh token, POST /auth/refresh (rotation), POST /auth/logout, JWT expiry 24h→1h, configurable via with_expiry(). Route protection: require_auth middleware on protected routes, public routes split (register, login, refresh, sharing/access). Authorization: caller_id added to ReadAssetFileQuery, ReadDerivativeQuery, GetStackQuery, DeleteStackCommand with ownership checks. Admin-only gates on processing, storage, sidecar, duplicates handlers. Quality fixes: visibility filtering bypass in search(), unwrap panics in date parsing, DRY auth header parsing, centralized parsers module, email validation via email_address crate, value objects (Username, MimeType, RelativePath), domain events (UserCreated, UserDeleted, AlbumCreated, TagCreated, DuplicateDetected), postgres error mapping for constraint violations, OptionExt::or_not_found helper, in_memory_repo! macro, GetStackQuery moved to queries, album add_entry 200→201.
This commit is contained in:
31
crates/application/src/catalog/queries/get_stack.rs
Normal file
31
crates/application/src/catalog/queries/get_stack.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use domain::{
|
||||
entities::AssetStack, errors::DomainError, ports::AssetStackRepository, value_objects::SystemId,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct GetStackQuery {
|
||||
pub stack_id: SystemId,
|
||||
pub caller_id: SystemId,
|
||||
}
|
||||
|
||||
pub struct GetStackHandler {
|
||||
stack_repo: Arc<dyn AssetStackRepository>,
|
||||
}
|
||||
|
||||
impl GetStackHandler {
|
||||
pub fn new(stack_repo: Arc<dyn AssetStackRepository>) -> Self {
|
||||
Self { stack_repo }
|
||||
}
|
||||
|
||||
pub async fn execute(&self, query: GetStackQuery) -> Result<AssetStack, DomainError> {
|
||||
let stack = self
|
||||
.stack_repo
|
||||
.find_by_id(&query.stack_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("Stack not found".into()))?;
|
||||
if stack.owner_user_id != query.caller_id {
|
||||
return Err(DomainError::Forbidden("Not your stack".into()));
|
||||
}
|
||||
Ok(stack)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
pub mod get_asset;
|
||||
pub mod get_stack;
|
||||
pub mod get_timeline;
|
||||
pub mod read_asset_file;
|
||||
pub mod read_derivative;
|
||||
pub mod search_assets;
|
||||
|
||||
@@ -9,6 +9,7 @@ use std::sync::Arc;
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ReadAssetFileQuery {
|
||||
pub asset_id: SystemId,
|
||||
pub caller_id: SystemId,
|
||||
}
|
||||
|
||||
pub struct AssetFileResult {
|
||||
@@ -40,6 +41,10 @@ impl ReadAssetFileHandler {
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound(format!("Asset {} not found", query.asset_id)))?;
|
||||
|
||||
if asset.owner_user_id != query.caller_id {
|
||||
return Err(DomainError::Forbidden("Access denied".into()));
|
||||
}
|
||||
|
||||
let data = self
|
||||
.file_storage
|
||||
.read_file(&asset.source_reference.relative_path)
|
||||
|
||||
@@ -10,6 +10,7 @@ use std::sync::Arc;
|
||||
pub struct ReadDerivativeQuery {
|
||||
pub asset_id: SystemId,
|
||||
pub profile: DerivativeProfile,
|
||||
pub caller_id: SystemId,
|
||||
}
|
||||
|
||||
pub struct DerivativeFileResult {
|
||||
@@ -19,16 +20,19 @@ pub struct DerivativeFileResult {
|
||||
|
||||
pub struct ReadDerivativeHandler {
|
||||
derivative_repo: Arc<dyn DerivativeRepository>,
|
||||
asset_repo: Arc<dyn domain::ports::AssetRepository>,
|
||||
file_storage: Arc<dyn FileStoragePort>,
|
||||
}
|
||||
|
||||
impl ReadDerivativeHandler {
|
||||
pub fn new(
|
||||
derivative_repo: Arc<dyn DerivativeRepository>,
|
||||
asset_repo: Arc<dyn domain::ports::AssetRepository>,
|
||||
file_storage: Arc<dyn FileStoragePort>,
|
||||
) -> Self {
|
||||
Self {
|
||||
derivative_repo,
|
||||
asset_repo,
|
||||
file_storage,
|
||||
}
|
||||
}
|
||||
@@ -37,6 +41,15 @@ impl ReadDerivativeHandler {
|
||||
&self,
|
||||
query: ReadDerivativeQuery,
|
||||
) -> Result<DerivativeFileResult, DomainError> {
|
||||
let asset = self
|
||||
.asset_repo
|
||||
.find_by_id(&query.asset_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("Asset not found".into()))?;
|
||||
if asset.owner_user_id != query.caller_id {
|
||||
return Err(DomainError::Forbidden("Access denied".into()));
|
||||
}
|
||||
|
||||
let derivative = self
|
||||
.derivative_repo
|
||||
.find_by_asset_and_profile(&query.asset_id, query.profile)
|
||||
|
||||
31
crates/application/src/catalog/queries/search_assets.rs
Normal file
31
crates/application/src/catalog/queries/search_assets.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::{
|
||||
entities::{Asset, AssetFilters},
|
||||
errors::DomainError,
|
||||
ports::AssetRepository,
|
||||
value_objects::SystemId,
|
||||
};
|
||||
|
||||
pub struct SearchAssetsQuery {
|
||||
pub owner_id: SystemId,
|
||||
pub filters: AssetFilters,
|
||||
pub limit: u32,
|
||||
pub offset: u32,
|
||||
}
|
||||
|
||||
pub struct SearchAssetsHandler {
|
||||
asset_repo: Arc<dyn AssetRepository>,
|
||||
}
|
||||
|
||||
impl SearchAssetsHandler {
|
||||
pub fn new(asset_repo: Arc<dyn AssetRepository>) -> Self {
|
||||
Self { asset_repo }
|
||||
}
|
||||
|
||||
pub async fn execute(&self, query: SearchAssetsQuery) -> Result<Vec<Asset>, DomainError> {
|
||||
self.asset_repo
|
||||
.search(&query.owner_id, &query.filters, query.limit, query.offset)
|
||||
.await
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user