mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-08-31 23:36:55 +00:00
256 lines
7.7 KiB
Rust
256 lines
7.7 KiB
Rust
use crate::state::AppState;
|
|
use axum::{
|
|
extract::State,
|
|
http::StatusCode,
|
|
response::{IntoResponse, Response},
|
|
Json,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::{json, Value};
|
|
|
|
const APP_BSKY_NAMESPACE: &str = "app.bsky";
|
|
const MAX_PREFERENCES_COUNT: usize = 100;
|
|
const MAX_PREFERENCE_SIZE: usize = 10_000;
|
|
|
|
#[derive(Serialize)]
|
|
pub struct GetPreferencesOutput {
|
|
pub preferences: Vec<Value>,
|
|
}
|
|
|
|
pub async fn get_preferences(
|
|
State(state): State<AppState>,
|
|
headers: axum::http::HeaderMap,
|
|
) -> Response {
|
|
let token = match crate::auth::extract_bearer_token_from_header(
|
|
headers.get("Authorization").and_then(|h| h.to_str().ok()),
|
|
) {
|
|
Some(t) => t,
|
|
None => {
|
|
return (
|
|
StatusCode::UNAUTHORIZED,
|
|
Json(json!({"error": "AuthenticationRequired"})),
|
|
)
|
|
.into_response();
|
|
}
|
|
};
|
|
|
|
let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await {
|
|
Ok(user) => user,
|
|
Err(_) => {
|
|
return (
|
|
StatusCode::UNAUTHORIZED,
|
|
Json(json!({"error": "AuthenticationFailed"})),
|
|
)
|
|
.into_response();
|
|
}
|
|
};
|
|
|
|
let user_id: uuid::Uuid =
|
|
match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", auth_user.did)
|
|
.fetch_optional(&state.db)
|
|
.await
|
|
{
|
|
Ok(Some(id)) => id,
|
|
_ => {
|
|
return (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(json!({"error": "InternalError", "message": "User not found"})),
|
|
)
|
|
.into_response();
|
|
}
|
|
};
|
|
|
|
let prefs_result = sqlx::query!(
|
|
"SELECT name, value_json FROM account_preferences WHERE user_id = $1",
|
|
user_id
|
|
)
|
|
.fetch_all(&state.db)
|
|
.await;
|
|
|
|
let prefs = match prefs_result {
|
|
Ok(rows) => rows,
|
|
Err(_) => {
|
|
return (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(json!({"error": "InternalError", "message": "Failed to fetch preferences"})),
|
|
)
|
|
.into_response();
|
|
}
|
|
};
|
|
|
|
let preferences: Vec<Value> = prefs
|
|
.into_iter()
|
|
.filter(|row| {
|
|
row.name == APP_BSKY_NAMESPACE || row.name.starts_with(&format!("{}.", APP_BSKY_NAMESPACE))
|
|
})
|
|
.filter_map(|row| {
|
|
if row.name == "app.bsky.actor.defs#declaredAgePref" {
|
|
return None;
|
|
}
|
|
serde_json::from_value(row.value_json).ok()
|
|
})
|
|
.collect();
|
|
|
|
(StatusCode::OK, Json(GetPreferencesOutput { preferences })).into_response()
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct PutPreferencesInput {
|
|
pub preferences: Vec<Value>,
|
|
}
|
|
|
|
pub async fn put_preferences(
|
|
State(state): State<AppState>,
|
|
headers: axum::http::HeaderMap,
|
|
Json(input): Json<PutPreferencesInput>,
|
|
) -> Response {
|
|
let token = match crate::auth::extract_bearer_token_from_header(
|
|
headers.get("Authorization").and_then(|h| h.to_str().ok()),
|
|
) {
|
|
Some(t) => t,
|
|
None => {
|
|
return (
|
|
StatusCode::UNAUTHORIZED,
|
|
Json(json!({"error": "AuthenticationRequired"})),
|
|
)
|
|
.into_response();
|
|
}
|
|
};
|
|
|
|
let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await {
|
|
Ok(user) => user,
|
|
Err(_) => {
|
|
return (
|
|
StatusCode::UNAUTHORIZED,
|
|
Json(json!({"error": "AuthenticationFailed"})),
|
|
)
|
|
.into_response();
|
|
}
|
|
};
|
|
|
|
let user_id: uuid::Uuid =
|
|
match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", auth_user.did)
|
|
.fetch_optional(&state.db)
|
|
.await
|
|
{
|
|
Ok(Some(id)) => id,
|
|
_ => {
|
|
return (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(json!({"error": "InternalError", "message": "User not found"})),
|
|
)
|
|
.into_response();
|
|
}
|
|
};
|
|
|
|
if input.preferences.len() > MAX_PREFERENCES_COUNT {
|
|
return (
|
|
StatusCode::BAD_REQUEST,
|
|
Json(json!({"error": "InvalidRequest", "message": format!("Too many preferences: {} exceeds limit of {}", input.preferences.len(), MAX_PREFERENCES_COUNT)})),
|
|
)
|
|
.into_response();
|
|
}
|
|
|
|
for pref in &input.preferences {
|
|
let pref_str = serde_json::to_string(pref).unwrap_or_default();
|
|
if pref_str.len() > MAX_PREFERENCE_SIZE {
|
|
return (
|
|
StatusCode::BAD_REQUEST,
|
|
Json(json!({"error": "InvalidRequest", "message": format!("Preference too large: {} bytes exceeds limit of {}", pref_str.len(), MAX_PREFERENCE_SIZE)})),
|
|
)
|
|
.into_response();
|
|
}
|
|
|
|
let pref_type = match pref.get("$type").and_then(|t| t.as_str()) {
|
|
Some(t) => t,
|
|
None => {
|
|
return (
|
|
StatusCode::BAD_REQUEST,
|
|
Json(json!({"error": "InvalidRequest", "message": "Preference missing $type field"})),
|
|
)
|
|
.into_response();
|
|
}
|
|
};
|
|
|
|
if !pref_type.starts_with(APP_BSKY_NAMESPACE) {
|
|
return (
|
|
StatusCode::BAD_REQUEST,
|
|
Json(json!({"error": "InvalidRequest", "message": format!("Invalid preference namespace: {}", pref_type)})),
|
|
)
|
|
.into_response();
|
|
}
|
|
|
|
if pref_type == "app.bsky.actor.defs#declaredAgePref" {
|
|
return (
|
|
StatusCode::BAD_REQUEST,
|
|
Json(json!({"error": "InvalidRequest", "message": "declaredAgePref is read-only"})),
|
|
)
|
|
.into_response();
|
|
}
|
|
}
|
|
|
|
let mut tx = match state.db.begin().await {
|
|
Ok(tx) => tx,
|
|
Err(_) => {
|
|
return (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(json!({"error": "InternalError", "message": "Failed to start transaction"})),
|
|
)
|
|
.into_response();
|
|
}
|
|
};
|
|
|
|
let delete_result = sqlx::query!(
|
|
"DELETE FROM account_preferences WHERE user_id = $1 AND (name = $2 OR name LIKE $3)",
|
|
user_id,
|
|
APP_BSKY_NAMESPACE,
|
|
format!("{}.%", APP_BSKY_NAMESPACE)
|
|
)
|
|
.execute(&mut *tx)
|
|
.await;
|
|
|
|
if delete_result.is_err() {
|
|
let _ = tx.rollback().await;
|
|
return (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(json!({"error": "InternalError", "message": "Failed to clear preferences"})),
|
|
)
|
|
.into_response();
|
|
}
|
|
|
|
for pref in input.preferences {
|
|
let pref_type = match pref.get("$type").and_then(|t| t.as_str()) {
|
|
Some(t) => t,
|
|
None => continue,
|
|
};
|
|
|
|
let insert_result = sqlx::query!(
|
|
"INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)",
|
|
user_id,
|
|
pref_type,
|
|
pref
|
|
)
|
|
.execute(&mut *tx)
|
|
.await;
|
|
|
|
if insert_result.is_err() {
|
|
let _ = tx.rollback().await;
|
|
return (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(json!({"error": "InternalError", "message": "Failed to save preference"})),
|
|
)
|
|
.into_response();
|
|
}
|
|
}
|
|
|
|
if let Err(_) = tx.commit().await {
|
|
return (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(json!({"error": "InternalError", "message": "Failed to commit transaction"})),
|
|
)
|
|
.into_response();
|
|
}
|
|
|
|
StatusCode::OK.into_response()
|
|
}
|