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