feat(ap): ActivityPub spec compliance and profile completeness

Phase 1 — spec compliance:
- Add AS_PUBLIC constant; add to/cc fields to CreateActivity, DeleteActivity,
  UpdateActivity, AddActivity; populate on all broadcast call sites
- Add @context to outbox CreateActivity items
- Set manuallyApprovesFollowers: true to match actual Pending follow flow
- Gate PermissiveVerifier behind FEDERATION_DEBUG env var
- Add updated timestamp to Person actor JSON
- Improve actor update delivery logging

Phase 2a Batch 1 — AP layer:
- Add /inbox shared inbox route; add endpoints.sharedInbox to Person
- Paginate followers and following collections (20/page, OrderedCollectionPage)

Phase 2a Batch 2 — profile completeness:
- DB migrations: banner_path, also_known_as columns; user_profile_fields table
- ProfileField value object; UserProfileFieldsRepository port
- Banner image upload (stored via image-converter, surfaced as image in Person)
- alsoKnownAs field in Person (account migration support)
- Custom profile fields (up to 4 PropertyValue attachments in Person)
- Profile settings UI: banner preview/upload, alsoKnownAs input, fields form
- PUT /api/v1/profile/fields API endpoint
This commit is contained in:
2026-05-13 22:21:41 +02:00
parent 0a97fe5544
commit 815178e6a4
56 changed files with 1388 additions and 246 deletions

View File

@@ -306,6 +306,12 @@ pub enum UserRole {
Admin,
}
#[derive(Debug, Clone)]
pub struct ProfileField {
pub name: String,
pub value: String,
}
#[derive(Clone, Debug)]
pub struct User {
id: UserId,
@@ -315,6 +321,8 @@ pub struct User {
role: UserRole,
bio: Option<String>,
avatar_path: Option<String>,
banner_path: Option<String>,
also_known_as: Option<String>,
}
impl User {
@@ -332,6 +340,8 @@ impl User {
role,
bio: None,
avatar_path: None,
banner_path: None,
also_known_as: None,
}
}
@@ -343,6 +353,8 @@ impl User {
role: UserRole,
bio: Option<String>,
avatar_path: Option<String>,
banner_path: Option<String>,
also_known_as: Option<String>,
) -> Self {
Self {
id,
@@ -352,6 +364,8 @@ impl User {
role,
bio,
avatar_path,
banner_path,
also_known_as,
}
}
@@ -359,9 +373,11 @@ impl User {
self.password_hash = new_hash;
}
pub fn update_profile(&mut self, bio: Option<String>, avatar_path: Option<String>) {
pub fn update_profile(&mut self, bio: Option<String>, avatar_path: Option<String>, banner_path: Option<String>, also_known_as: Option<String>) {
self.bio = bio;
self.avatar_path = avatar_path;
self.banner_path = banner_path;
self.also_known_as = also_known_as;
}
pub fn email(&self) -> &Email {
@@ -386,6 +402,14 @@ impl User {
pub fn avatar_path(&self) -> Option<&str> {
self.avatar_path.as_deref()
}
pub fn banner_path(&self) -> Option<&str> {
self.banner_path.as_deref()
}
pub fn also_known_as(&self) -> Option<&str> {
self.also_known_as.as_deref()
}
}
#[derive(Clone, Debug)]

View File

@@ -188,9 +188,17 @@ pub trait UserRepository: Send + Sync {
user_id: &UserId,
bio: Option<String>,
avatar_path: Option<String>,
banner_path: Option<String>,
also_known_as: Option<String>,
) -> Result<(), DomainError>;
}
#[async_trait]
pub trait UserProfileFieldsRepository: Send + Sync {
async fn get_fields(&self, user_id: &UserId) -> Result<Vec<crate::models::ProfileField>, DomainError>;
async fn set_fields(&self, user_id: &UserId, fields: Vec<crate::models::ProfileField>) -> Result<(), DomainError>;
}
#[async_trait]
pub trait EventPublisher: Send + Sync {
async fn publish(&self, event: &DomainEvent) -> Result<(), DomainError>;