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