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