this repo has no description
1use crate::state::AppState; 2use axum::{ 3 extract::State, 4 http::StatusCode, 5 response::{IntoResponse, Response}, 6 Json, 7}; 8use serde::{Deserialize, Serialize}; 9use serde_json::{json, Value}; 10const APP_BSKY_NAMESPACE: &str = "app.bsky"; 11const MAX_PREFERENCES_COUNT: usize = 100; 12const MAX_PREFERENCE_SIZE: usize = 10_000; 13#[derive(Serialize)] 14pub struct GetPreferencesOutput { 15 pub preferences: Vec<Value>, 16} 17pub async fn get_preferences( 18 State(state): State<AppState>, 19 headers: axum::http::HeaderMap, 20) -> Response { 21 let token = match crate::auth::extract_bearer_token_from_header( 22 headers.get("Authorization").and_then(|h| h.to_str().ok()), 23 ) { 24 Some(t) => t, 25 None => { 26 return ( 27 StatusCode::UNAUTHORIZED, 28 Json(json!({"error": "AuthenticationRequired"})), 29 ) 30 .into_response(); 31 } 32 }; 33 let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await { 34 Ok(user) => user, 35 Err(_) => { 36 return ( 37 StatusCode::UNAUTHORIZED, 38 Json(json!({"error": "AuthenticationFailed"})), 39 ) 40 .into_response(); 41 } 42 }; 43 let user_id: uuid::Uuid = 44 match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", auth_user.did) 45 .fetch_optional(&state.db) 46 .await 47 { 48 Ok(Some(id)) => id, 49 _ => { 50 return ( 51 StatusCode::INTERNAL_SERVER_ERROR, 52 Json(json!({"error": "InternalError", "message": "User not found"})), 53 ) 54 .into_response(); 55 } 56 }; 57 let prefs_result = sqlx::query!( 58 "SELECT name, value_json FROM account_preferences WHERE user_id = $1", 59 user_id 60 ) 61 .fetch_all(&state.db) 62 .await; 63 let prefs = match prefs_result { 64 Ok(rows) => rows, 65 Err(_) => { 66 return ( 67 StatusCode::INTERNAL_SERVER_ERROR, 68 Json(json!({"error": "InternalError", "message": "Failed to fetch preferences"})), 69 ) 70 .into_response(); 71 } 72 }; 73 let preferences: Vec<Value> = prefs 74 .into_iter() 75 .filter(|row| { 76 row.name == APP_BSKY_NAMESPACE || row.name.starts_with(&format!("{}.", APP_BSKY_NAMESPACE)) 77 }) 78 .filter_map(|row| { 79 if row.name == "app.bsky.actor.defs#declaredAgePref" { 80 return None; 81 } 82 serde_json::from_value(row.value_json).ok() 83 }) 84 .collect(); 85 (StatusCode::OK, Json(GetPreferencesOutput { preferences })).into_response() 86} 87#[derive(Deserialize)] 88pub struct PutPreferencesInput { 89 pub preferences: Vec<Value>, 90} 91pub async fn put_preferences( 92 State(state): State<AppState>, 93 headers: axum::http::HeaderMap, 94 Json(input): Json<PutPreferencesInput>, 95) -> Response { 96 let token = match crate::auth::extract_bearer_token_from_header( 97 headers.get("Authorization").and_then(|h| h.to_str().ok()), 98 ) { 99 Some(t) => t, 100 None => { 101 return ( 102 StatusCode::UNAUTHORIZED, 103 Json(json!({"error": "AuthenticationRequired"})), 104 ) 105 .into_response(); 106 } 107 }; 108 let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await { 109 Ok(user) => user, 110 Err(_) => { 111 return ( 112 StatusCode::UNAUTHORIZED, 113 Json(json!({"error": "AuthenticationFailed"})), 114 ) 115 .into_response(); 116 } 117 }; 118 let user_id: uuid::Uuid = 119 match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", auth_user.did) 120 .fetch_optional(&state.db) 121 .await 122 { 123 Ok(Some(id)) => id, 124 _ => { 125 return ( 126 StatusCode::INTERNAL_SERVER_ERROR, 127 Json(json!({"error": "InternalError", "message": "User not found"})), 128 ) 129 .into_response(); 130 } 131 }; 132 if input.preferences.len() > MAX_PREFERENCES_COUNT { 133 return ( 134 StatusCode::BAD_REQUEST, 135 Json(json!({"error": "InvalidRequest", "message": format!("Too many preferences: {} exceeds limit of {}", input.preferences.len(), MAX_PREFERENCES_COUNT)})), 136 ) 137 .into_response(); 138 } 139 for pref in &input.preferences { 140 let pref_str = serde_json::to_string(pref).unwrap_or_default(); 141 if pref_str.len() > MAX_PREFERENCE_SIZE { 142 return ( 143 StatusCode::BAD_REQUEST, 144 Json(json!({"error": "InvalidRequest", "message": format!("Preference too large: {} bytes exceeds limit of {}", pref_str.len(), MAX_PREFERENCE_SIZE)})), 145 ) 146 .into_response(); 147 } 148 let pref_type = match pref.get("$type").and_then(|t| t.as_str()) { 149 Some(t) => t, 150 None => { 151 return ( 152 StatusCode::BAD_REQUEST, 153 Json(json!({"error": "InvalidRequest", "message": "Preference missing $type field"})), 154 ) 155 .into_response(); 156 } 157 }; 158 if !pref_type.starts_with(APP_BSKY_NAMESPACE) { 159 return ( 160 StatusCode::BAD_REQUEST, 161 Json(json!({"error": "InvalidRequest", "message": format!("Invalid preference namespace: {}", pref_type)})), 162 ) 163 .into_response(); 164 } 165 if pref_type == "app.bsky.actor.defs#declaredAgePref" { 166 return ( 167 StatusCode::BAD_REQUEST, 168 Json(json!({"error": "InvalidRequest", "message": "declaredAgePref is read-only"})), 169 ) 170 .into_response(); 171 } 172 } 173 let mut tx = match state.db.begin().await { 174 Ok(tx) => tx, 175 Err(_) => { 176 return ( 177 StatusCode::INTERNAL_SERVER_ERROR, 178 Json(json!({"error": "InternalError", "message": "Failed to start transaction"})), 179 ) 180 .into_response(); 181 } 182 }; 183 let delete_result = sqlx::query!( 184 "DELETE FROM account_preferences WHERE user_id = $1 AND (name = $2 OR name LIKE $3)", 185 user_id, 186 APP_BSKY_NAMESPACE, 187 format!("{}.%", APP_BSKY_NAMESPACE) 188 ) 189 .execute(&mut *tx) 190 .await; 191 if delete_result.is_err() { 192 let _ = tx.rollback().await; 193 return ( 194 StatusCode::INTERNAL_SERVER_ERROR, 195 Json(json!({"error": "InternalError", "message": "Failed to clear preferences"})), 196 ) 197 .into_response(); 198 } 199 for pref in input.preferences { 200 let pref_type = match pref.get("$type").and_then(|t| t.as_str()) { 201 Some(t) => t, 202 None => continue, 203 }; 204 let insert_result = sqlx::query!( 205 "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)", 206 user_id, 207 pref_type, 208 pref 209 ) 210 .execute(&mut *tx) 211 .await; 212 if insert_result.is_err() { 213 let _ = tx.rollback().await; 214 return ( 215 StatusCode::INTERNAL_SERVER_ERROR, 216 Json(json!({"error": "InternalError", "message": "Failed to save preference"})), 217 ) 218 .into_response(); 219 } 220 } 221 if let Err(_) = tx.commit().await { 222 return ( 223 StatusCode::INTERNAL_SERVER_ERROR, 224 Json(json!({"error": "InternalError", "message": "Failed to commit transaction"})), 225 ) 226 .into_response(); 227 } 228 StatusCode::OK.into_response() 229}