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