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:
@@ -38,6 +38,8 @@ impl PostgresUserRepository {
|
||||
username_str: String,
|
||||
hash_str: String,
|
||||
role: UserRole,
|
||||
bio: Option<String>,
|
||||
avatar_path: Option<String>,
|
||||
) -> Result<User, DomainError> {
|
||||
let id = uuid::Uuid::parse_str(&id_str)
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
||||
@@ -53,6 +55,8 @@ impl PostgresUserRepository {
|
||||
username,
|
||||
hash,
|
||||
role,
|
||||
bio,
|
||||
avatar_path,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -68,9 +72,11 @@ impl UserRepository for PostgresUserRepository {
|
||||
username: String,
|
||||
password_hash: String,
|
||||
role: String,
|
||||
bio: Option<String>,
|
||||
avatar_path: Option<String>,
|
||||
}
|
||||
let row = sqlx::query_as::<_, Row>(
|
||||
"SELECT id, email, username, password_hash, role FROM users WHERE email = $1",
|
||||
"SELECT id, email, username, password_hash, role, bio, avatar_path FROM users WHERE email = $1",
|
||||
)
|
||||
.bind(email_str)
|
||||
.fetch_optional(&self.pool)
|
||||
@@ -83,6 +89,8 @@ impl UserRepository for PostgresUserRepository {
|
||||
r.username,
|
||||
r.password_hash,
|
||||
Self::parse_role(&r.role),
|
||||
r.bio,
|
||||
r.avatar_path,
|
||||
)
|
||||
})
|
||||
.transpose()
|
||||
@@ -97,9 +105,11 @@ impl UserRepository for PostgresUserRepository {
|
||||
username: String,
|
||||
password_hash: String,
|
||||
role: String,
|
||||
bio: Option<String>,
|
||||
avatar_path: Option<String>,
|
||||
}
|
||||
let row = sqlx::query_as::<_, Row>(
|
||||
"SELECT id, email, username, password_hash, role FROM users WHERE username = $1",
|
||||
"SELECT id, email, username, password_hash, role, bio, avatar_path FROM users WHERE username = $1",
|
||||
)
|
||||
.bind(username_str)
|
||||
.fetch_optional(&self.pool)
|
||||
@@ -112,6 +122,8 @@ impl UserRepository for PostgresUserRepository {
|
||||
r.username,
|
||||
r.password_hash,
|
||||
Self::parse_role(&r.role),
|
||||
r.bio,
|
||||
r.avatar_path,
|
||||
)
|
||||
})
|
||||
.transpose()
|
||||
@@ -164,9 +176,11 @@ impl UserRepository for PostgresUserRepository {
|
||||
username: String,
|
||||
password_hash: String,
|
||||
role: String,
|
||||
bio: Option<String>,
|
||||
avatar_path: Option<String>,
|
||||
}
|
||||
let row = sqlx::query_as::<_, Row>(
|
||||
"SELECT id, email, username, password_hash, role FROM users WHERE id = $1",
|
||||
"SELECT id, email, username, password_hash, role, bio, avatar_path FROM users WHERE id = $1",
|
||||
)
|
||||
.bind(&id_str)
|
||||
.fetch_optional(&self.pool)
|
||||
@@ -179,11 +193,30 @@ impl UserRepository for PostgresUserRepository {
|
||||
r.username,
|
||||
r.password_hash,
|
||||
Self::parse_role(&r.role),
|
||||
r.bio,
|
||||
r.avatar_path,
|
||||
)
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
async fn update_profile(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
bio: Option<String>,
|
||||
avatar_path: Option<String>,
|
||||
) -> Result<(), DomainError> {
|
||||
let id_str = user_id.value().to_string();
|
||||
sqlx::query("UPDATE users SET bio = $1, avatar_path = $2 WHERE id = $3")
|
||||
.bind(&bio)
|
||||
.bind(&avatar_path)
|
||||
.bind(&id_str)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_with_stats(&self) -> Result<Vec<domain::models::UserSummary>, DomainError> {
|
||||
sqlx::query_as::<_, UserSummaryRow>(
|
||||
r#"SELECT u.id, u.email,
|
||||
|
||||
Reference in New Issue
Block a user