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:
81
crates/application/src/catalog/commands/create_stack.rs
Normal file
81
crates/application/src/catalog/commands/create_stack.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::{
|
||||
entities::{AssetStack, StackMemberRole, StackType},
|
||||
errors::DomainError,
|
||||
ports::{AssetRepository, AssetStackRepository},
|
||||
value_objects::SystemId,
|
||||
};
|
||||
|
||||
pub struct CreateStackCommand {
|
||||
pub stack_type: StackType,
|
||||
pub primary_asset_id: SystemId,
|
||||
pub additional_asset_ids: Vec<(SystemId, StackMemberRole)>,
|
||||
pub owner_id: SystemId,
|
||||
}
|
||||
|
||||
pub struct CreateStackHandler {
|
||||
asset_repo: Arc<dyn AssetRepository>,
|
||||
stack_repo: Arc<dyn AssetStackRepository>,
|
||||
}
|
||||
|
||||
impl CreateStackHandler {
|
||||
pub fn new(
|
||||
asset_repo: Arc<dyn AssetRepository>,
|
||||
stack_repo: Arc<dyn AssetStackRepository>,
|
||||
) -> Self {
|
||||
Self {
|
||||
asset_repo,
|
||||
stack_repo,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute(&self, cmd: CreateStackCommand) -> Result<AssetStack, DomainError> {
|
||||
self.asset_repo
|
||||
.find_by_id(&cmd.primary_asset_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("Primary asset not found".into()))?;
|
||||
|
||||
let mut stack = AssetStack::new(cmd.stack_type, cmd.primary_asset_id, cmd.owner_id);
|
||||
|
||||
for (asset_id, role) in cmd.additional_asset_ids {
|
||||
self.asset_repo
|
||||
.find_by_id(&asset_id)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
DomainError::NotFound(format!("Asset {} not found", asset_id.as_uuid()))
|
||||
})?;
|
||||
stack.add_member(asset_id, role)?;
|
||||
}
|
||||
|
||||
self.stack_repo.save(&stack).await?;
|
||||
Ok(stack)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DeleteStackCommand {
|
||||
pub stack_id: SystemId,
|
||||
pub caller_id: SystemId,
|
||||
}
|
||||
|
||||
pub struct DeleteStackHandler {
|
||||
stack_repo: Arc<dyn AssetStackRepository>,
|
||||
}
|
||||
|
||||
impl DeleteStackHandler {
|
||||
pub fn new(stack_repo: Arc<dyn AssetStackRepository>) -> Self {
|
||||
Self { stack_repo }
|
||||
}
|
||||
|
||||
pub async fn execute(&self, cmd: DeleteStackCommand) -> Result<(), DomainError> {
|
||||
let stack = self
|
||||
.stack_repo
|
||||
.find_by_id(&cmd.stack_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("Stack not found".into()))?;
|
||||
if stack.owner_user_id != cmd.caller_id {
|
||||
return Err(DomainError::Forbidden("Not your stack".into()));
|
||||
}
|
||||
self.stack_repo.delete(&cmd.stack_id).await
|
||||
}
|
||||
}
|
||||
84
crates/application/src/catalog/commands/delete_asset.rs
Normal file
84
crates/application/src/catalog/commands/delete_asset.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
events::DomainEvent,
|
||||
ports::{
|
||||
AssetRepository, DerivativeRepository, EventPublisher, FileStoragePort, SidecarRepository,
|
||||
},
|
||||
value_objects::{DateTimeStamp, SystemId},
|
||||
};
|
||||
|
||||
pub struct DeleteAssetCommand {
|
||||
pub asset_id: SystemId,
|
||||
pub deleted_by: SystemId,
|
||||
}
|
||||
|
||||
pub struct DeleteAssetHandler {
|
||||
asset_repo: Arc<dyn AssetRepository>,
|
||||
derivative_repo: Arc<dyn DerivativeRepository>,
|
||||
sidecar_repo: Arc<dyn SidecarRepository>,
|
||||
file_storage: Arc<dyn FileStoragePort>,
|
||||
event_publisher: Arc<dyn EventPublisher>,
|
||||
}
|
||||
|
||||
impl DeleteAssetHandler {
|
||||
pub fn new(
|
||||
asset_repo: Arc<dyn AssetRepository>,
|
||||
derivative_repo: Arc<dyn DerivativeRepository>,
|
||||
sidecar_repo: Arc<dyn SidecarRepository>,
|
||||
file_storage: Arc<dyn FileStoragePort>,
|
||||
event_publisher: Arc<dyn EventPublisher>,
|
||||
) -> Self {
|
||||
Self {
|
||||
asset_repo,
|
||||
derivative_repo,
|
||||
sidecar_repo,
|
||||
file_storage,
|
||||
event_publisher,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute(&self, cmd: DeleteAssetCommand) -> Result<(), DomainError> {
|
||||
let asset = self
|
||||
.asset_repo
|
||||
.find_by_id(&cmd.asset_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("Asset not found".into()))?;
|
||||
|
||||
// Delete derivative files + DB records
|
||||
let derivatives = self.derivative_repo.find_by_asset(&cmd.asset_id).await?;
|
||||
for d in &derivatives {
|
||||
let _ = self.file_storage.delete_file(&d.storage_path).await;
|
||||
self.derivative_repo.delete(&d.derivative_id).await?;
|
||||
}
|
||||
|
||||
// Delete sidecar file + DB record
|
||||
if let Some(sidecar) = self.sidecar_repo.find_by_asset(&cmd.asset_id).await? {
|
||||
let _ = self
|
||||
.file_storage
|
||||
.delete_file(&sidecar.sidecar_storage_path)
|
||||
.await;
|
||||
self.sidecar_repo.delete(&cmd.asset_id).await?;
|
||||
}
|
||||
|
||||
// Delete asset file
|
||||
let _ = self
|
||||
.file_storage
|
||||
.delete_file(&asset.source_reference.relative_path)
|
||||
.await;
|
||||
|
||||
// Delete asset DB record
|
||||
self.asset_repo.delete(&cmd.asset_id).await?;
|
||||
|
||||
self.event_publisher
|
||||
.publish(&DomainEvent::AssetDeleted {
|
||||
asset_id: cmd.asset_id,
|
||||
deleted_by: cmd.deleted_by,
|
||||
timestamp: DateTimeStamp::now(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::{
|
||||
entities::{AssetStack, AssetType, StackMemberRole, StackType},
|
||||
errors::DomainError,
|
||||
ports::{AssetRepository, AssetStackRepository},
|
||||
value_objects::SystemId,
|
||||
};
|
||||
|
||||
pub struct DetectLivePhotosCommand {
|
||||
pub owner_id: SystemId,
|
||||
}
|
||||
|
||||
pub struct DetectLivePhotosHandler {
|
||||
asset_repo: Arc<dyn AssetRepository>,
|
||||
stack_repo: Arc<dyn AssetStackRepository>,
|
||||
}
|
||||
|
||||
impl DetectLivePhotosHandler {
|
||||
pub fn new(
|
||||
asset_repo: Arc<dyn AssetRepository>,
|
||||
stack_repo: Arc<dyn AssetStackRepository>,
|
||||
) -> Self {
|
||||
Self {
|
||||
asset_repo,
|
||||
stack_repo,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute(
|
||||
&self,
|
||||
cmd: DetectLivePhotosCommand,
|
||||
) -> Result<Vec<AssetStack>, DomainError> {
|
||||
let assets = self
|
||||
.asset_repo
|
||||
.find_by_owner(&cmd.owner_id, 10_000, 0)
|
||||
.await?;
|
||||
|
||||
let mut by_basename: HashMap<String, Vec<(SystemId, AssetType, String)>> = HashMap::new();
|
||||
|
||||
for asset in &assets {
|
||||
let path = &asset.source_reference.relative_path;
|
||||
if let Some(stem) = std::path::Path::new(path)
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
{
|
||||
let key = stem.to_lowercase();
|
||||
by_basename.entry(key).or_default().push((
|
||||
asset.asset_id,
|
||||
asset.asset_type,
|
||||
asset.mime_type.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let mut created = Vec::new();
|
||||
|
||||
for group in by_basename.values() {
|
||||
if group.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let image = group.iter().find(|(_, t, _)| *t == AssetType::Image);
|
||||
let video = group
|
||||
.iter()
|
||||
.find(|(_, t, m)| *t == AssetType::Video || m.starts_with("video/"));
|
||||
|
||||
if let (Some((img_id, _, _)), Some((vid_id, _, _))) = (image, video) {
|
||||
let existing = self.stack_repo.find_by_asset(img_id).await?;
|
||||
if existing
|
||||
.iter()
|
||||
.any(|s| s.stack_type == StackType::LivePhoto)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut stack = AssetStack::new(StackType::LivePhoto, *img_id, cmd.owner_id);
|
||||
stack.add_member(*vid_id, StackMemberRole::MotionClip)?;
|
||||
self.stack_repo.save(&stack).await?;
|
||||
created.push(stack);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(created)
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,6 @@
|
||||
pub mod create_stack;
|
||||
pub mod delete_asset;
|
||||
pub mod detect_live_photos;
|
||||
pub mod register_asset;
|
||||
pub mod resolve_duplicate;
|
||||
pub mod update_metadata;
|
||||
|
||||
86
crates/application/src/catalog/commands/resolve_duplicate.rs
Normal file
86
crates/application/src/catalog/commands/resolve_duplicate.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::{errors::DomainError, ports::DuplicateRepository, value_objects::SystemId};
|
||||
|
||||
use super::delete_asset::{DeleteAssetCommand, DeleteAssetHandler};
|
||||
|
||||
pub struct ResolveDuplicateCommand {
|
||||
pub group_id: SystemId,
|
||||
pub keep_asset_id: SystemId,
|
||||
pub resolved_by: SystemId,
|
||||
}
|
||||
|
||||
pub struct ResolveDuplicateHandler {
|
||||
duplicate_repo: Arc<dyn DuplicateRepository>,
|
||||
delete_handler: Arc<DeleteAssetHandler>,
|
||||
}
|
||||
|
||||
impl ResolveDuplicateHandler {
|
||||
pub fn new(
|
||||
duplicate_repo: Arc<dyn DuplicateRepository>,
|
||||
delete_handler: Arc<DeleteAssetHandler>,
|
||||
) -> Self {
|
||||
Self {
|
||||
duplicate_repo,
|
||||
delete_handler,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute(&self, cmd: ResolveDuplicateCommand) -> Result<(), DomainError> {
|
||||
let mut group = self
|
||||
.duplicate_repo
|
||||
.find_by_id(&cmd.group_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("Duplicate group not found".into()))?;
|
||||
|
||||
if !group
|
||||
.candidates
|
||||
.iter()
|
||||
.any(|c| c.asset_id == cmd.keep_asset_id)
|
||||
{
|
||||
return Err(DomainError::Validation(
|
||||
"keep_asset_id not in duplicate group".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let to_delete: Vec<SystemId> = group
|
||||
.candidates
|
||||
.iter()
|
||||
.filter(|c| c.asset_id != cmd.keep_asset_id)
|
||||
.map(|c| c.asset_id)
|
||||
.collect();
|
||||
|
||||
for asset_id in to_delete {
|
||||
self.delete_handler
|
||||
.execute(DeleteAssetCommand {
|
||||
asset_id,
|
||||
deleted_by: cmd.resolved_by,
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
group.resolve();
|
||||
self.duplicate_repo.save(&group).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ListDuplicatesQuery;
|
||||
|
||||
pub struct ListDuplicatesHandler {
|
||||
duplicate_repo: Arc<dyn DuplicateRepository>,
|
||||
}
|
||||
|
||||
impl ListDuplicatesHandler {
|
||||
pub fn new(duplicate_repo: Arc<dyn DuplicateRepository>) -> Self {
|
||||
Self { duplicate_repo }
|
||||
}
|
||||
|
||||
pub async fn execute(
|
||||
&self,
|
||||
_query: ListDuplicatesQuery,
|
||||
) -> Result<Vec<domain::entities::DuplicateGroup>, DomainError> {
|
||||
self.duplicate_repo.find_unresolved().await
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,22 @@ pub mod commands;
|
||||
pub mod queries;
|
||||
pub mod visibility;
|
||||
|
||||
pub use commands::create_stack::{
|
||||
CreateStackCommand, CreateStackHandler, DeleteStackCommand, DeleteStackHandler,
|
||||
};
|
||||
pub use commands::delete_asset::{DeleteAssetCommand, DeleteAssetHandler};
|
||||
pub use commands::detect_live_photos::{DetectLivePhotosCommand, DetectLivePhotosHandler};
|
||||
pub use commands::register_asset::{RegisterAssetCommand, RegisterAssetHandler};
|
||||
pub use commands::resolve_duplicate::{
|
||||
ListDuplicatesHandler, ListDuplicatesQuery, ResolveDuplicateCommand, ResolveDuplicateHandler,
|
||||
};
|
||||
pub use commands::update_metadata::{UpdateMetadataCommand, UpdateMetadataHandler};
|
||||
pub use queries::get_asset::{GetAssetHandler, GetAssetQuery};
|
||||
pub use queries::get_stack::{GetStackHandler, GetStackQuery};
|
||||
pub use queries::get_timeline::{GetTimelineHandler, GetTimelineQuery};
|
||||
pub use queries::read_asset_file::{AssetFileResult, ReadAssetFileHandler, ReadAssetFileQuery};
|
||||
pub use queries::read_derivative::{
|
||||
DerivativeFileResult, ReadDerivativeHandler, ReadDerivativeQuery,
|
||||
};
|
||||
pub use queries::search_assets::{SearchAssetsHandler, SearchAssetsQuery};
|
||||
pub use visibility::VisibilityFilteredAssetRepository;
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use async_trait::async_trait;
|
||||
use domain::{
|
||||
catalog::entities::Asset,
|
||||
catalog::entities::{Asset, AssetFilters},
|
||||
errors::DomainError,
|
||||
ports::{AssetRepository, ShareRepository},
|
||||
value_objects::{Checksum, SystemId},
|
||||
@@ -112,6 +112,27 @@ impl AssetRepository for VisibilityFilteredAssetRepository {
|
||||
Ok(visible)
|
||||
}
|
||||
|
||||
async fn search(
|
||||
&self,
|
||||
owner_id: &SystemId,
|
||||
filters: &AssetFilters,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
) -> Result<Vec<Asset>, DomainError> {
|
||||
if owner_id == &self.caller_id {
|
||||
return self.inner.search(owner_id, filters, limit, offset).await;
|
||||
}
|
||||
|
||||
let assets = self.inner.search(owner_id, filters, limit, offset).await?;
|
||||
let mut visible = Vec::with_capacity(assets.len());
|
||||
for asset in assets {
|
||||
if self.caller_can_access(&asset).await? {
|
||||
visible.push(asset);
|
||||
}
|
||||
}
|
||||
Ok(visible)
|
||||
}
|
||||
|
||||
async fn save(&self, asset: &Asset) -> Result<(), DomainError> {
|
||||
self.inner.save(asset).await
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use domain::{
|
||||
entities::User,
|
||||
entities::{RefreshToken, User},
|
||||
errors::DomainError,
|
||||
ports::{PasswordHasher, TokenIssuer, UserRepository},
|
||||
value_objects::Email,
|
||||
ports::{PasswordHasher, RefreshTokenRepository, TokenIssuer, UserRepository},
|
||||
value_objects::{DateTimeStamp, Email},
|
||||
};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
@@ -16,6 +17,7 @@ pub struct LoginUserHandler {
|
||||
repo: Arc<dyn UserRepository>,
|
||||
hasher: Arc<dyn PasswordHasher>,
|
||||
issuer: Arc<dyn TokenIssuer>,
|
||||
refresh_repo: Arc<dyn RefreshTokenRepository>,
|
||||
}
|
||||
|
||||
impl LoginUserHandler {
|
||||
@@ -23,15 +25,20 @@ impl LoginUserHandler {
|
||||
repo: Arc<dyn UserRepository>,
|
||||
hasher: Arc<dyn PasswordHasher>,
|
||||
issuer: Arc<dyn TokenIssuer>,
|
||||
refresh_repo: Arc<dyn RefreshTokenRepository>,
|
||||
) -> Self {
|
||||
Self {
|
||||
repo,
|
||||
hasher,
|
||||
issuer,
|
||||
refresh_repo,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute(&self, cmd: LoginUserCommand) -> Result<(User, String), DomainError> {
|
||||
pub async fn execute(
|
||||
&self,
|
||||
cmd: LoginUserCommand,
|
||||
) -> Result<(User, String, String), DomainError> {
|
||||
let email = Email::new(&cmd.email)?;
|
||||
let user = self
|
||||
.repo
|
||||
@@ -45,7 +52,21 @@ impl LoginUserHandler {
|
||||
if !valid {
|
||||
return Err(DomainError::Unauthorized("Invalid credentials".to_string()));
|
||||
}
|
||||
let token = self.issuer.issue(&user.id, "user").await?;
|
||||
Ok((user, token))
|
||||
let access_token = self.issuer.issue(&user.id, "user").await?;
|
||||
let (raw_refresh, _) = generate_refresh_token(&self.refresh_repo, &user.id).await?;
|
||||
Ok((user, access_token, raw_refresh))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn generate_refresh_token(
|
||||
repo: &Arc<dyn RefreshTokenRepository>,
|
||||
user_id: &domain::value_objects::SystemId,
|
||||
) -> Result<(String, domain::value_objects::SystemId), DomainError> {
|
||||
let raw = uuid::Uuid::new_v4().to_string();
|
||||
let hash = format!("{:x}", Sha256::digest(raw.as_bytes()));
|
||||
let expires_at = DateTimeStamp::from_datetime(chrono::Utc::now() + chrono::Duration::days(30));
|
||||
let token = RefreshToken::new(*user_id, hash, expires_at);
|
||||
let token_id = token.token_id;
|
||||
repo.save(&token).await?;
|
||||
Ok((raw, token_id))
|
||||
}
|
||||
|
||||
16
crates/application/src/identity/commands/logout.rs
Normal file
16
crates/application/src/identity/commands/logout.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use domain::{errors::DomainError, ports::RefreshTokenRepository, value_objects::SystemId};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct LogoutHandler {
|
||||
refresh_repo: Arc<dyn RefreshTokenRepository>,
|
||||
}
|
||||
|
||||
impl LogoutHandler {
|
||||
pub fn new(refresh_repo: Arc<dyn RefreshTokenRepository>) -> Self {
|
||||
Self { refresh_repo }
|
||||
}
|
||||
|
||||
pub async fn execute(&self, user_id: &SystemId) -> Result<(), DomainError> {
|
||||
self.refresh_repo.delete_by_user(user_id).await
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
pub mod login_user;
|
||||
pub mod logout;
|
||||
pub mod refresh_token;
|
||||
pub mod register_user;
|
||||
|
||||
pub use login_user::{LoginUserCommand, LoginUserHandler};
|
||||
pub use logout::LogoutHandler;
|
||||
pub use refresh_token::{RefreshTokenCommand, RefreshTokenHandler};
|
||||
pub use register_user::{RegisterUserCommand, RegisterUserHandler};
|
||||
|
||||
53
crates/application/src/identity/commands/refresh_token.rs
Normal file
53
crates/application/src/identity/commands/refresh_token.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use super::login_user::generate_refresh_token;
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
ports::{RefreshTokenRepository, TokenIssuer},
|
||||
};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct RefreshTokenCommand {
|
||||
pub refresh_token: String,
|
||||
}
|
||||
|
||||
pub struct RefreshTokenHandler {
|
||||
refresh_repo: Arc<dyn RefreshTokenRepository>,
|
||||
issuer: Arc<dyn TokenIssuer>,
|
||||
}
|
||||
|
||||
impl RefreshTokenHandler {
|
||||
pub fn new(
|
||||
refresh_repo: Arc<dyn RefreshTokenRepository>,
|
||||
issuer: Arc<dyn TokenIssuer>,
|
||||
) -> Self {
|
||||
Self {
|
||||
refresh_repo,
|
||||
issuer,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute(&self, cmd: RefreshTokenCommand) -> Result<(String, String), DomainError> {
|
||||
let hash = format!("{:x}", Sha256::digest(cmd.refresh_token.as_bytes()));
|
||||
|
||||
let token = self
|
||||
.refresh_repo
|
||||
.find_by_hash(&hash)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::Unauthorized("Invalid refresh token".to_string()))?;
|
||||
|
||||
if !token.is_valid() {
|
||||
return Err(DomainError::Unauthorized(
|
||||
"Refresh token expired or revoked".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Rotation: delete old, issue new pair
|
||||
self.refresh_repo.delete(&token.token_id).await?;
|
||||
|
||||
let access_token = self.issuer.issue(&token.user_id, "user").await?;
|
||||
let (raw_refresh, _) = generate_refresh_token(&self.refresh_repo, &token.user_id).await?;
|
||||
|
||||
Ok((access_token, raw_refresh))
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
pub mod commands;
|
||||
pub mod queries;
|
||||
|
||||
pub use commands::{LoginUserCommand, LoginUserHandler, RegisterUserCommand, RegisterUserHandler};
|
||||
pub use commands::{
|
||||
LoginUserCommand, LoginUserHandler, LogoutHandler, RefreshTokenCommand, RefreshTokenHandler,
|
||||
RegisterUserCommand, RegisterUserHandler, login_user::generate_refresh_token,
|
||||
};
|
||||
pub use queries::{GetProfileHandler, GetProfileQuery};
|
||||
|
||||
@@ -11,6 +11,7 @@ pub use commands::fail_job::{FailJobCommand, FailJobHandler};
|
||||
pub use commands::manage_plugin::{ManagePluginCommand, ManagePluginHandler, PluginAction};
|
||||
pub use commands::process_next_job::{ProcessNextJobCommand, ProcessNextJobHandler};
|
||||
pub use commands::start_job::{StartJobCommand, StartJobHandler};
|
||||
pub use queries::list_jobs::{JobListResult, ListJobsHandler, ListJobsQuery};
|
||||
pub use queries::report_batch_progress::{
|
||||
BatchProgress, ReportBatchProgressHandler, ReportBatchProgressQuery,
|
||||
};
|
||||
|
||||
34
crates/application/src/processing/queries/list_jobs.rs
Normal file
34
crates/application/src/processing/queries/list_jobs.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::{entities::Job, errors::DomainError, ports::JobRepository};
|
||||
|
||||
pub struct ListJobsQuery {
|
||||
pub status: Option<String>,
|
||||
pub limit: u32,
|
||||
pub offset: u32,
|
||||
}
|
||||
|
||||
pub struct JobListResult {
|
||||
pub jobs: Vec<Job>,
|
||||
pub total: u64,
|
||||
}
|
||||
|
||||
pub struct ListJobsHandler {
|
||||
job_repo: Arc<dyn JobRepository>,
|
||||
}
|
||||
|
||||
impl ListJobsHandler {
|
||||
pub fn new(job_repo: Arc<dyn JobRepository>) -> Self {
|
||||
Self { job_repo }
|
||||
}
|
||||
|
||||
pub async fn execute(&self, query: ListJobsQuery) -> Result<JobListResult, DomainError> {
|
||||
let status_ref = query.status.as_deref();
|
||||
let jobs = self
|
||||
.job_repo
|
||||
.find_all(status_ref, query.limit, query.offset)
|
||||
.await?;
|
||||
let total = self.job_repo.count(status_ref).await?;
|
||||
Ok(JobListResult { jobs, total })
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod list_jobs;
|
||||
pub mod report_batch_progress;
|
||||
|
||||
@@ -1,24 +1,46 @@
|
||||
use async_trait::async_trait;
|
||||
use domain::{
|
||||
entities::{
|
||||
Album, Asset, AssetMetadata, AssetTag, DuplicateGroup, DuplicateStatus, Group,
|
||||
IngestSession, InviteCode, Job, JobBatch, JobStatus, LibraryPath, MetadataSource, Plugin,
|
||||
ProcessingPipeline, QuotaDefinition, Role, ShareLink, ShareScope, ShareTarget,
|
||||
SidecarRecord, StorageVolume, SyncStatus, Tag, UsageLedgerEntry, UsageType, User,
|
||||
Album, Asset, AssetFilters, AssetMetadata, AssetTag, DuplicateGroup, DuplicateStatus,
|
||||
Group, IngestSession, InviteCode, Job, JobBatch, JobStatus, LibraryPath, MetadataSource,
|
||||
Plugin, ProcessingPipeline, QuotaDefinition, RefreshToken, Role, ShareLink, ShareScope,
|
||||
ShareTarget, SidecarRecord, StorageVolume, SyncStatus, Tag, UsageLedgerEntry, UsageType,
|
||||
User,
|
||||
},
|
||||
errors::DomainError,
|
||||
ports::{
|
||||
AlbumRepository, AssetMetadataRepository, AssetRepository, DuplicateRepository,
|
||||
GroupRepository, IngestSessionRepository, IngestTransaction, JobBatchRepository,
|
||||
JobRepository, LibraryPathRepository, PipelineRepository, PluginRepository,
|
||||
QuotaRepository, RoleRepository, ShareRepository, SidecarRepository,
|
||||
StorageVolumeRepository, TagRepository, UsageLedgerRepository, UserRepository,
|
||||
QuotaRepository, RefreshTokenRepository, RoleRepository, ShareRepository,
|
||||
SidecarRepository, StorageVolumeRepository, TagRepository, UsageLedgerRepository,
|
||||
UserRepository,
|
||||
},
|
||||
value_objects::{Checksum, DateTimeStamp, Email, SystemId},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
macro_rules! in_memory_repo {
|
||||
($name:ident, $entity:ty) => {
|
||||
pub struct $name {
|
||||
data: Mutex<HashMap<String, $entity>>,
|
||||
}
|
||||
impl $name {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Default for $name {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// --- InMemoryUserRepository ---
|
||||
|
||||
pub struct InMemoryUserRepository {
|
||||
@@ -83,25 +105,7 @@ impl UserRepository for InMemoryUserRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// --- InMemoryAssetRepository ---
|
||||
|
||||
pub struct InMemoryAssetRepository {
|
||||
data: Mutex<HashMap<String, Asset>>,
|
||||
}
|
||||
|
||||
impl InMemoryAssetRepository {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InMemoryAssetRepository {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
in_memory_repo!(InMemoryAssetRepository, Asset);
|
||||
|
||||
#[async_trait]
|
||||
impl AssetRepository for InMemoryAssetRepository {
|
||||
@@ -141,6 +145,16 @@ impl AssetRepository for InMemoryAssetRepository {
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn search(
|
||||
&self,
|
||||
owner_id: &SystemId,
|
||||
_filters: &AssetFilters,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
) -> Result<Vec<Asset>, DomainError> {
|
||||
self.find_by_owner(owner_id, limit, offset).await
|
||||
}
|
||||
|
||||
async fn save(&self, asset: &Asset) -> Result<(), DomainError> {
|
||||
self.data
|
||||
.lock()
|
||||
@@ -155,25 +169,7 @@ impl AssetRepository for InMemoryAssetRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// --- InMemoryAlbumRepository ---
|
||||
|
||||
pub struct InMemoryAlbumRepository {
|
||||
data: Mutex<HashMap<String, Album>>,
|
||||
}
|
||||
|
||||
impl InMemoryAlbumRepository {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InMemoryAlbumRepository {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
in_memory_repo!(InMemoryAlbumRepository, Album);
|
||||
|
||||
#[async_trait]
|
||||
impl AlbumRepository for InMemoryAlbumRepository {
|
||||
@@ -206,25 +202,7 @@ impl AlbumRepository for InMemoryAlbumRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// --- InMemoryJobRepository ---
|
||||
|
||||
pub struct InMemoryJobRepository {
|
||||
data: Mutex<HashMap<String, Job>>,
|
||||
}
|
||||
|
||||
impl InMemoryJobRepository {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InMemoryJobRepository {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
in_memory_repo!(InMemoryJobRepository, Job);
|
||||
|
||||
#[async_trait]
|
||||
impl JobRepository for InMemoryJobRepository {
|
||||
@@ -252,6 +230,24 @@ impl JobRepository for InMemoryJobRepository {
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn find_all(
|
||||
&self,
|
||||
_status: Option<&str>,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
) -> Result<Vec<Job>, DomainError> {
|
||||
let all: Vec<Job> = self.data.lock().await.values().cloned().collect();
|
||||
Ok(all
|
||||
.into_iter()
|
||||
.skip(offset as usize)
|
||||
.take(limit as usize)
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn count(&self, _status: Option<&str>) -> Result<u64, DomainError> {
|
||||
Ok(self.data.lock().await.len() as u64)
|
||||
}
|
||||
|
||||
async fn save(&self, job: &Job) -> Result<(), DomainError> {
|
||||
self.data
|
||||
.lock()
|
||||
@@ -261,25 +257,7 @@ impl JobRepository for InMemoryJobRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// --- InMemoryRoleRepository ---
|
||||
|
||||
pub struct InMemoryRoleRepository {
|
||||
data: Mutex<HashMap<String, Role>>,
|
||||
}
|
||||
|
||||
impl InMemoryRoleRepository {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InMemoryRoleRepository {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
in_memory_repo!(InMemoryRoleRepository, Role);
|
||||
|
||||
#[async_trait]
|
||||
impl RoleRepository for InMemoryRoleRepository {
|
||||
@@ -322,25 +300,7 @@ impl RoleRepository for InMemoryRoleRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// --- InMemoryGroupRepository ---
|
||||
|
||||
pub struct InMemoryGroupRepository {
|
||||
data: Mutex<HashMap<String, Group>>,
|
||||
}
|
||||
|
||||
impl InMemoryGroupRepository {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InMemoryGroupRepository {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
in_memory_repo!(InMemoryGroupRepository, Group);
|
||||
|
||||
#[async_trait]
|
||||
impl GroupRepository for InMemoryGroupRepository {
|
||||
@@ -373,25 +333,7 @@ impl GroupRepository for InMemoryGroupRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// --- InMemoryStorageVolumeRepository ---
|
||||
|
||||
pub struct InMemoryStorageVolumeRepository {
|
||||
data: Mutex<HashMap<String, StorageVolume>>,
|
||||
}
|
||||
|
||||
impl InMemoryStorageVolumeRepository {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InMemoryStorageVolumeRepository {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
in_memory_repo!(InMemoryStorageVolumeRepository, StorageVolume);
|
||||
|
||||
#[async_trait]
|
||||
impl StorageVolumeRepository for InMemoryStorageVolumeRepository {
|
||||
@@ -417,25 +359,7 @@ impl StorageVolumeRepository for InMemoryStorageVolumeRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// --- InMemoryLibraryPathRepository ---
|
||||
|
||||
pub struct InMemoryLibraryPathRepository {
|
||||
data: Mutex<HashMap<String, LibraryPath>>,
|
||||
}
|
||||
|
||||
impl InMemoryLibraryPathRepository {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InMemoryLibraryPathRepository {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
in_memory_repo!(InMemoryLibraryPathRepository, LibraryPath);
|
||||
|
||||
#[async_trait]
|
||||
impl LibraryPathRepository for InMemoryLibraryPathRepository {
|
||||
@@ -482,25 +406,7 @@ impl LibraryPathRepository for InMemoryLibraryPathRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// --- InMemoryIngestSessionRepository ---
|
||||
|
||||
pub struct InMemoryIngestSessionRepository {
|
||||
data: Mutex<HashMap<String, IngestSession>>,
|
||||
}
|
||||
|
||||
impl InMemoryIngestSessionRepository {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InMemoryIngestSessionRepository {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
in_memory_repo!(InMemoryIngestSessionRepository, IngestSession);
|
||||
|
||||
#[async_trait]
|
||||
impl IngestSessionRepository for InMemoryIngestSessionRepository {
|
||||
@@ -528,25 +434,7 @@ impl IngestSessionRepository for InMemoryIngestSessionRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// --- InMemoryQuotaRepository ---
|
||||
|
||||
pub struct InMemoryQuotaRepository {
|
||||
data: Mutex<HashMap<String, QuotaDefinition>>,
|
||||
}
|
||||
|
||||
impl InMemoryQuotaRepository {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InMemoryQuotaRepository {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
in_memory_repo!(InMemoryQuotaRepository, QuotaDefinition);
|
||||
|
||||
#[async_trait]
|
||||
impl QuotaRepository for InMemoryQuotaRepository {
|
||||
@@ -889,25 +777,7 @@ impl TagRepository for InMemoryTagRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// --- InMemoryDuplicateRepository ---
|
||||
|
||||
pub struct InMemoryDuplicateRepository {
|
||||
data: Mutex<HashMap<String, DuplicateGroup>>,
|
||||
}
|
||||
|
||||
impl InMemoryDuplicateRepository {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InMemoryDuplicateRepository {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
in_memory_repo!(InMemoryDuplicateRepository, DuplicateGroup);
|
||||
|
||||
#[async_trait]
|
||||
impl DuplicateRepository for InMemoryDuplicateRepository {
|
||||
@@ -946,25 +816,7 @@ impl DuplicateRepository for InMemoryDuplicateRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// --- InMemorySidecarRepository ---
|
||||
|
||||
pub struct InMemorySidecarRepository {
|
||||
data: Mutex<HashMap<String, SidecarRecord>>,
|
||||
}
|
||||
|
||||
impl InMemorySidecarRepository {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InMemorySidecarRepository {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
in_memory_repo!(InMemorySidecarRepository, SidecarRecord);
|
||||
|
||||
#[async_trait]
|
||||
impl SidecarRepository for InMemorySidecarRepository {
|
||||
@@ -1000,25 +852,7 @@ impl SidecarRepository for InMemorySidecarRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// --- InMemoryJobBatchRepository ---
|
||||
|
||||
pub struct InMemoryJobBatchRepository {
|
||||
data: Mutex<HashMap<String, JobBatch>>,
|
||||
}
|
||||
|
||||
impl InMemoryJobBatchRepository {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InMemoryJobBatchRepository {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
in_memory_repo!(InMemoryJobBatchRepository, JobBatch);
|
||||
|
||||
#[async_trait]
|
||||
impl JobBatchRepository for InMemoryJobBatchRepository {
|
||||
@@ -1035,25 +869,7 @@ impl JobBatchRepository for InMemoryJobBatchRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// --- InMemoryPluginRepository ---
|
||||
|
||||
pub struct InMemoryPluginRepository {
|
||||
data: Mutex<HashMap<String, Plugin>>,
|
||||
}
|
||||
|
||||
impl InMemoryPluginRepository {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InMemoryPluginRepository {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
in_memory_repo!(InMemoryPluginRepository, Plugin);
|
||||
|
||||
#[async_trait]
|
||||
impl PluginRepository for InMemoryPluginRepository {
|
||||
@@ -1081,25 +897,7 @@ impl PluginRepository for InMemoryPluginRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// --- InMemoryPipelineRepository ---
|
||||
|
||||
pub struct InMemoryPipelineRepository {
|
||||
data: Mutex<HashMap<String, ProcessingPipeline>>,
|
||||
}
|
||||
|
||||
impl InMemoryPipelineRepository {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InMemoryPipelineRepository {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
in_memory_repo!(InMemoryPipelineRepository, ProcessingPipeline);
|
||||
|
||||
#[async_trait]
|
||||
impl PipelineRepository for InMemoryPipelineRepository {
|
||||
@@ -1216,3 +1014,36 @@ impl IngestTransaction for InMemoryIngestTransaction {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
in_memory_repo!(InMemoryRefreshTokenRepository, RefreshToken);
|
||||
|
||||
#[async_trait]
|
||||
impl RefreshTokenRepository for InMemoryRefreshTokenRepository {
|
||||
async fn save(&self, token: &RefreshToken) -> Result<(), DomainError> {
|
||||
self.data
|
||||
.lock()
|
||||
.await
|
||||
.insert(token.token_id.to_string(), token.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn find_by_hash(&self, token_hash: &str) -> Result<Option<RefreshToken>, DomainError> {
|
||||
Ok(self
|
||||
.data
|
||||
.lock()
|
||||
.await
|
||||
.values()
|
||||
.find(|t| t.token_hash == token_hash)
|
||||
.cloned())
|
||||
}
|
||||
|
||||
async fn delete_by_user(&self, user_id: &SystemId) -> Result<(), DomainError> {
|
||||
self.data.lock().await.retain(|_, t| &t.user_id != user_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError> {
|
||||
self.data.lock().await.remove(&id.to_string());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user