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