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(age) = personal_details_pref 112 .as_ref() 113 .and_then(|pref| pref.get("birthDate")) 114 .and_then(|v| v.as_str()) 115 .and_then(get_age_from_datestring) 116 { 117 let declared_age_pref = json!({ 118 "$type": DECLARED_AGE_PREF, 119 "isOverAge13": age >= 13, 120 "isOverAge16": age >= 16, 121 "isOverAge18": age >= 18, 122 }); 123 preferences.push(declared_age_pref); 124 } 125 (StatusCode::OK, Json(GetPreferencesOutput { preferences })).into_response() 126} 127 128#[derive(Deserialize)] 129pub struct PutPreferencesInput { 130 pub preferences: Vec<Value>, 131} 132pub async fn put_preferences( 133 State(state): State<AppState>, 134 headers: axum::http::HeaderMap, 135 Json(input): Json<PutPreferencesInput>, 136) -> Response { 137 let token = match crate::auth::extract_bearer_token_from_header( 138 headers.get("Authorization").and_then(|h| h.to_str().ok()), 139 ) { 140 Some(t) => t, 141 None => { 142 return ( 143 StatusCode::UNAUTHORIZED, 144 Json(json!({"error": "AuthenticationRequired"})), 145 ) 146 .into_response(); 147 } 148 }; 149 let auth_user = 150 match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await { 151 Ok(user) => user, 152 Err(_) => { 153 return ( 154 StatusCode::UNAUTHORIZED, 155 Json(json!({"error": "AuthenticationFailed"})), 156 ) 157 .into_response(); 158 } 159 }; 160 let has_full_access = auth_user.permissions().has_full_access(); 161 let user_id: uuid::Uuid = 162 match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", auth_user.did) 163 .fetch_optional(&state.db) 164 .await 165 { 166 Ok(Some(id)) => id, 167 _ => { 168 return ( 169 StatusCode::INTERNAL_SERVER_ERROR, 170 Json(json!({"error": "InternalError", "message": "User not found"})), 171 ) 172 .into_response(); 173 } 174 }; 175 if input.preferences.len() > MAX_PREFERENCES_COUNT { 176 return ( 177 StatusCode::BAD_REQUEST, 178 Json(json!({"error": "InvalidRequest", "message": format!("Too many preferences: {} exceeds limit of {}", input.preferences.len(), MAX_PREFERENCES_COUNT)})), 179 ) 180 .into_response(); 181 } 182 let mut forbidden_prefs: Vec<String> = Vec::new(); 183 for pref in &input.preferences { 184 let pref_str = serde_json::to_string(pref).unwrap_or_default(); 185 if pref_str.len() > MAX_PREFERENCE_SIZE { 186 return ( 187 StatusCode::BAD_REQUEST, 188 Json(json!({"error": "InvalidRequest", "message": format!("Preference too large: {} bytes exceeds limit of {}", pref_str.len(), MAX_PREFERENCE_SIZE)})), 189 ) 190 .into_response(); 191 } 192 let pref_type = match pref.get("$type").and_then(|t| t.as_str()) { 193 Some(t) => t, 194 None => { 195 return ( 196 StatusCode::BAD_REQUEST, 197 Json(json!({"error": "InvalidRequest", "message": "Preference is missing a $type"})), 198 ) 199 .into_response(); 200 } 201 }; 202 if !pref_type.starts_with(APP_BSKY_NAMESPACE) { 203 return ( 204 StatusCode::BAD_REQUEST, 205 Json(json!({"error": "InvalidRequest", "message": format!("Some preferences are not in the {} namespace", APP_BSKY_NAMESPACE)})), 206 ) 207 .into_response(); 208 } 209 if pref_type == PERSONAL_DETAILS_PREF && !has_full_access { 210 forbidden_prefs.push(pref_type.to_string()); 211 } 212 } 213 if !forbidden_prefs.is_empty() { 214 return ( 215 StatusCode::BAD_REQUEST, 216 Json(json!({"error": "InvalidRequest", "message": format!("Do not have authorization to set preferences: {}", forbidden_prefs.join(", "))})), 217 ) 218 .into_response(); 219 } 220 let mut tx = match state.db.begin().await { 221 Ok(tx) => tx, 222 Err(_) => { 223 return ( 224 StatusCode::INTERNAL_SERVER_ERROR, 225 Json(json!({"error": "InternalError", "message": "Failed to start transaction"})), 226 ) 227 .into_response(); 228 } 229 }; 230 let delete_result = sqlx::query!( 231 "DELETE FROM account_preferences WHERE user_id = $1 AND (name = $2 OR name LIKE $3)", 232 user_id, 233 APP_BSKY_NAMESPACE, 234 format!("{}.%", APP_BSKY_NAMESPACE) 235 ) 236 .execute(&mut *tx) 237 .await; 238 if delete_result.is_err() { 239 let _ = tx.rollback().await; 240 return ( 241 StatusCode::INTERNAL_SERVER_ERROR, 242 Json(json!({"error": "InternalError", "message": "Failed to clear preferences"})), 243 ) 244 .into_response(); 245 } 246 for pref in input.preferences { 247 let pref_type = match pref.get("$type").and_then(|t| t.as_str()) { 248 Some(t) => t, 249 None => continue, 250 }; 251 if pref_type == DECLARED_AGE_PREF { 252 continue; 253 } 254 let insert_result = sqlx::query!( 255 "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)", 256 user_id, 257 pref_type, 258 pref 259 ) 260 .execute(&mut *tx) 261 .await; 262 if insert_result.is_err() { 263 let _ = tx.rollback().await; 264 return ( 265 StatusCode::INTERNAL_SERVER_ERROR, 266 Json(json!({"error": "InternalError", "message": "Failed to save preference"})), 267 ) 268 .into_response(); 269 } 270 } 271 if tx.commit().await.is_err() { 272 return ( 273 StatusCode::INTERNAL_SERVER_ERROR, 274 Json(json!({"error": "InternalError", "message": "Failed to commit transaction"})), 275 ) 276 .into_response(); 277 } 278 StatusCode::OK.into_response() 279}