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