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}