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:
@@ -290,6 +290,8 @@ pub struct User {
|
||||
username: Username,
|
||||
password_hash: PasswordHash,
|
||||
role: UserRole,
|
||||
bio: Option<String>,
|
||||
avatar_path: Option<String>,
|
||||
}
|
||||
|
||||
impl User {
|
||||
@@ -305,6 +307,8 @@ impl User {
|
||||
username,
|
||||
password_hash,
|
||||
role,
|
||||
bio: None,
|
||||
avatar_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,6 +318,8 @@ impl User {
|
||||
username: Username,
|
||||
password_hash: PasswordHash,
|
||||
role: UserRole,
|
||||
bio: Option<String>,
|
||||
avatar_path: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
@@ -321,6 +327,8 @@ impl User {
|
||||
username,
|
||||
password_hash,
|
||||
role,
|
||||
bio,
|
||||
avatar_path,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,6 +336,11 @@ impl User {
|
||||
self.password_hash = new_hash;
|
||||
}
|
||||
|
||||
pub fn update_profile(&mut self, bio: Option<String>, avatar_path: Option<String>) {
|
||||
self.bio = bio;
|
||||
self.avatar_path = avatar_path;
|
||||
}
|
||||
|
||||
pub fn email(&self) -> &Email {
|
||||
&self.email
|
||||
}
|
||||
@@ -343,6 +356,13 @@ impl User {
|
||||
pub fn role(&self) -> &UserRole {
|
||||
&self.role
|
||||
}
|
||||
pub fn bio(&self) -> Option<&str> {
|
||||
self.bio.as_deref()
|
||||
}
|
||||
|
||||
pub fn avatar_path(&self) -> Option<&str> {
|
||||
self.avatar_path.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -435,3 +455,38 @@ pub enum ExportFormat {
|
||||
Csv,
|
||||
Json,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::value_objects::{Email, PasswordHash, UserId, Username};
|
||||
|
||||
fn make_user() -> User {
|
||||
User::from_persistence(
|
||||
UserId::generate(),
|
||||
Email::new("a@b.com".to_string()).unwrap(),
|
||||
Username::new("alice".to_string()).unwrap(),
|
||||
PasswordHash::new("hash".to_string()).unwrap(),
|
||||
UserRole::Standard,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_profile_sets_fields() {
|
||||
let mut user = make_user();
|
||||
user.update_profile(Some("My bio".to_string()), Some("avatars/abc".to_string()));
|
||||
assert_eq!(user.bio(), Some("My bio"));
|
||||
assert_eq!(user.avatar_path(), Some("avatars/abc"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_profile_clears_with_none() {
|
||||
let mut user = make_user();
|
||||
user.update_profile(Some("bio".to_string()), Some("path".to_string()));
|
||||
user.update_profile(None, None);
|
||||
assert_eq!(user.bio(), None);
|
||||
assert_eq!(user.avatar_path(), None);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user