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