this repo has no description
1use crate::state::AppState; 2use axum::{ 3 Json, 4 extract::State, 5 http::StatusCode, 6 response::{IntoResponse, Response}, 7}; 8use serde::{Deserialize, Serialize}; 9use serde_json::{Value, json}; 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_allow_deactivated(&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 79 || row.name.starts_with(&format!("{}.", APP_BSKY_NAMESPACE)) 80 }) 81 .filter_map(|row| { 82 if row.name == "app.bsky.actor.defs#declaredAgePref" { 83 return None; 84 } 85 serde_json::from_value(row.value_json).ok() 86 }) 87 .collect(); 88 (StatusCode::OK, Json(GetPreferencesOutput { preferences })).into_response() 89} 90 91#[derive(Deserialize)] 92pub struct PutPreferencesInput { 93 pub preferences: Vec<Value>, 94} 95pub async fn put_preferences( 96 State(state): State<AppState>, 97 headers: axum::http::HeaderMap, 98 Json(input): Json<PutPreferencesInput>, 99) -> Response { 100 let token = match crate::auth::extract_bearer_token_from_header( 101 headers.get("Authorization").and_then(|h| h.to_str().ok()), 102 ) { 103 Some(t) => t, 104 None => { 105 return ( 106 StatusCode::UNAUTHORIZED, 107 Json(json!({"error": "AuthenticationRequired"})), 108 ) 109 .into_response(); 110 } 111 }; 112 let auth_user = match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await { 113 Ok(user) => user, 114 Err(_) => { 115 return ( 116 StatusCode::UNAUTHORIZED, 117 Json(json!({"error": "AuthenticationFailed"})), 118 ) 119 .into_response(); 120 } 121 }; 122 let (user_id, is_migration): (uuid::Uuid, bool) = 123 match sqlx::query!("SELECT id, deactivated_at FROM users WHERE did = $1", auth_user.did) 124 .fetch_optional(&state.db) 125 .await 126 { 127 Ok(Some(row)) => (row.id, row.deactivated_at.is_some()), 128 _ => { 129 return ( 130 StatusCode::INTERNAL_SERVER_ERROR, 131 Json(json!({"error": "InternalError", "message": "User not found"})), 132 ) 133 .into_response(); 134 } 135 }; 136 if input.preferences.len() > MAX_PREFERENCES_COUNT { 137 return ( 138 StatusCode::BAD_REQUEST, 139 Json(json!({"error": "InvalidRequest", "message": format!("Too many preferences: {} exceeds limit of {}", input.preferences.len(), MAX_PREFERENCES_COUNT)})), 140 ) 141 .into_response(); 142 } 143 for pref in &input.preferences { 144 let pref_str = serde_json::to_string(pref).unwrap_or_default(); 145 if pref_str.len() > MAX_PREFERENCE_SIZE { 146 return ( 147 StatusCode::BAD_REQUEST, 148 Json(json!({"error": "InvalidRequest", "message": format!("Preference too large: {} bytes exceeds limit of {}", pref_str.len(), MAX_PREFERENCE_SIZE)})), 149 ) 150 .into_response(); 151 } 152 let pref_type = match pref.get("$type").and_then(|t| t.as_str()) { 153 Some(t) => t, 154 None => { 155 return ( 156 StatusCode::BAD_REQUEST, 157 Json(json!({"error": "InvalidRequest", "message": "Preference missing $type field"})), 158 ) 159 .into_response(); 160 } 161 }; 162 if !pref_type.starts_with(APP_BSKY_NAMESPACE) { 163 return ( 164 StatusCode::BAD_REQUEST, 165 Json(json!({"error": "InvalidRequest", "message": format!("Invalid preference namespace: {}", pref_type)})), 166 ) 167 .into_response(); 168 } 169 if pref_type == "app.bsky.actor.defs#declaredAgePref" && !is_migration { 170 return ( 171 StatusCode::BAD_REQUEST, 172 Json(json!({"error": "InvalidRequest", "message": "declaredAgePref is read-only"})), 173 ) 174 .into_response(); 175 } 176 } 177 let mut tx = match state.db.begin().await { 178 Ok(tx) => tx, 179 Err(_) => { 180 return ( 181 StatusCode::INTERNAL_SERVER_ERROR, 182 Json(json!({"error": "InternalError", "message": "Failed to start transaction"})), 183 ) 184 .into_response(); 185 } 186 }; 187 let delete_result = sqlx::query!( 188 "DELETE FROM account_preferences WHERE user_id = $1 AND (name = $2 OR name LIKE $3)", 189 user_id, 190 APP_BSKY_NAMESPACE, 191 format!("{}.%", APP_BSKY_NAMESPACE) 192 ) 193 .execute(&mut *tx) 194 .await; 195 if delete_result.is_err() { 196 let _ = tx.rollback().await; 197 return ( 198 StatusCode::INTERNAL_SERVER_ERROR, 199 Json(json!({"error": "InternalError", "message": "Failed to clear preferences"})), 200 ) 201 .into_response(); 202 } 203 for pref in input.preferences { 204 let pref_type = match pref.get("$type").and_then(|t| t.as_str()) { 205 Some(t) => t, 206 None => continue, 207 }; 208 let insert_result = sqlx::query!( 209 "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)", 210 user_id, 211 pref_type, 212 pref 213 ) 214 .execute(&mut *tx) 215 .await; 216 if insert_result.is_err() { 217 let _ = tx.rollback().await; 218 return ( 219 StatusCode::INTERNAL_SERVER_ERROR, 220 Json(json!({"error": "InternalError", "message": "Failed to save preference"})), 221 ) 222 .into_response(); 223 } 224 } 225 if tx.commit().await.is_err() { 226 return ( 227 StatusCode::INTERNAL_SERVER_ERROR, 228 Json(json!({"error": "InternalError", "message": "Failed to commit transaction"})), 229 ) 230 .into_response(); 231 } 232 StatusCode::OK.into_response() 233}