feat: image storage generalization, user profile, and federation polish

- Replace PosterStorage with generic ImageStorage port (IMAGE_STORAGE_BACKEND/PATH env vars)
- Rename poster-storage crate to image-storage; serve at /images/{*key}
- Add bio and avatar_path to User model (migration 0009_user_profile)
- update_profile use case with avatar upload, mime validation, old avatar cleanup
- GET/PUT /api/v1/profile and GET/POST /settings/profile HTML page
- Enrich AP Person actor with summary, icon, url, discoverable fields
- Store remote actor avatar_url (migration 0010_ap_remote_actor_avatar)
- Shared inbox delivery via collect_inboxes deduplication
- Broadcast Update(Person) to followers on UserUpdated event
- Paginated outbox: OrderedCollection + OrderedCollectionPage with cursor
- Announce/boost tracking in ap_announces table (migration 0011_ap_announces)
This commit is contained in:
2026-05-11 22:59:52 +02:00
parent 8a254346f4
commit 80f620c840
89 changed files with 2231 additions and 499 deletions

View File

@@ -12,11 +12,11 @@ use domain::{
events::DomainEvent,
models::{Movie, User},
ports::{
AuthService, EventPublisher, GeneratedToken, MetadataClient, MetadataSearchCriteria,
PasswordHasher, PosterFetcherClient, PosterStorage, UserRepository,
AuthService, EventPublisher, GeneratedToken, ImageStorage, MetadataClient, MetadataSearchCriteria,
PasswordHasher, PosterFetcherClient, UserRepository,
},
value_objects::{
Email, ExternalMetadataId, MovieId, PasswordHash, PosterPath, PosterUrl, UserId,
Email, ExternalMetadataId, PasswordHash, PosterUrl, UserId,
},
};
use http_body_util::BodyExt;
@@ -57,18 +57,12 @@ impl PosterFetcherClient for PanicFetcher {
}
}
struct PanicStorage;
struct PanicImageStorage;
#[async_trait]
impl PosterStorage for PanicStorage {
async fn store_poster(&self, _: &MovieId, _: &[u8]) -> Result<PosterPath, DomainError> {
panic!()
}
async fn get_poster(&self, _: &PosterPath) -> Result<Vec<u8>, DomainError> {
panic!()
}
async fn delete_poster(&self, _: &PosterPath) -> Result<(), DomainError> {
panic!()
}
impl ImageStorage for PanicImageStorage {
async fn store(&self, _: &str, _: &[u8]) -> Result<String, DomainError> { panic!() }
async fn get(&self, _: &str) -> Result<Vec<u8>, DomainError> { panic!() }
async fn delete(&self, _: &str) -> Result<(), DomainError> { panic!() }
}
struct PanicHasher;
@@ -114,6 +108,9 @@ impl UserRepository for NobodyUserRepo {
async fn list_with_stats(&self) -> Result<Vec<domain::models::UserSummary>, DomainError> {
panic!()
}
async fn update_profile(&self, _: &UserId, _: Option<String>, _: Option<String>) -> Result<(), DomainError> {
Ok(())
}
}
struct PanicExporter;
@@ -194,7 +191,7 @@ async fn test_app() -> Router {
stats_repository: Arc::clone(&repo) as _,
metadata_client: Arc::new(PanicMeta),
poster_fetcher: Arc::new(PanicFetcher),
poster_storage: Arc::new(PanicStorage),
image_storage: Arc::new(PanicImageStorage),
event_publisher: Arc::new(NoopEventPublisher),
auth_service: Arc::new(PanicAuth),
password_hasher: Arc::new(PanicHasher),