diff --git a/crates/adapters/postgres-federation/src/lib.rs b/crates/adapters/postgres-federation/src/lib.rs index 91df294..9cee64c 100644 --- a/crates/adapters/postgres-federation/src/lib.rs +++ b/crates/adapters/postgres-federation/src/lib.rs @@ -546,6 +546,7 @@ impl PostgresApUserRepository { bio: Option, avatar_url: Option, header_url: Option, + also_known_as: Option, ) -> ApUser { let profile_url = url::Url::parse(&format!("{}/users/{}", self.base_url, username)).ok(); let avatar_url = avatar_url.and_then(|u| url::Url::parse(&u).ok()); @@ -556,7 +557,7 @@ impl PostgresApUserRepository { bio, avatar_url, banner_url, - also_known_as: None, + also_known_as, profile_url, attachment: vec![], } @@ -573,15 +574,25 @@ impl ApUserRepository for PostgresApUserRepository { bio: Option, avatar_url: Option, header_url: Option, + also_known_as: Option, } let row = sqlx::query_as::<_, Row>( - "SELECT id,username,bio,avatar_url,header_url FROM users WHERE id=$1 AND local=true", + "SELECT id,username,bio,avatar_url,header_url,also_known_as FROM users WHERE id=$1 AND local=true", ) .bind(id) .fetch_optional(&self.pool) .await .map_err(|e| anyhow!(e))?; - Ok(row.map(|r| self.row_to_ap_user(r.id, r.username, r.bio, r.avatar_url, r.header_url))) + Ok(row.map(|r| { + self.row_to_ap_user( + r.id, + r.username, + r.bio, + r.avatar_url, + r.header_url, + r.also_known_as, + ) + })) } async fn find_by_username(&self, username: &str) -> Result> { @@ -592,15 +603,25 @@ impl ApUserRepository for PostgresApUserRepository { bio: Option, avatar_url: Option, header_url: Option, + also_known_as: Option, } let row = sqlx::query_as::<_, Row>( - "SELECT id,username,bio,avatar_url,header_url FROM users WHERE username=$1 AND local=true", + "SELECT id,username,bio,avatar_url,header_url,also_known_as FROM users WHERE username=$1 AND local=true", ) .bind(username) .fetch_optional(&self.pool) .await .map_err(|e| anyhow!(e))?; - Ok(row.map(|r| self.row_to_ap_user(r.id, r.username, r.bio, r.avatar_url, r.header_url))) + Ok(row.map(|r| { + self.row_to_ap_user( + r.id, + r.username, + r.bio, + r.avatar_url, + r.header_url, + r.also_known_as, + ) + })) } async fn count_users(&self) -> Result { diff --git a/crates/adapters/postgres/migrations/013_also_known_as.sql b/crates/adapters/postgres/migrations/013_also_known_as.sql new file mode 100644 index 0000000..3a4f0b2 --- /dev/null +++ b/crates/adapters/postgres/migrations/013_also_known_as.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN IF NOT EXISTS also_known_as TEXT; diff --git a/crates/adapters/postgres/src/user/mod.rs b/crates/adapters/postgres/src/user/mod.rs index 69e59b7..67b1d64 100644 --- a/crates/adapters/postgres/src/user/mod.rs +++ b/crates/adapters/postgres/src/user/mod.rs @@ -269,12 +269,12 @@ impl UserWriter for PgUserRepository { ) -> Result<(), DomainError> { sqlx::query( "UPDATE users SET \ - display_name = COALESCE($2, display_name), \ - bio = COALESCE($3, bio), \ - avatar_url = COALESCE($4, avatar_url), \ - header_url = COALESCE($5, header_url), \ - custom_css = COALESCE($6, custom_css), \ - updated_at = NOW() \ + display_name = COALESCE($2, display_name), \ + bio = COALESCE($3, bio), \ + avatar_url = COALESCE($4, avatar_url), \ + header_url = COALESCE($5, header_url), \ + custom_css = COALESCE($6, custom_css), \ + updated_at = NOW() \ WHERE id = $1", ) .bind(user_id.as_uuid()) @@ -288,6 +288,20 @@ impl UserWriter for PgUserRepository { .into_domain() .map(|_| ()) } + + async fn set_also_known_as( + &self, + user_id: &UserId, + value: Option, + ) -> Result<(), DomainError> { + sqlx::query("UPDATE users SET also_known_as = $2, updated_at = NOW() WHERE id = $1") + .bind(user_id.as_uuid()) + .bind(value) + .execute(&self.pool) + .await + .into_domain() + .map(|_| ()) + } } #[cfg(test)] diff --git a/crates/application/src/use_cases/auth/tests.rs b/crates/application/src/use_cases/auth/tests.rs index 009223d..9f4ce4c 100644 --- a/crates/application/src/use_cases/auth/tests.rs +++ b/crates/application/src/use_cases/auth/tests.rs @@ -60,6 +60,13 @@ impl UserWriter for ConflictOnSaveStore { ) -> Result<(), DomainError> { self.0.update_profile(user_id, input).await } + async fn set_also_known_as( + &self, + user_id: &UserId, + value: Option, + ) -> Result<(), DomainError> { + self.0.set_also_known_as(user_id, value).await + } } #[async_trait] @@ -105,6 +112,13 @@ impl UserWriter for EmailConflictOnSaveStore { ) -> Result<(), DomainError> { self.0.update_profile(user_id, input).await } + async fn set_also_known_as( + &self, + user_id: &UserId, + value: Option, + ) -> Result<(), DomainError> { + self.0.set_also_known_as(user_id, value).await + } } struct FakeHasher; diff --git a/crates/domain/src/ports.rs b/crates/domain/src/ports.rs index 29da537..33338e8 100644 --- a/crates/domain/src/ports.rs +++ b/crates/domain/src/ports.rs @@ -83,6 +83,11 @@ pub trait UserWriter: Send + Sync { user_id: &UserId, input: UpdateProfileInput, ) -> Result<(), DomainError>; + async fn set_also_known_as( + &self, + user_id: &UserId, + value: Option, + ) -> Result<(), DomainError>; } /// Combined supertrait — `AppState.users` stays `Arc`. diff --git a/crates/domain/src/testing/mod.rs b/crates/domain/src/testing/mod.rs index 31efd1d..ac48cb4 100644 --- a/crates/domain/src/testing/mod.rs +++ b/crates/domain/src/testing/mod.rs @@ -152,6 +152,14 @@ impl UserWriter for TestStore { } Ok(()) } + + async fn set_also_known_as( + &self, + _user_id: &UserId, + _value: Option, + ) -> Result<(), DomainError> { + Ok(()) + } } #[async_trait] diff --git a/crates/presentation/src/handlers/federation_management.rs b/crates/presentation/src/handlers/federation_management.rs index 1d54e9c..9189d05 100644 --- a/crates/presentation/src/handlers/federation_management.rs +++ b/crates/presentation/src/handlers/federation_management.rs @@ -126,3 +126,17 @@ pub async fn post_move_account( .map_err(ApiError::from)?; Ok(StatusCode::NO_CONTENT) } + +#[derive(Deserialize)] +pub struct AlsoKnownAsBody { + pub also_known_as: Option, +} + +pub async fn patch_also_known_as( + Deps(d): Deps, + AuthUser(uid): AuthUser, + Json(body): Json, +) -> Result { + d.users.set_also_known_as(&uid, body.also_known_as).await?; + Ok(StatusCode::NO_CONTENT) +} diff --git a/crates/presentation/src/routes.rs b/crates/presentation/src/routes.rs index e9ac3c6..ead9440 100644 --- a/crates/presentation/src/routes.rs +++ b/crates/presentation/src/routes.rs @@ -108,6 +108,10 @@ pub fn router() -> Router { "/federation/me/move", post(federation_management::post_move_account), ) + .route( + "/federation/me/also-known-as", + patch(federation_management::patch_also_known_as), + ) .route("/tags/popular", get(feed::get_popular_tags)) .route("/tags/{name}", get(feed::tag_thoughts_handler)) // notifications