this repo has no description
1use crate::state::AppState;
2use axum::{
3 Json,
4 extract::{Query, State},
5 http::StatusCode,
6 response::{IntoResponse, Response},
7};
8use serde::{Deserialize, Serialize};
9use serde_json::json;
10use tracing::{error, warn};
11
12#[derive(Deserialize)]
13pub struct GetAccountInfoParams {
14 pub did: String,
15}
16
17#[derive(Serialize)]
18#[serde(rename_all = "camelCase")]
19pub struct AccountInfo {
20 pub did: String,
21 pub handle: String,
22 pub email: Option<String>,
23 pub indexed_at: String,
24 pub invite_note: Option<String>,
25 pub invites_disabled: bool,
26 pub email_confirmed_at: Option<String>,
27 pub deactivated_at: Option<String>,
28}
29
30#[derive(Serialize)]
31#[serde(rename_all = "camelCase")]
32pub struct GetAccountInfosOutput {
33 pub infos: Vec<AccountInfo>,
34}
35
36pub async fn get_account_info(
37 State(state): State<AppState>,
38 headers: axum::http::HeaderMap,
39 Query(params): Query<GetAccountInfoParams>,
40) -> Response {
41 let auth_header = headers.get("Authorization");
42 if auth_header.is_none() {
43 return (
44 StatusCode::UNAUTHORIZED,
45 Json(json!({"error": "AuthenticationRequired"})),
46 )
47 .into_response();
48 }
49
50 let did = params.did.trim();
51 if did.is_empty() {
52 return (
53 StatusCode::BAD_REQUEST,
54 Json(json!({"error": "InvalidRequest", "message": "did is required"})),
55 )
56 .into_response();
57 }
58
59 let result = sqlx::query!(
60 r#"
61 SELECT did, handle, email, created_at
62 FROM users
63 WHERE did = $1
64 "#,
65 did
66 )
67 .fetch_optional(&state.db)
68 .await;
69
70 match result {
71 Ok(Some(row)) => {
72 (
73 StatusCode::OK,
74 Json(AccountInfo {
75 did: row.did,
76 handle: row.handle,
77 email: Some(row.email),
78 indexed_at: row.created_at.to_rfc3339(),
79 invite_note: None,
80 invites_disabled: false,
81 email_confirmed_at: None,
82 deactivated_at: None,
83 }),
84 )
85 .into_response()
86 }
87 Ok(None) => (
88 StatusCode::NOT_FOUND,
89 Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
90 )
91 .into_response(),
92 Err(e) => {
93 error!("DB error in get_account_info: {:?}", e);
94 (
95 StatusCode::INTERNAL_SERVER_ERROR,
96 Json(json!({"error": "InternalError"})),
97 )
98 .into_response()
99 }
100 }
101}
102
103#[derive(Deserialize)]
104pub struct GetAccountInfosParams {
105 pub dids: String,
106}
107
108pub async fn get_account_infos(
109 State(state): State<AppState>,
110 headers: axum::http::HeaderMap,
111 Query(params): Query<GetAccountInfosParams>,
112) -> Response {
113 let auth_header = headers.get("Authorization");
114 if auth_header.is_none() {
115 return (
116 StatusCode::UNAUTHORIZED,
117 Json(json!({"error": "AuthenticationRequired"})),
118 )
119 .into_response();
120 }
121
122 let dids: Vec<&str> = params.dids.split(',').map(|s| s.trim()).collect();
123 if dids.is_empty() {
124 return (
125 StatusCode::BAD_REQUEST,
126 Json(json!({"error": "InvalidRequest", "message": "dids is required"})),
127 )
128 .into_response();
129 }
130
131 let mut infos = Vec::new();
132
133 for did in dids {
134 if did.is_empty() {
135 continue;
136 }
137
138 let result = sqlx::query!(
139 r#"
140 SELECT did, handle, email, created_at
141 FROM users
142 WHERE did = $1
143 "#,
144 did
145 )
146 .fetch_optional(&state.db)
147 .await;
148
149 if let Ok(Some(row)) = result {
150 infos.push(AccountInfo {
151 did: row.did,
152 handle: row.handle,
153 email: Some(row.email),
154 indexed_at: row.created_at.to_rfc3339(),
155 invite_note: None,
156 invites_disabled: false,
157 email_confirmed_at: None,
158 deactivated_at: None,
159 });
160 }
161 }
162
163 (StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response()
164}
165
166#[derive(Deserialize)]
167pub struct DeleteAccountInput {
168 pub did: String,
169}
170
171pub async fn delete_account(
172 State(state): State<AppState>,
173 headers: axum::http::HeaderMap,
174 Json(input): Json<DeleteAccountInput>,
175) -> Response {
176 let auth_header = headers.get("Authorization");
177 if auth_header.is_none() {
178 return (
179 StatusCode::UNAUTHORIZED,
180 Json(json!({"error": "AuthenticationRequired"})),
181 )
182 .into_response();
183 }
184
185 let did = input.did.trim();
186 if did.is_empty() {
187 return (
188 StatusCode::BAD_REQUEST,
189 Json(json!({"error": "InvalidRequest", "message": "did is required"})),
190 )
191 .into_response();
192 }
193
194 let user = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
195 .fetch_optional(&state.db)
196 .await;
197
198 let user_id = match user {
199 Ok(Some(row)) => row.id,
200 Ok(None) => {
201 return (
202 StatusCode::NOT_FOUND,
203 Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
204 )
205 .into_response();
206 }
207 Err(e) => {
208 error!("DB error in delete_account: {:?}", e);
209 return (
210 StatusCode::INTERNAL_SERVER_ERROR,
211 Json(json!({"error": "InternalError"})),
212 )
213 .into_response();
214 }
215 };
216
217 let _ = sqlx::query!("DELETE FROM sessions WHERE did = $1", did)
218 .execute(&state.db)
219 .await;
220
221 let _ = sqlx::query!("DELETE FROM records WHERE repo_id = $1", user_id)
222 .execute(&state.db)
223 .await;
224
225 let _ = sqlx::query!("DELETE FROM repos WHERE user_id = $1", user_id)
226 .execute(&state.db)
227 .await;
228
229 let _ = sqlx::query!("DELETE FROM blobs WHERE created_by_user = $1", user_id)
230 .execute(&state.db)
231 .await;
232
233 let _ = sqlx::query!("DELETE FROM user_keys WHERE user_id = $1", user_id)
234 .execute(&state.db)
235 .await;
236
237 let result = sqlx::query!("DELETE FROM users WHERE id = $1", user_id)
238 .execute(&state.db)
239 .await;
240
241 match result {
242 Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(),
243 Err(e) => {
244 error!("DB error deleting account: {:?}", e);
245 (
246 StatusCode::INTERNAL_SERVER_ERROR,
247 Json(json!({"error": "InternalError"})),
248 )
249 .into_response()
250 }
251 }
252}
253
254#[derive(Deserialize)]
255pub struct UpdateAccountEmailInput {
256 pub account: String,
257 pub email: String,
258}
259
260pub async fn update_account_email(
261 State(state): State<AppState>,
262 headers: axum::http::HeaderMap,
263 Json(input): Json<UpdateAccountEmailInput>,
264) -> Response {
265 let auth_header = headers.get("Authorization");
266 if auth_header.is_none() {
267 return (
268 StatusCode::UNAUTHORIZED,
269 Json(json!({"error": "AuthenticationRequired"})),
270 )
271 .into_response();
272 }
273
274 let account = input.account.trim();
275 let email = input.email.trim();
276
277 if account.is_empty() || email.is_empty() {
278 return (
279 StatusCode::BAD_REQUEST,
280 Json(json!({"error": "InvalidRequest", "message": "account and email are required"})),
281 )
282 .into_response();
283 }
284
285 let result = sqlx::query!("UPDATE users SET email = $1 WHERE did = $2", email, account)
286 .execute(&state.db)
287 .await;
288
289 match result {
290 Ok(r) => {
291 if r.rows_affected() == 0 {
292 return (
293 StatusCode::NOT_FOUND,
294 Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
295 )
296 .into_response();
297 }
298 (StatusCode::OK, Json(json!({}))).into_response()
299 }
300 Err(e) => {
301 error!("DB error updating email: {:?}", e);
302 (
303 StatusCode::INTERNAL_SERVER_ERROR,
304 Json(json!({"error": "InternalError"})),
305 )
306 .into_response()
307 }
308 }
309}
310
311#[derive(Deserialize)]
312pub struct UpdateAccountHandleInput {
313 pub did: String,
314 pub handle: String,
315}
316
317pub async fn update_account_handle(
318 State(state): State<AppState>,
319 headers: axum::http::HeaderMap,
320 Json(input): Json<UpdateAccountHandleInput>,
321) -> Response {
322 let auth_header = headers.get("Authorization");
323 if auth_header.is_none() {
324 return (
325 StatusCode::UNAUTHORIZED,
326 Json(json!({"error": "AuthenticationRequired"})),
327 )
328 .into_response();
329 }
330
331 let did = input.did.trim();
332 let handle = input.handle.trim();
333
334 if did.is_empty() || handle.is_empty() {
335 return (
336 StatusCode::BAD_REQUEST,
337 Json(json!({"error": "InvalidRequest", "message": "did and handle are required"})),
338 )
339 .into_response();
340 }
341
342 if !handle
343 .chars()
344 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_')
345 {
346 return (
347 StatusCode::BAD_REQUEST,
348 Json(json!({"error": "InvalidHandle", "message": "Handle contains invalid characters"})),
349 )
350 .into_response();
351 }
352
353 let existing = sqlx::query!("SELECT id FROM users WHERE handle = $1 AND did != $2", handle, did)
354 .fetch_optional(&state.db)
355 .await;
356
357 if let Ok(Some(_)) = existing {
358 return (
359 StatusCode::BAD_REQUEST,
360 Json(json!({"error": "HandleTaken", "message": "Handle is already in use"})),
361 )
362 .into_response();
363 }
364
365 let result = sqlx::query!("UPDATE users SET handle = $1 WHERE did = $2", handle, did)
366 .execute(&state.db)
367 .await;
368
369 match result {
370 Ok(r) => {
371 if r.rows_affected() == 0 {
372 return (
373 StatusCode::NOT_FOUND,
374 Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
375 )
376 .into_response();
377 }
378 (StatusCode::OK, Json(json!({}))).into_response()
379 }
380 Err(e) => {
381 error!("DB error updating handle: {:?}", e);
382 (
383 StatusCode::INTERNAL_SERVER_ERROR,
384 Json(json!({"error": "InternalError"})),
385 )
386 .into_response()
387 }
388 }
389}
390
391#[derive(Deserialize)]
392pub struct UpdateAccountPasswordInput {
393 pub did: String,
394 pub password: String,
395}
396
397pub async fn update_account_password(
398 State(state): State<AppState>,
399 headers: axum::http::HeaderMap,
400 Json(input): Json<UpdateAccountPasswordInput>,
401) -> Response {
402 let auth_header = headers.get("Authorization");
403 if auth_header.is_none() {
404 return (
405 StatusCode::UNAUTHORIZED,
406 Json(json!({"error": "AuthenticationRequired"})),
407 )
408 .into_response();
409 }
410
411 let did = input.did.trim();
412 let password = input.password.trim();
413
414 if did.is_empty() || password.is_empty() {
415 return (
416 StatusCode::BAD_REQUEST,
417 Json(json!({"error": "InvalidRequest", "message": "did and password are required"})),
418 )
419 .into_response();
420 }
421
422 let password_hash = match bcrypt::hash(password, bcrypt::DEFAULT_COST) {
423 Ok(h) => h,
424 Err(e) => {
425 error!("Failed to hash password: {:?}", e);
426 return (
427 StatusCode::INTERNAL_SERVER_ERROR,
428 Json(json!({"error": "InternalError"})),
429 )
430 .into_response();
431 }
432 };
433
434 let result = sqlx::query!("UPDATE users SET password_hash = $1 WHERE did = $2", password_hash, did)
435 .execute(&state.db)
436 .await;
437
438 match result {
439 Ok(r) => {
440 if r.rows_affected() == 0 {
441 return (
442 StatusCode::NOT_FOUND,
443 Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
444 )
445 .into_response();
446 }
447 (StatusCode::OK, Json(json!({}))).into_response()
448 }
449 Err(e) => {
450 error!("DB error updating password: {:?}", e);
451 (
452 StatusCode::INTERNAL_SERVER_ERROR,
453 Json(json!({"error": "InternalError"})),
454 )
455 .into_response()
456 }
457 }
458}
459
460#[derive(Deserialize)]
461#[serde(rename_all = "camelCase")]
462pub struct SendEmailInput {
463 pub recipient_did: String,
464 pub sender_did: String,
465 pub content: String,
466 pub subject: Option<String>,
467 pub comment: Option<String>,
468}
469
470#[derive(Serialize)]
471pub struct SendEmailOutput {
472 pub sent: bool,
473}
474
475pub async fn send_email(
476 State(state): State<AppState>,
477 headers: axum::http::HeaderMap,
478 Json(input): Json<SendEmailInput>,
479) -> Response {
480 let auth_header = headers.get("Authorization");
481 if auth_header.is_none() {
482 return (
483 StatusCode::UNAUTHORIZED,
484 Json(json!({"error": "AuthenticationRequired"})),
485 )
486 .into_response();
487 }
488
489 let recipient_did = input.recipient_did.trim();
490 let content = input.content.trim();
491
492 if recipient_did.is_empty() {
493 return (
494 StatusCode::BAD_REQUEST,
495 Json(json!({"error": "InvalidRequest", "message": "recipientDid is required"})),
496 )
497 .into_response();
498 }
499
500 if content.is_empty() {
501 return (
502 StatusCode::BAD_REQUEST,
503 Json(json!({"error": "InvalidRequest", "message": "content is required"})),
504 )
505 .into_response();
506 }
507
508 let user = sqlx::query!(
509 "SELECT id, email, handle FROM users WHERE did = $1",
510 recipient_did
511 )
512 .fetch_optional(&state.db)
513 .await;
514
515 let (user_id, email, handle) = match user {
516 Ok(Some(row)) => (row.id, row.email, row.handle),
517 Ok(None) => {
518 return (
519 StatusCode::NOT_FOUND,
520 Json(json!({"error": "AccountNotFound", "message": "Recipient account not found"})),
521 )
522 .into_response();
523 }
524 Err(e) => {
525 error!("DB error in send_email: {:?}", e);
526 return (
527 StatusCode::INTERNAL_SERVER_ERROR,
528 Json(json!({"error": "InternalError"})),
529 )
530 .into_response();
531 }
532 };
533
534 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
535 let subject = input
536 .subject
537 .clone()
538 .unwrap_or_else(|| format!("Message from {}", hostname));
539
540 let notification = crate::notifications::NewNotification::email(
541 user_id,
542 crate::notifications::NotificationType::AdminEmail,
543 email,
544 subject,
545 content.to_string(),
546 );
547
548 let result = crate::notifications::enqueue_notification(&state.db, notification).await;
549
550 match result {
551 Ok(_) => {
552 tracing::info!(
553 "Admin email queued for {} ({})",
554 handle,
555 recipient_did
556 );
557 (StatusCode::OK, Json(SendEmailOutput { sent: true })).into_response()
558 }
559 Err(e) => {
560 warn!("Failed to enqueue admin email: {:?}", e);
561 (StatusCode::OK, Json(SendEmailOutput { sent: false })).into_response()
562 }
563 }
564}