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