refactor: dedup JSONB name/value helpers, add profile fields validation
Extract parse/serialize into postgres::jsonb, used by user, remote_actor, and postgres-federation. Validate profile fields in update_profile use case (max 4, name≤64, value≤256).
This commit is contained in:
20
crates/adapters/postgres/src/jsonb.rs
Normal file
20
crates/adapters/postgres/src/jsonb.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
pub fn parse_name_value(v: Option<serde_json::Value>) -> Vec<(String, String)> {
|
||||
v.and_then(|v| v.as_array().cloned())
|
||||
.map(|arr| {
|
||||
arr.into_iter()
|
||||
.filter_map(|item| {
|
||||
let name = item.get("name")?.as_str()?.to_string();
|
||||
let value = item.get("value")?.as_str()?.to_string();
|
||||
Some((name, value))
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn serialize_name_value(fields: &[(String, String)]) -> serde_json::Value {
|
||||
fields
|
||||
.iter()
|
||||
.map(|(n, v)| serde_json::json!({"name": n, "value": v}))
|
||||
.collect()
|
||||
}
|
||||
@@ -8,6 +8,7 @@ pub mod engagement;
|
||||
pub mod failed_event;
|
||||
pub mod feed;
|
||||
pub mod follow;
|
||||
pub(crate) mod jsonb;
|
||||
pub mod like;
|
||||
pub mod notification;
|
||||
pub mod outbox;
|
||||
|
||||
@@ -23,11 +23,7 @@ impl RemoteActorRepository for PgRemoteActorRepository {
|
||||
} else {
|
||||
Some(a.also_known_as.iter().map(|s| s.as_str()).collect())
|
||||
};
|
||||
let attachment_json: serde_json::Value = a
|
||||
.attachment
|
||||
.iter()
|
||||
.map(|(n, v)| serde_json::json!({"name": n, "value": v}))
|
||||
.collect();
|
||||
let attachment_json = crate::jsonb::serialize_name_value(&a.attachment);
|
||||
sqlx::query(
|
||||
"INSERT INTO remote_actors(url,handle,display_name,avatar_url,last_fetched_at,
|
||||
bio,banner_url,outbox_url,followers_url,following_url,also_known_as,attachment)
|
||||
@@ -101,19 +97,7 @@ impl RemoteActorRepository for PgRemoteActorRepository {
|
||||
following_url: r.following_url,
|
||||
inbox_url: r.inbox_url,
|
||||
shared_inbox_url: r.shared_inbox_url,
|
||||
attachment: r
|
||||
.attachment
|
||||
.and_then(|v| v.as_array().cloned())
|
||||
.map(|arr| {
|
||||
arr.into_iter()
|
||||
.filter_map(|item| {
|
||||
let name = item.get("name")?.as_str()?.to_string();
|
||||
let value = item.get("value")?.as_str()?.to_string();
|
||||
Some((name, value))
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
attachment: crate::jsonb::parse_name_value(r.attachment),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -37,20 +37,6 @@ pub struct UserRow {
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
fn parse_profile_fields(v: Option<serde_json::Value>) -> Vec<(String, String)> {
|
||||
v.and_then(|v| v.as_array().cloned())
|
||||
.map(|arr| {
|
||||
arr.into_iter()
|
||||
.filter_map(|item| {
|
||||
let name = item.get("name")?.as_str()?.to_string();
|
||||
let value = item.get("value")?.as_str()?.to_string();
|
||||
Some((name, value))
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
impl From<UserRow> for User {
|
||||
fn from(r: UserRow) -> Self {
|
||||
User {
|
||||
@@ -63,7 +49,7 @@ impl From<UserRow> for User {
|
||||
avatar_url: r.avatar_url,
|
||||
header_url: r.header_url,
|
||||
custom_css: r.custom_css,
|
||||
profile_fields: parse_profile_fields(r.profile_fields),
|
||||
profile_fields: crate::jsonb::parse_name_value(r.profile_fields),
|
||||
local: r.local,
|
||||
created_at: r.created_at,
|
||||
updated_at: r.updated_at,
|
||||
@@ -238,11 +224,7 @@ impl UserReader for PgUserRepository {
|
||||
#[async_trait]
|
||||
impl UserWriter for PgUserRepository {
|
||||
async fn save(&self, user: &User) -> Result<(), DomainError> {
|
||||
let profile_fields_json: serde_json::Value = user
|
||||
.profile_fields
|
||||
.iter()
|
||||
.map(|(n, v)| serde_json::json!({"name": n, "value": v}))
|
||||
.collect();
|
||||
let profile_fields_json = crate::jsonb::serialize_name_value(&user.profile_fields);
|
||||
sqlx::query(
|
||||
"INSERT INTO users (id,username,email,password_hash,display_name,bio,avatar_url,header_url,custom_css,profile_fields,local,created_at,updated_at)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
|
||||
@@ -290,13 +272,10 @@ impl UserWriter for PgUserRepository {
|
||||
user_id: &UserId,
|
||||
input: UpdateProfileInput,
|
||||
) -> Result<(), DomainError> {
|
||||
let profile_fields_json: Option<serde_json::Value> =
|
||||
input.profile_fields.as_ref().map(|fields| {
|
||||
fields
|
||||
.iter()
|
||||
.map(|(n, v)| serde_json::json!({"name": n, "value": v}))
|
||||
.collect()
|
||||
});
|
||||
let profile_fields_json: Option<serde_json::Value> = input
|
||||
.profile_fields
|
||||
.as_ref()
|
||||
.map(|f| crate::jsonb::serialize_name_value(f));
|
||||
sqlx::query(
|
||||
"UPDATE users SET \
|
||||
display_name = COALESCE($2, display_name), \
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const MAX_TOP_FRIENDS: usize = 8;
|
||||
const MAX_PROFILE_FIELDS: usize = 4;
|
||||
const MAX_FIELD_NAME_LEN: usize = 64;
|
||||
const MAX_FIELD_VALUE_LEN: usize = 256;
|
||||
|
||||
use bytes::Bytes;
|
||||
use domain::{
|
||||
@@ -55,6 +58,20 @@ pub async fn update_profile(
|
||||
user_id: &UserId,
|
||||
input: UpdateProfileInput,
|
||||
) -> Result<(), DomainError> {
|
||||
if let Some(ref fields) = input.profile_fields {
|
||||
if fields.len() > MAX_PROFILE_FIELDS {
|
||||
return Err(DomainError::InvalidInput(format!(
|
||||
"profile fields: max {MAX_PROFILE_FIELDS}"
|
||||
)));
|
||||
}
|
||||
for (name, value) in fields {
|
||||
if name.len() > MAX_FIELD_NAME_LEN || value.len() > MAX_FIELD_VALUE_LEN {
|
||||
return Err(DomainError::InvalidInput(
|
||||
"profile field name or value too long".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
users.update_profile(user_id, input).await?;
|
||||
events
|
||||
.publish(&DomainEvent::ProfileUpdated {
|
||||
|
||||
Reference in New Issue
Block a user