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())) 74 .into_response(); 75 } 76 }; 77 let mut personal_details_pref: Option<Value> = None; 78 let mut preferences: Vec<Value> = prefs 79 .into_iter() 80 .filter(|row| { 81 row.name == APP_BSKY_NAMESPACE 82 || row.name.starts_with(&format!("{}.", APP_BSKY_NAMESPACE)) 83 }) 84 .filter_map(|row| { 85 if row.name == DECLARED_AGE_PREF { 86 return None; 87 } 88 if row.name == PERSONAL_DETAILS_PREF { 89 if !has_full_access { 90 return None; 91 } 92 personal_details_pref = serde_json::from_value(row.value_json.clone()).ok(); 93 } 94 serde_json::from_value(row.value_json).ok() 95 }) 96 .collect(); 97 if let Some(age) = personal_details_pref 98 .as_ref() 99 .and_then(|pref| pref.get("birthDate")) 100 .and_then(|v| v.as_str()) 101 .and_then(get_age_from_datestring) 102 { 103 let declared_age_pref = serde_json::json!({ 104 "$type": DECLARED_AGE_PREF, 105 "isOverAge13": age >= 13, 106 "isOverAge16": age >= 16, 107 "isOverAge18": age >= 18, 108 }); 109 preferences.push(declared_age_pref); 110 } 111 (StatusCode::OK, Json(GetPreferencesOutput { preferences })).into_response() 112} 113 114#[derive(Deserialize)] 115pub struct PutPreferencesInput { 116 pub preferences: Vec<Value>, 117} 118pub async fn put_preferences( 119 State(state): State<AppState>, 120 headers: axum::http::HeaderMap, 121 Json(input): Json<PutPreferencesInput>, 122) -> Response { 123 let token = match crate::auth::extract_bearer_token_from_header( 124 headers.get("Authorization").and_then(|h| h.to_str().ok()), 125 ) { 126 Some(t) => t, 127 None => { 128 return ApiError::AuthenticationRequired.into_response(); 129 } 130 }; 131 let auth_user = 132 match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await { 133 Ok(user) => user, 134 Err(_) => { 135 return ApiError::AuthenticationFailed(None).into_response(); 136 } 137 }; 138 let has_full_access = auth_user.permissions().has_full_access(); 139 let user_id: uuid::Uuid = 140 match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", &*auth_user.did) 141 .fetch_optional(&state.db) 142 .await 143 { 144 Ok(Some(id)) => id, 145 _ => { 146 return ApiError::InternalError(Some("User not found".into())).into_response(); 147 } 148 }; 149 if input.preferences.len() > MAX_PREFERENCES_COUNT { 150 return ApiError::InvalidRequest(format!( 151 "Too many preferences: {} exceeds limit of {}", 152 input.preferences.len(), 153 MAX_PREFERENCES_COUNT 154 )) 155 .into_response(); 156 } 157 let mut forbidden_prefs: Vec<String> = Vec::new(); 158 for pref in &input.preferences { 159 let pref_str = serde_json::to_string(pref).unwrap_or_default(); 160 if pref_str.len() > MAX_PREFERENCE_SIZE { 161 return ApiError::InvalidRequest(format!( 162 "Preference too large: {} bytes exceeds limit of {}", 163 pref_str.len(), 164 MAX_PREFERENCE_SIZE 165 )) 166 .into_response(); 167 } 168 let pref_type = match pref.get("$type").and_then(|t| t.as_str()) { 169 Some(t) => t, 170 None => { 171 return ApiError::InvalidRequest("Preference is missing a $type".into()) 172 .into_response(); 173 } 174 }; 175 if !pref_type.starts_with(APP_BSKY_NAMESPACE) { 176 return ApiError::InvalidRequest(format!( 177 "Some preferences are not in the {} namespace", 178 APP_BSKY_NAMESPACE 179 )) 180 .into_response(); 181 } 182 if pref_type == PERSONAL_DETAILS_PREF && !has_full_access { 183 forbidden_prefs.push(pref_type.to_string()); 184 } 185 } 186 if !forbidden_prefs.is_empty() { 187 return ApiError::InvalidRequest(format!( 188 "Do not have authorization to set preferences: {}", 189 forbidden_prefs.join(", ") 190 )) 191 .into_response(); 192 } 193 let mut tx = match state.db.begin().await { 194 Ok(tx) => tx, 195 Err(_) => { 196 return ApiError::InternalError(Some("Failed to start transaction".into())) 197 .into_response(); 198 } 199 }; 200 let delete_result = sqlx::query!( 201 "DELETE FROM account_preferences WHERE user_id = $1 AND (name = $2 OR name LIKE $3)", 202 user_id, 203 APP_BSKY_NAMESPACE, 204 format!("{}.%", APP_BSKY_NAMESPACE) 205 ) 206 .execute(&mut *tx) 207 .await; 208 if delete_result.is_err() { 209 let _ = tx.rollback().await; 210 return ApiError::InternalError(Some("Failed to clear preferences".into())).into_response(); 211 } 212 for pref in input.preferences { 213 let pref_type = match pref.get("$type").and_then(|t| t.as_str()) { 214 Some(t) => t, 215 None => continue, 216 }; 217 if pref_type == DECLARED_AGE_PREF { 218 continue; 219 } 220 let insert_result = sqlx::query!( 221 "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)", 222 user_id, 223 pref_type, 224 pref 225 ) 226 .execute(&mut *tx) 227 .await; 228 if insert_result.is_err() { 229 let _ = tx.rollback().await; 230 return ApiError::InternalError(Some("Failed to save preference".into())) 231 .into_response(); 232 } 233 } 234 if tx.commit().await.is_err() { 235 return ApiError::InternalError(Some("Failed to commit transaction".into())) 236 .into_response(); 237 } 238 StatusCode::OK.into_response() 239}