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