+564
src/api/admin/account.rs
+564
src/api/admin/account.rs
···
···
1
+
use crate::state::AppState;
2
+
use axum::{
3
+
Json,
4
+
extract::{Query, State},
5
+
http::StatusCode,
6
+
response::{IntoResponse, Response},
7
+
};
8
+
use serde::{Deserialize, Serialize};
9
+
use serde_json::json;
10
+
use tracing::{error, warn};
11
+
12
+
#[derive(Deserialize)]
13
+
pub struct GetAccountInfoParams {
14
+
pub did: String,
15
+
}
16
+
17
+
#[derive(Serialize)]
18
+
#[serde(rename_all = "camelCase")]
19
+
pub 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")]
32
+
pub struct GetAccountInfosOutput {
33
+
pub infos: Vec<AccountInfo>,
34
+
}
35
+
36
+
pub 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)]
104
+
pub struct GetAccountInfosParams {
105
+
pub dids: String,
106
+
}
107
+
108
+
pub 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)]
167
+
pub struct DeleteAccountInput {
168
+
pub did: String,
169
+
}
170
+
171
+
pub 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)]
255
+
pub struct UpdateAccountEmailInput {
256
+
pub account: String,
257
+
pub email: String,
258
+
}
259
+
260
+
pub 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)]
312
+
pub struct UpdateAccountHandleInput {
313
+
pub did: String,
314
+
pub handle: String,
315
+
}
316
+
317
+
pub 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)]
392
+
pub struct UpdateAccountPasswordInput {
393
+
pub did: String,
394
+
pub password: String,
395
+
}
396
+
397
+
pub 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")]
462
+
pub 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)]
471
+
pub struct SendEmailOutput {
472
+
pub sent: bool,
473
+
}
474
+
475
+
pub 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
+
}
+323
src/api/admin/invite.rs
+323
src/api/admin/invite.rs
···
···
1
+
use crate::state::AppState;
2
+
use axum::{
3
+
Json,
4
+
extract::{Query, State},
5
+
http::StatusCode,
6
+
response::{IntoResponse, Response},
7
+
};
8
+
use serde::{Deserialize, Serialize};
9
+
use serde_json::json;
10
+
use tracing::error;
11
+
12
+
#[derive(Deserialize)]
13
+
#[serde(rename_all = "camelCase")]
14
+
pub struct DisableInviteCodesInput {
15
+
pub codes: Option<Vec<String>>,
16
+
pub accounts: Option<Vec<String>>,
17
+
}
18
+
19
+
pub async fn disable_invite_codes(
20
+
State(state): State<AppState>,
21
+
headers: axum::http::HeaderMap,
22
+
Json(input): Json<DisableInviteCodesInput>,
23
+
) -> Response {
24
+
let auth_header = headers.get("Authorization");
25
+
if auth_header.is_none() {
26
+
return (
27
+
StatusCode::UNAUTHORIZED,
28
+
Json(json!({"error": "AuthenticationRequired"})),
29
+
)
30
+
.into_response();
31
+
}
32
+
33
+
if let Some(codes) = &input.codes {
34
+
for code in codes {
35
+
let _ = sqlx::query!("UPDATE invite_codes SET disabled = TRUE WHERE code = $1", code)
36
+
.execute(&state.db)
37
+
.await;
38
+
}
39
+
}
40
+
41
+
if let Some(accounts) = &input.accounts {
42
+
for account in accounts {
43
+
let user = sqlx::query!("SELECT id FROM users WHERE did = $1", account)
44
+
.fetch_optional(&state.db)
45
+
.await;
46
+
47
+
if let Ok(Some(user_row)) = user {
48
+
let _ = sqlx::query!(
49
+
"UPDATE invite_codes SET disabled = TRUE WHERE created_by_user = $1",
50
+
user_row.id
51
+
)
52
+
.execute(&state.db)
53
+
.await;
54
+
}
55
+
}
56
+
}
57
+
58
+
(StatusCode::OK, Json(json!({}))).into_response()
59
+
}
60
+
61
+
#[derive(Deserialize)]
62
+
pub struct GetInviteCodesParams {
63
+
pub sort: Option<String>,
64
+
pub limit: Option<i64>,
65
+
pub cursor: Option<String>,
66
+
}
67
+
68
+
#[derive(Serialize)]
69
+
#[serde(rename_all = "camelCase")]
70
+
pub struct InviteCodeInfo {
71
+
pub code: String,
72
+
pub available: i32,
73
+
pub disabled: bool,
74
+
pub for_account: String,
75
+
pub created_by: String,
76
+
pub created_at: String,
77
+
pub uses: Vec<InviteCodeUseInfo>,
78
+
}
79
+
80
+
#[derive(Serialize)]
81
+
#[serde(rename_all = "camelCase")]
82
+
pub struct InviteCodeUseInfo {
83
+
pub used_by: String,
84
+
pub used_at: String,
85
+
}
86
+
87
+
#[derive(Serialize)]
88
+
pub struct GetInviteCodesOutput {
89
+
pub cursor: Option<String>,
90
+
pub codes: Vec<InviteCodeInfo>,
91
+
}
92
+
93
+
pub async fn get_invite_codes(
94
+
State(state): State<AppState>,
95
+
headers: axum::http::HeaderMap,
96
+
Query(params): Query<GetInviteCodesParams>,
97
+
) -> Response {
98
+
let auth_header = headers.get("Authorization");
99
+
if auth_header.is_none() {
100
+
return (
101
+
StatusCode::UNAUTHORIZED,
102
+
Json(json!({"error": "AuthenticationRequired"})),
103
+
)
104
+
.into_response();
105
+
}
106
+
107
+
let limit = params.limit.unwrap_or(100).min(500);
108
+
let sort = params.sort.as_deref().unwrap_or("recent");
109
+
110
+
let order_clause = match sort {
111
+
"usage" => "available_uses DESC",
112
+
_ => "created_at DESC",
113
+
};
114
+
115
+
let codes_result = if let Some(cursor) = ¶ms.cursor {
116
+
sqlx::query_as::<_, (String, i32, Option<bool>, uuid::Uuid, chrono::DateTime<chrono::Utc>)>(&format!(
117
+
r#"
118
+
SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at
119
+
FROM invite_codes ic
120
+
WHERE ic.created_at < (SELECT created_at FROM invite_codes WHERE code = $1)
121
+
ORDER BY {}
122
+
LIMIT $2
123
+
"#,
124
+
order_clause
125
+
))
126
+
.bind(cursor)
127
+
.bind(limit)
128
+
.fetch_all(&state.db)
129
+
.await
130
+
} else {
131
+
sqlx::query_as::<_, (String, i32, Option<bool>, uuid::Uuid, chrono::DateTime<chrono::Utc>)>(&format!(
132
+
r#"
133
+
SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at
134
+
FROM invite_codes ic
135
+
ORDER BY {}
136
+
LIMIT $1
137
+
"#,
138
+
order_clause
139
+
))
140
+
.bind(limit)
141
+
.fetch_all(&state.db)
142
+
.await
143
+
};
144
+
145
+
let codes_rows = match codes_result {
146
+
Ok(rows) => rows,
147
+
Err(e) => {
148
+
error!("DB error fetching invite codes: {:?}", e);
149
+
return (
150
+
StatusCode::INTERNAL_SERVER_ERROR,
151
+
Json(json!({"error": "InternalError"})),
152
+
)
153
+
.into_response();
154
+
}
155
+
};
156
+
157
+
let mut codes = Vec::new();
158
+
for (code, available_uses, disabled, created_by_user, created_at) in &codes_rows {
159
+
let creator_did = sqlx::query_scalar!("SELECT did FROM users WHERE id = $1", created_by_user)
160
+
.fetch_optional(&state.db)
161
+
.await
162
+
.ok()
163
+
.flatten()
164
+
.unwrap_or_else(|| "unknown".to_string());
165
+
166
+
let uses_result = sqlx::query!(
167
+
r#"
168
+
SELECT u.did, icu.used_at
169
+
FROM invite_code_uses icu
170
+
JOIN users u ON icu.used_by_user = u.id
171
+
WHERE icu.code = $1
172
+
ORDER BY icu.used_at DESC
173
+
"#,
174
+
code
175
+
)
176
+
.fetch_all(&state.db)
177
+
.await;
178
+
179
+
let uses = match uses_result {
180
+
Ok(use_rows) => use_rows
181
+
.iter()
182
+
.map(|u| InviteCodeUseInfo {
183
+
used_by: u.did.clone(),
184
+
used_at: u.used_at.to_rfc3339(),
185
+
})
186
+
.collect(),
187
+
Err(_) => Vec::new(),
188
+
};
189
+
190
+
codes.push(InviteCodeInfo {
191
+
code: code.clone(),
192
+
available: *available_uses,
193
+
disabled: disabled.unwrap_or(false),
194
+
for_account: creator_did.clone(),
195
+
created_by: creator_did,
196
+
created_at: created_at.to_rfc3339(),
197
+
uses,
198
+
});
199
+
}
200
+
201
+
let next_cursor = if codes_rows.len() == limit as usize {
202
+
codes_rows.last().map(|(code, _, _, _, _)| code.clone())
203
+
} else {
204
+
None
205
+
};
206
+
207
+
(
208
+
StatusCode::OK,
209
+
Json(GetInviteCodesOutput {
210
+
cursor: next_cursor,
211
+
codes,
212
+
}),
213
+
)
214
+
.into_response()
215
+
}
216
+
217
+
#[derive(Deserialize)]
218
+
pub struct DisableAccountInvitesInput {
219
+
pub account: String,
220
+
}
221
+
222
+
pub async fn disable_account_invites(
223
+
State(state): State<AppState>,
224
+
headers: axum::http::HeaderMap,
225
+
Json(input): Json<DisableAccountInvitesInput>,
226
+
) -> Response {
227
+
let auth_header = headers.get("Authorization");
228
+
if auth_header.is_none() {
229
+
return (
230
+
StatusCode::UNAUTHORIZED,
231
+
Json(json!({"error": "AuthenticationRequired"})),
232
+
)
233
+
.into_response();
234
+
}
235
+
236
+
let account = input.account.trim();
237
+
if account.is_empty() {
238
+
return (
239
+
StatusCode::BAD_REQUEST,
240
+
Json(json!({"error": "InvalidRequest", "message": "account is required"})),
241
+
)
242
+
.into_response();
243
+
}
244
+
245
+
let result = sqlx::query!("UPDATE users SET invites_disabled = TRUE WHERE did = $1", account)
246
+
.execute(&state.db)
247
+
.await;
248
+
249
+
match result {
250
+
Ok(r) => {
251
+
if r.rows_affected() == 0 {
252
+
return (
253
+
StatusCode::NOT_FOUND,
254
+
Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
255
+
)
256
+
.into_response();
257
+
}
258
+
(StatusCode::OK, Json(json!({}))).into_response()
259
+
}
260
+
Err(e) => {
261
+
error!("DB error disabling account invites: {:?}", e);
262
+
(
263
+
StatusCode::INTERNAL_SERVER_ERROR,
264
+
Json(json!({"error": "InternalError"})),
265
+
)
266
+
.into_response()
267
+
}
268
+
}
269
+
}
270
+
271
+
#[derive(Deserialize)]
272
+
pub struct EnableAccountInvitesInput {
273
+
pub account: String,
274
+
}
275
+
276
+
pub async fn enable_account_invites(
277
+
State(state): State<AppState>,
278
+
headers: axum::http::HeaderMap,
279
+
Json(input): Json<EnableAccountInvitesInput>,
280
+
) -> Response {
281
+
let auth_header = headers.get("Authorization");
282
+
if auth_header.is_none() {
283
+
return (
284
+
StatusCode::UNAUTHORIZED,
285
+
Json(json!({"error": "AuthenticationRequired"})),
286
+
)
287
+
.into_response();
288
+
}
289
+
290
+
let account = input.account.trim();
291
+
if account.is_empty() {
292
+
return (
293
+
StatusCode::BAD_REQUEST,
294
+
Json(json!({"error": "InvalidRequest", "message": "account is required"})),
295
+
)
296
+
.into_response();
297
+
}
298
+
299
+
let result = sqlx::query!("UPDATE users SET invites_disabled = FALSE WHERE did = $1", account)
300
+
.execute(&state.db)
301
+
.await;
302
+
303
+
match result {
304
+
Ok(r) => {
305
+
if r.rows_affected() == 0 {
306
+
return (
307
+
StatusCode::NOT_FOUND,
308
+
Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
309
+
)
310
+
.into_response();
311
+
}
312
+
(StatusCode::OK, Json(json!({}))).into_response()
313
+
}
314
+
Err(e) => {
315
+
error!("DB error enabling account invites: {:?}", e);
316
+
(
317
+
StatusCode::INTERNAL_SERVER_ERROR,
318
+
Json(json!({"error": "InternalError"})),
319
+
)
320
+
.into_response()
321
+
}
322
+
}
323
+
}
+11
-1222
src/api/admin/mod.rs
+11
-1222
src/api/admin/mod.rs
···
1
-
use crate::state::AppState;
2
-
use axum::{
3
-
Json,
4
-
extract::{Query, State},
5
-
http::StatusCode,
6
-
response::{IntoResponse, Response},
7
-
};
8
-
use serde::{Deserialize, Serialize};
9
-
use serde_json::json;
10
-
use tracing::{error, warn};
11
-
12
-
#[derive(Deserialize)]
13
-
#[serde(rename_all = "camelCase")]
14
-
pub struct DisableInviteCodesInput {
15
-
pub codes: Option<Vec<String>>,
16
-
pub accounts: Option<Vec<String>>,
17
-
}
18
-
19
-
pub async fn disable_invite_codes(
20
-
State(state): State<AppState>,
21
-
headers: axum::http::HeaderMap,
22
-
Json(input): Json<DisableInviteCodesInput>,
23
-
) -> Response {
24
-
let auth_header = headers.get("Authorization");
25
-
if auth_header.is_none() {
26
-
return (
27
-
StatusCode::UNAUTHORIZED,
28
-
Json(json!({"error": "AuthenticationRequired"})),
29
-
)
30
-
.into_response();
31
-
}
32
-
33
-
if let Some(codes) = &input.codes {
34
-
for code in codes {
35
-
let _ = sqlx::query!("UPDATE invite_codes SET disabled = TRUE WHERE code = $1", code)
36
-
.execute(&state.db)
37
-
.await;
38
-
}
39
-
}
40
-
41
-
if let Some(accounts) = &input.accounts {
42
-
for account in accounts {
43
-
let user = sqlx::query!("SELECT id FROM users WHERE did = $1", account)
44
-
.fetch_optional(&state.db)
45
-
.await;
46
47
-
if let Ok(Some(user_row)) = user {
48
-
let _ = sqlx::query!(
49
-
"UPDATE invite_codes SET disabled = TRUE WHERE created_by_user = $1",
50
-
user_row.id
51
-
)
52
-
.execute(&state.db)
53
-
.await;
54
-
}
55
-
}
56
-
}
57
-
58
-
(StatusCode::OK, Json(json!({}))).into_response()
59
-
}
60
-
61
-
#[derive(Deserialize)]
62
-
pub struct GetSubjectStatusParams {
63
-
pub did: Option<String>,
64
-
pub uri: Option<String>,
65
-
pub blob: Option<String>,
66
-
}
67
-
68
-
#[derive(Serialize)]
69
-
pub struct SubjectStatus {
70
-
pub subject: serde_json::Value,
71
-
pub takedown: Option<StatusAttr>,
72
-
pub deactivated: Option<StatusAttr>,
73
-
}
74
-
75
-
#[derive(Serialize)]
76
-
#[serde(rename_all = "camelCase")]
77
-
pub struct StatusAttr {
78
-
pub applied: bool,
79
-
pub r#ref: Option<String>,
80
-
}
81
-
82
-
pub async fn get_subject_status(
83
-
State(state): State<AppState>,
84
-
headers: axum::http::HeaderMap,
85
-
Query(params): Query<GetSubjectStatusParams>,
86
-
) -> Response {
87
-
let auth_header = headers.get("Authorization");
88
-
if auth_header.is_none() {
89
-
return (
90
-
StatusCode::UNAUTHORIZED,
91
-
Json(json!({"error": "AuthenticationRequired"})),
92
-
)
93
-
.into_response();
94
-
}
95
-
96
-
if params.did.is_none() && params.uri.is_none() && params.blob.is_none() {
97
-
return (
98
-
StatusCode::BAD_REQUEST,
99
-
Json(json!({"error": "InvalidRequest", "message": "Must provide did, uri, or blob"})),
100
-
)
101
-
.into_response();
102
-
}
103
-
104
-
if let Some(did) = ¶ms.did {
105
-
let user = sqlx::query!(
106
-
"SELECT did, deactivated_at, takedown_ref FROM users WHERE did = $1",
107
-
did
108
-
)
109
-
.fetch_optional(&state.db)
110
-
.await;
111
-
112
-
match user {
113
-
Ok(Some(row)) => {
114
-
let deactivated = row.deactivated_at.map(|_| StatusAttr {
115
-
applied: true,
116
-
r#ref: None,
117
-
});
118
-
let takedown = row.takedown_ref.as_ref().map(|r| StatusAttr {
119
-
applied: true,
120
-
r#ref: Some(r.clone()),
121
-
});
122
-
123
-
return (
124
-
StatusCode::OK,
125
-
Json(SubjectStatus {
126
-
subject: json!({
127
-
"$type": "com.atproto.admin.defs#repoRef",
128
-
"did": row.did
129
-
}),
130
-
takedown,
131
-
deactivated,
132
-
}),
133
-
)
134
-
.into_response();
135
-
}
136
-
Ok(None) => {
137
-
return (
138
-
StatusCode::NOT_FOUND,
139
-
Json(json!({"error": "SubjectNotFound", "message": "Subject not found"})),
140
-
)
141
-
.into_response();
142
-
}
143
-
Err(e) => {
144
-
error!("DB error in get_subject_status: {:?}", e);
145
-
return (
146
-
StatusCode::INTERNAL_SERVER_ERROR,
147
-
Json(json!({"error": "InternalError"})),
148
-
)
149
-
.into_response();
150
-
}
151
-
}
152
-
}
153
-
154
-
if let Some(uri) = ¶ms.uri {
155
-
let record = sqlx::query!(
156
-
"SELECT r.id, r.takedown_ref FROM records r WHERE r.record_cid = $1",
157
-
uri
158
-
)
159
-
.fetch_optional(&state.db)
160
-
.await;
161
-
162
-
match record {
163
-
Ok(Some(row)) => {
164
-
let takedown = row.takedown_ref.as_ref().map(|r| StatusAttr {
165
-
applied: true,
166
-
r#ref: Some(r.clone()),
167
-
});
168
-
169
-
return (
170
-
StatusCode::OK,
171
-
Json(SubjectStatus {
172
-
subject: json!({
173
-
"$type": "com.atproto.repo.strongRef",
174
-
"uri": uri,
175
-
"cid": uri
176
-
}),
177
-
takedown,
178
-
deactivated: None,
179
-
}),
180
-
)
181
-
.into_response();
182
-
}
183
-
Ok(None) => {
184
-
return (
185
-
StatusCode::NOT_FOUND,
186
-
Json(json!({"error": "SubjectNotFound", "message": "Subject not found"})),
187
-
)
188
-
.into_response();
189
-
}
190
-
Err(e) => {
191
-
error!("DB error in get_subject_status: {:?}", e);
192
-
return (
193
-
StatusCode::INTERNAL_SERVER_ERROR,
194
-
Json(json!({"error": "InternalError"})),
195
-
)
196
-
.into_response();
197
-
}
198
-
}
199
-
}
200
-
201
-
if let Some(blob_cid) = ¶ms.blob {
202
-
let blob = sqlx::query!("SELECT cid, takedown_ref FROM blobs WHERE cid = $1", blob_cid)
203
-
.fetch_optional(&state.db)
204
-
.await;
205
-
206
-
match blob {
207
-
Ok(Some(row)) => {
208
-
let takedown = row.takedown_ref.as_ref().map(|r| StatusAttr {
209
-
applied: true,
210
-
r#ref: Some(r.clone()),
211
-
});
212
-
213
-
return (
214
-
StatusCode::OK,
215
-
Json(SubjectStatus {
216
-
subject: json!({
217
-
"$type": "com.atproto.admin.defs#repoBlobRef",
218
-
"did": "",
219
-
"cid": row.cid
220
-
}),
221
-
takedown,
222
-
deactivated: None,
223
-
}),
224
-
)
225
-
.into_response();
226
-
}
227
-
Ok(None) => {
228
-
return (
229
-
StatusCode::NOT_FOUND,
230
-
Json(json!({"error": "SubjectNotFound", "message": "Subject not found"})),
231
-
)
232
-
.into_response();
233
-
}
234
-
Err(e) => {
235
-
error!("DB error in get_subject_status: {:?}", e);
236
-
return (
237
-
StatusCode::INTERNAL_SERVER_ERROR,
238
-
Json(json!({"error": "InternalError"})),
239
-
)
240
-
.into_response();
241
-
}
242
-
}
243
-
}
244
-
245
-
(
246
-
StatusCode::BAD_REQUEST,
247
-
Json(json!({"error": "InvalidRequest", "message": "Invalid subject type"})),
248
-
)
249
-
.into_response()
250
-
}
251
-
252
-
#[derive(Deserialize)]
253
-
#[serde(rename_all = "camelCase")]
254
-
pub struct UpdateSubjectStatusInput {
255
-
pub subject: serde_json::Value,
256
-
pub takedown: Option<StatusAttrInput>,
257
-
pub deactivated: Option<StatusAttrInput>,
258
-
}
259
-
260
-
#[derive(Deserialize)]
261
-
pub struct StatusAttrInput {
262
-
pub apply: bool,
263
-
pub r#ref: Option<String>,
264
-
}
265
-
266
-
pub async fn update_subject_status(
267
-
State(state): State<AppState>,
268
-
headers: axum::http::HeaderMap,
269
-
Json(input): Json<UpdateSubjectStatusInput>,
270
-
) -> Response {
271
-
let auth_header = headers.get("Authorization");
272
-
if auth_header.is_none() {
273
-
return (
274
-
StatusCode::UNAUTHORIZED,
275
-
Json(json!({"error": "AuthenticationRequired"})),
276
-
)
277
-
.into_response();
278
-
}
279
-
280
-
let subject_type = input.subject.get("$type").and_then(|t| t.as_str());
281
-
282
-
match subject_type {
283
-
Some("com.atproto.admin.defs#repoRef") => {
284
-
let did = input.subject.get("did").and_then(|d| d.as_str());
285
-
if let Some(did) = did {
286
-
if let Some(takedown) = &input.takedown {
287
-
let takedown_ref = if takedown.apply {
288
-
takedown.r#ref.clone()
289
-
} else {
290
-
None
291
-
};
292
-
let _ = sqlx::query!(
293
-
"UPDATE users SET takedown_ref = $1 WHERE did = $2",
294
-
takedown_ref,
295
-
did
296
-
)
297
-
.execute(&state.db)
298
-
.await;
299
-
}
300
-
301
-
if let Some(deactivated) = &input.deactivated {
302
-
if deactivated.apply {
303
-
let _ = sqlx::query!(
304
-
"UPDATE users SET deactivated_at = NOW() WHERE did = $1",
305
-
did
306
-
)
307
-
.execute(&state.db)
308
-
.await;
309
-
} else {
310
-
let _ = sqlx::query!(
311
-
"UPDATE users SET deactivated_at = NULL WHERE did = $1",
312
-
did
313
-
)
314
-
.execute(&state.db)
315
-
.await;
316
-
}
317
-
}
318
-
319
-
return (
320
-
StatusCode::OK,
321
-
Json(json!({
322
-
"subject": input.subject,
323
-
"takedown": input.takedown.as_ref().map(|t| json!({
324
-
"applied": t.apply,
325
-
"ref": t.r#ref
326
-
})),
327
-
"deactivated": input.deactivated.as_ref().map(|d| json!({
328
-
"applied": d.apply
329
-
}))
330
-
})),
331
-
)
332
-
.into_response();
333
-
}
334
-
}
335
-
Some("com.atproto.repo.strongRef") => {
336
-
let uri = input.subject.get("uri").and_then(|u| u.as_str());
337
-
if let Some(uri) = uri {
338
-
if let Some(takedown) = &input.takedown {
339
-
let takedown_ref = if takedown.apply {
340
-
takedown.r#ref.clone()
341
-
} else {
342
-
None
343
-
};
344
-
let _ = sqlx::query!(
345
-
"UPDATE records SET takedown_ref = $1 WHERE record_cid = $2",
346
-
takedown_ref,
347
-
uri
348
-
)
349
-
.execute(&state.db)
350
-
.await;
351
-
}
352
-
353
-
return (
354
-
StatusCode::OK,
355
-
Json(json!({
356
-
"subject": input.subject,
357
-
"takedown": input.takedown.as_ref().map(|t| json!({
358
-
"applied": t.apply,
359
-
"ref": t.r#ref
360
-
}))
361
-
})),
362
-
)
363
-
.into_response();
364
-
}
365
-
}
366
-
Some("com.atproto.admin.defs#repoBlobRef") => {
367
-
let cid = input.subject.get("cid").and_then(|c| c.as_str());
368
-
if let Some(cid) = cid {
369
-
if let Some(takedown) = &input.takedown {
370
-
let takedown_ref = if takedown.apply {
371
-
takedown.r#ref.clone()
372
-
} else {
373
-
None
374
-
};
375
-
let _ = sqlx::query!(
376
-
"UPDATE blobs SET takedown_ref = $1 WHERE cid = $2",
377
-
takedown_ref,
378
-
cid
379
-
)
380
-
.execute(&state.db)
381
-
.await;
382
-
}
383
-
384
-
return (
385
-
StatusCode::OK,
386
-
Json(json!({
387
-
"subject": input.subject,
388
-
"takedown": input.takedown.as_ref().map(|t| json!({
389
-
"applied": t.apply,
390
-
"ref": t.r#ref
391
-
}))
392
-
})),
393
-
)
394
-
.into_response();
395
-
}
396
-
}
397
-
_ => {}
398
-
}
399
-
400
-
(
401
-
StatusCode::BAD_REQUEST,
402
-
Json(json!({"error": "InvalidRequest", "message": "Invalid subject type"})),
403
-
)
404
-
.into_response()
405
-
}
406
-
407
-
#[derive(Deserialize)]
408
-
pub struct GetInviteCodesParams {
409
-
pub sort: Option<String>,
410
-
pub limit: Option<i64>,
411
-
pub cursor: Option<String>,
412
-
}
413
-
414
-
#[derive(Serialize)]
415
-
#[serde(rename_all = "camelCase")]
416
-
pub struct InviteCodeInfo {
417
-
pub code: String,
418
-
pub available: i32,
419
-
pub disabled: bool,
420
-
pub for_account: String,
421
-
pub created_by: String,
422
-
pub created_at: String,
423
-
pub uses: Vec<InviteCodeUseInfo>,
424
-
}
425
-
426
-
#[derive(Serialize)]
427
-
#[serde(rename_all = "camelCase")]
428
-
pub struct InviteCodeUseInfo {
429
-
pub used_by: String,
430
-
pub used_at: String,
431
-
}
432
-
433
-
#[derive(Serialize)]
434
-
pub struct GetInviteCodesOutput {
435
-
pub cursor: Option<String>,
436
-
pub codes: Vec<InviteCodeInfo>,
437
-
}
438
-
439
-
pub async fn get_invite_codes(
440
-
State(state): State<AppState>,
441
-
headers: axum::http::HeaderMap,
442
-
Query(params): Query<GetInviteCodesParams>,
443
-
) -> Response {
444
-
let auth_header = headers.get("Authorization");
445
-
if auth_header.is_none() {
446
-
return (
447
-
StatusCode::UNAUTHORIZED,
448
-
Json(json!({"error": "AuthenticationRequired"})),
449
-
)
450
-
.into_response();
451
-
}
452
-
453
-
let limit = params.limit.unwrap_or(100).min(500);
454
-
let sort = params.sort.as_deref().unwrap_or("recent");
455
-
456
-
let order_clause = match sort {
457
-
"usage" => "available_uses DESC",
458
-
_ => "created_at DESC",
459
-
};
460
-
461
-
let codes_result = if let Some(cursor) = ¶ms.cursor {
462
-
sqlx::query_as::<_, (String, i32, Option<bool>, uuid::Uuid, chrono::DateTime<chrono::Utc>)>(&format!(
463
-
r#"
464
-
SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at
465
-
FROM invite_codes ic
466
-
WHERE ic.created_at < (SELECT created_at FROM invite_codes WHERE code = $1)
467
-
ORDER BY {}
468
-
LIMIT $2
469
-
"#,
470
-
order_clause
471
-
))
472
-
.bind(cursor)
473
-
.bind(limit)
474
-
.fetch_all(&state.db)
475
-
.await
476
-
} else {
477
-
sqlx::query_as::<_, (String, i32, Option<bool>, uuid::Uuid, chrono::DateTime<chrono::Utc>)>(&format!(
478
-
r#"
479
-
SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at
480
-
FROM invite_codes ic
481
-
ORDER BY {}
482
-
LIMIT $1
483
-
"#,
484
-
order_clause
485
-
))
486
-
.bind(limit)
487
-
.fetch_all(&state.db)
488
-
.await
489
-
};
490
-
491
-
let codes_rows = match codes_result {
492
-
Ok(rows) => rows,
493
-
Err(e) => {
494
-
error!("DB error fetching invite codes: {:?}", e);
495
-
return (
496
-
StatusCode::INTERNAL_SERVER_ERROR,
497
-
Json(json!({"error": "InternalError"})),
498
-
)
499
-
.into_response();
500
-
}
501
-
};
502
-
503
-
let mut codes = Vec::new();
504
-
for (code, available_uses, disabled, created_by_user, created_at) in &codes_rows {
505
-
let creator_did = sqlx::query_scalar!("SELECT did FROM users WHERE id = $1", created_by_user)
506
-
.fetch_optional(&state.db)
507
-
.await
508
-
.ok()
509
-
.flatten()
510
-
.unwrap_or_else(|| "unknown".to_string());
511
-
512
-
let uses_result = sqlx::query!(
513
-
r#"
514
-
SELECT u.did, icu.used_at
515
-
FROM invite_code_uses icu
516
-
JOIN users u ON icu.used_by_user = u.id
517
-
WHERE icu.code = $1
518
-
ORDER BY icu.used_at DESC
519
-
"#,
520
-
code
521
-
)
522
-
.fetch_all(&state.db)
523
-
.await;
524
-
525
-
let uses = match uses_result {
526
-
Ok(use_rows) => use_rows
527
-
.iter()
528
-
.map(|u| InviteCodeUseInfo {
529
-
used_by: u.did.clone(),
530
-
used_at: u.used_at.to_rfc3339(),
531
-
})
532
-
.collect(),
533
-
Err(_) => Vec::new(),
534
-
};
535
-
536
-
codes.push(InviteCodeInfo {
537
-
code: code.clone(),
538
-
available: *available_uses,
539
-
disabled: disabled.unwrap_or(false),
540
-
for_account: creator_did.clone(),
541
-
created_by: creator_did,
542
-
created_at: created_at.to_rfc3339(),
543
-
uses,
544
-
});
545
-
}
546
-
547
-
let next_cursor = if codes_rows.len() == limit as usize {
548
-
codes_rows.last().map(|(code, _, _, _, _)| code.clone())
549
-
} else {
550
-
None
551
-
};
552
-
553
-
(
554
-
StatusCode::OK,
555
-
Json(GetInviteCodesOutput {
556
-
cursor: next_cursor,
557
-
codes,
558
-
}),
559
-
)
560
-
.into_response()
561
-
}
562
-
563
-
#[derive(Deserialize)]
564
-
pub struct DisableAccountInvitesInput {
565
-
pub account: String,
566
-
}
567
-
568
-
pub async fn disable_account_invites(
569
-
State(state): State<AppState>,
570
-
headers: axum::http::HeaderMap,
571
-
Json(input): Json<DisableAccountInvitesInput>,
572
-
) -> Response {
573
-
let auth_header = headers.get("Authorization");
574
-
if auth_header.is_none() {
575
-
return (
576
-
StatusCode::UNAUTHORIZED,
577
-
Json(json!({"error": "AuthenticationRequired"})),
578
-
)
579
-
.into_response();
580
-
}
581
-
582
-
let account = input.account.trim();
583
-
if account.is_empty() {
584
-
return (
585
-
StatusCode::BAD_REQUEST,
586
-
Json(json!({"error": "InvalidRequest", "message": "account is required"})),
587
-
)
588
-
.into_response();
589
-
}
590
-
591
-
let result = sqlx::query!("UPDATE users SET invites_disabled = TRUE WHERE did = $1", account)
592
-
.execute(&state.db)
593
-
.await;
594
-
595
-
match result {
596
-
Ok(r) => {
597
-
if r.rows_affected() == 0 {
598
-
return (
599
-
StatusCode::NOT_FOUND,
600
-
Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
601
-
)
602
-
.into_response();
603
-
}
604
-
(StatusCode::OK, Json(json!({}))).into_response()
605
-
}
606
-
Err(e) => {
607
-
error!("DB error disabling account invites: {:?}", e);
608
-
(
609
-
StatusCode::INTERNAL_SERVER_ERROR,
610
-
Json(json!({"error": "InternalError"})),
611
-
)
612
-
.into_response()
613
-
}
614
-
}
615
-
}
616
-
617
-
#[derive(Deserialize)]
618
-
pub struct EnableAccountInvitesInput {
619
-
pub account: String,
620
-
}
621
-
622
-
pub async fn enable_account_invites(
623
-
State(state): State<AppState>,
624
-
headers: axum::http::HeaderMap,
625
-
Json(input): Json<EnableAccountInvitesInput>,
626
-
) -> Response {
627
-
let auth_header = headers.get("Authorization");
628
-
if auth_header.is_none() {
629
-
return (
630
-
StatusCode::UNAUTHORIZED,
631
-
Json(json!({"error": "AuthenticationRequired"})),
632
-
)
633
-
.into_response();
634
-
}
635
-
636
-
let account = input.account.trim();
637
-
if account.is_empty() {
638
-
return (
639
-
StatusCode::BAD_REQUEST,
640
-
Json(json!({"error": "InvalidRequest", "message": "account is required"})),
641
-
)
642
-
.into_response();
643
-
}
644
-
645
-
let result = sqlx::query!("UPDATE users SET invites_disabled = FALSE WHERE did = $1", account)
646
-
.execute(&state.db)
647
-
.await;
648
-
649
-
match result {
650
-
Ok(r) => {
651
-
if r.rows_affected() == 0 {
652
-
return (
653
-
StatusCode::NOT_FOUND,
654
-
Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
655
-
)
656
-
.into_response();
657
-
}
658
-
(StatusCode::OK, Json(json!({}))).into_response()
659
-
}
660
-
Err(e) => {
661
-
error!("DB error enabling account invites: {:?}", e);
662
-
(
663
-
StatusCode::INTERNAL_SERVER_ERROR,
664
-
Json(json!({"error": "InternalError"})),
665
-
)
666
-
.into_response()
667
-
}
668
-
}
669
-
}
670
-
671
-
#[derive(Deserialize)]
672
-
pub struct GetAccountInfoParams {
673
-
pub did: String,
674
-
}
675
-
676
-
#[derive(Serialize)]
677
-
#[serde(rename_all = "camelCase")]
678
-
pub struct AccountInfo {
679
-
pub did: String,
680
-
pub handle: String,
681
-
pub email: Option<String>,
682
-
pub indexed_at: String,
683
-
pub invite_note: Option<String>,
684
-
pub invites_disabled: bool,
685
-
pub email_confirmed_at: Option<String>,
686
-
pub deactivated_at: Option<String>,
687
-
}
688
-
689
-
#[derive(Serialize)]
690
-
#[serde(rename_all = "camelCase")]
691
-
pub struct GetAccountInfosOutput {
692
-
pub infos: Vec<AccountInfo>,
693
-
}
694
-
695
-
pub async fn get_account_info(
696
-
State(state): State<AppState>,
697
-
headers: axum::http::HeaderMap,
698
-
Query(params): Query<GetAccountInfoParams>,
699
-
) -> Response {
700
-
let auth_header = headers.get("Authorization");
701
-
if auth_header.is_none() {
702
-
return (
703
-
StatusCode::UNAUTHORIZED,
704
-
Json(json!({"error": "AuthenticationRequired"})),
705
-
)
706
-
.into_response();
707
-
}
708
-
709
-
let did = params.did.trim();
710
-
if did.is_empty() {
711
-
return (
712
-
StatusCode::BAD_REQUEST,
713
-
Json(json!({"error": "InvalidRequest", "message": "did is required"})),
714
-
)
715
-
.into_response();
716
-
}
717
-
718
-
let result = sqlx::query!(
719
-
r#"
720
-
SELECT did, handle, email, created_at
721
-
FROM users
722
-
WHERE did = $1
723
-
"#,
724
-
did
725
-
)
726
-
.fetch_optional(&state.db)
727
-
.await;
728
-
729
-
match result {
730
-
Ok(Some(row)) => {
731
-
(
732
-
StatusCode::OK,
733
-
Json(AccountInfo {
734
-
did: row.did,
735
-
handle: row.handle,
736
-
email: Some(row.email),
737
-
indexed_at: row.created_at.to_rfc3339(),
738
-
invite_note: None,
739
-
invites_disabled: false,
740
-
email_confirmed_at: None,
741
-
deactivated_at: None,
742
-
}),
743
-
)
744
-
.into_response()
745
-
}
746
-
Ok(None) => (
747
-
StatusCode::NOT_FOUND,
748
-
Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
749
-
)
750
-
.into_response(),
751
-
Err(e) => {
752
-
error!("DB error in get_account_info: {:?}", e);
753
-
(
754
-
StatusCode::INTERNAL_SERVER_ERROR,
755
-
Json(json!({"error": "InternalError"})),
756
-
)
757
-
.into_response()
758
-
}
759
-
}
760
-
}
761
-
762
-
#[derive(Deserialize)]
763
-
pub struct GetAccountInfosParams {
764
-
pub dids: String,
765
-
}
766
-
767
-
pub async fn get_account_infos(
768
-
State(state): State<AppState>,
769
-
headers: axum::http::HeaderMap,
770
-
Query(params): Query<GetAccountInfosParams>,
771
-
) -> Response {
772
-
let auth_header = headers.get("Authorization");
773
-
if auth_header.is_none() {
774
-
return (
775
-
StatusCode::UNAUTHORIZED,
776
-
Json(json!({"error": "AuthenticationRequired"})),
777
-
)
778
-
.into_response();
779
-
}
780
-
781
-
let dids: Vec<&str> = params.dids.split(',').map(|s| s.trim()).collect();
782
-
if dids.is_empty() {
783
-
return (
784
-
StatusCode::BAD_REQUEST,
785
-
Json(json!({"error": "InvalidRequest", "message": "dids is required"})),
786
-
)
787
-
.into_response();
788
-
}
789
-
790
-
let mut infos = Vec::new();
791
-
792
-
for did in dids {
793
-
if did.is_empty() {
794
-
continue;
795
-
}
796
-
797
-
let result = sqlx::query!(
798
-
r#"
799
-
SELECT did, handle, email, created_at
800
-
FROM users
801
-
WHERE did = $1
802
-
"#,
803
-
did
804
-
)
805
-
.fetch_optional(&state.db)
806
-
.await;
807
-
808
-
if let Ok(Some(row)) = result {
809
-
infos.push(AccountInfo {
810
-
did: row.did,
811
-
handle: row.handle,
812
-
email: Some(row.email),
813
-
indexed_at: row.created_at.to_rfc3339(),
814
-
invite_note: None,
815
-
invites_disabled: false,
816
-
email_confirmed_at: None,
817
-
deactivated_at: None,
818
-
});
819
-
}
820
-
}
821
-
822
-
(StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response()
823
-
}
824
-
825
-
#[derive(Deserialize)]
826
-
pub struct DeleteAccountInput {
827
-
pub did: String,
828
-
}
829
-
830
-
pub async fn delete_account(
831
-
State(state): State<AppState>,
832
-
headers: axum::http::HeaderMap,
833
-
Json(input): Json<DeleteAccountInput>,
834
-
) -> Response {
835
-
let auth_header = headers.get("Authorization");
836
-
if auth_header.is_none() {
837
-
return (
838
-
StatusCode::UNAUTHORIZED,
839
-
Json(json!({"error": "AuthenticationRequired"})),
840
-
)
841
-
.into_response();
842
-
}
843
-
844
-
let did = input.did.trim();
845
-
if did.is_empty() {
846
-
return (
847
-
StatusCode::BAD_REQUEST,
848
-
Json(json!({"error": "InvalidRequest", "message": "did is required"})),
849
-
)
850
-
.into_response();
851
-
}
852
-
853
-
let user = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
854
-
.fetch_optional(&state.db)
855
-
.await;
856
-
857
-
let user_id = match user {
858
-
Ok(Some(row)) => row.id,
859
-
Ok(None) => {
860
-
return (
861
-
StatusCode::NOT_FOUND,
862
-
Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
863
-
)
864
-
.into_response();
865
-
}
866
-
Err(e) => {
867
-
error!("DB error in delete_account: {:?}", e);
868
-
return (
869
-
StatusCode::INTERNAL_SERVER_ERROR,
870
-
Json(json!({"error": "InternalError"})),
871
-
)
872
-
.into_response();
873
-
}
874
-
};
875
-
876
-
let _ = sqlx::query!("DELETE FROM sessions WHERE did = $1", did)
877
-
.execute(&state.db)
878
-
.await;
879
-
880
-
let _ = sqlx::query!("DELETE FROM records WHERE repo_id = $1", user_id)
881
-
.execute(&state.db)
882
-
.await;
883
-
884
-
let _ = sqlx::query!("DELETE FROM repos WHERE user_id = $1", user_id)
885
-
.execute(&state.db)
886
-
.await;
887
-
888
-
let _ = sqlx::query!("DELETE FROM blobs WHERE created_by_user = $1", user_id)
889
-
.execute(&state.db)
890
-
.await;
891
-
892
-
let _ = sqlx::query!("DELETE FROM user_keys WHERE user_id = $1", user_id)
893
-
.execute(&state.db)
894
-
.await;
895
-
896
-
let result = sqlx::query!("DELETE FROM users WHERE id = $1", user_id)
897
-
.execute(&state.db)
898
-
.await;
899
-
900
-
match result {
901
-
Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(),
902
-
Err(e) => {
903
-
error!("DB error deleting account: {:?}", e);
904
-
(
905
-
StatusCode::INTERNAL_SERVER_ERROR,
906
-
Json(json!({"error": "InternalError"})),
907
-
)
908
-
.into_response()
909
-
}
910
-
}
911
-
}
912
-
913
-
#[derive(Deserialize)]
914
-
pub struct UpdateAccountEmailInput {
915
-
pub account: String,
916
-
pub email: String,
917
-
}
918
-
919
-
pub async fn update_account_email(
920
-
State(state): State<AppState>,
921
-
headers: axum::http::HeaderMap,
922
-
Json(input): Json<UpdateAccountEmailInput>,
923
-
) -> Response {
924
-
let auth_header = headers.get("Authorization");
925
-
if auth_header.is_none() {
926
-
return (
927
-
StatusCode::UNAUTHORIZED,
928
-
Json(json!({"error": "AuthenticationRequired"})),
929
-
)
930
-
.into_response();
931
-
}
932
-
933
-
let account = input.account.trim();
934
-
let email = input.email.trim();
935
-
936
-
if account.is_empty() || email.is_empty() {
937
-
return (
938
-
StatusCode::BAD_REQUEST,
939
-
Json(json!({"error": "InvalidRequest", "message": "account and email are required"})),
940
-
)
941
-
.into_response();
942
-
}
943
-
944
-
let result = sqlx::query!("UPDATE users SET email = $1 WHERE did = $2", email, account)
945
-
.execute(&state.db)
946
-
.await;
947
-
948
-
match result {
949
-
Ok(r) => {
950
-
if r.rows_affected() == 0 {
951
-
return (
952
-
StatusCode::NOT_FOUND,
953
-
Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
954
-
)
955
-
.into_response();
956
-
}
957
-
(StatusCode::OK, Json(json!({}))).into_response()
958
-
}
959
-
Err(e) => {
960
-
error!("DB error updating email: {:?}", e);
961
-
(
962
-
StatusCode::INTERNAL_SERVER_ERROR,
963
-
Json(json!({"error": "InternalError"})),
964
-
)
965
-
.into_response()
966
-
}
967
-
}
968
-
}
969
-
970
-
#[derive(Deserialize)]
971
-
pub struct UpdateAccountHandleInput {
972
-
pub did: String,
973
-
pub handle: String,
974
-
}
975
-
976
-
pub async fn update_account_handle(
977
-
State(state): State<AppState>,
978
-
headers: axum::http::HeaderMap,
979
-
Json(input): Json<UpdateAccountHandleInput>,
980
-
) -> Response {
981
-
let auth_header = headers.get("Authorization");
982
-
if auth_header.is_none() {
983
-
return (
984
-
StatusCode::UNAUTHORIZED,
985
-
Json(json!({"error": "AuthenticationRequired"})),
986
-
)
987
-
.into_response();
988
-
}
989
-
990
-
let did = input.did.trim();
991
-
let handle = input.handle.trim();
992
-
993
-
if did.is_empty() || handle.is_empty() {
994
-
return (
995
-
StatusCode::BAD_REQUEST,
996
-
Json(json!({"error": "InvalidRequest", "message": "did and handle are required"})),
997
-
)
998
-
.into_response();
999
-
}
1000
-
1001
-
if !handle
1002
-
.chars()
1003
-
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_')
1004
-
{
1005
-
return (
1006
-
StatusCode::BAD_REQUEST,
1007
-
Json(json!({"error": "InvalidHandle", "message": "Handle contains invalid characters"})),
1008
-
)
1009
-
.into_response();
1010
-
}
1011
-
1012
-
let existing = sqlx::query!("SELECT id FROM users WHERE handle = $1 AND did != $2", handle, did)
1013
-
.fetch_optional(&state.db)
1014
-
.await;
1015
-
1016
-
if let Ok(Some(_)) = existing {
1017
-
return (
1018
-
StatusCode::BAD_REQUEST,
1019
-
Json(json!({"error": "HandleTaken", "message": "Handle is already in use"})),
1020
-
)
1021
-
.into_response();
1022
-
}
1023
-
1024
-
let result = sqlx::query!("UPDATE users SET handle = $1 WHERE did = $2", handle, did)
1025
-
.execute(&state.db)
1026
-
.await;
1027
-
1028
-
match result {
1029
-
Ok(r) => {
1030
-
if r.rows_affected() == 0 {
1031
-
return (
1032
-
StatusCode::NOT_FOUND,
1033
-
Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
1034
-
)
1035
-
.into_response();
1036
-
}
1037
-
(StatusCode::OK, Json(json!({}))).into_response()
1038
-
}
1039
-
Err(e) => {
1040
-
error!("DB error updating handle: {:?}", e);
1041
-
(
1042
-
StatusCode::INTERNAL_SERVER_ERROR,
1043
-
Json(json!({"error": "InternalError"})),
1044
-
)
1045
-
.into_response()
1046
-
}
1047
-
}
1048
-
}
1049
-
1050
-
#[derive(Deserialize)]
1051
-
pub struct UpdateAccountPasswordInput {
1052
-
pub did: String,
1053
-
pub password: String,
1054
-
}
1055
-
1056
-
pub async fn update_account_password(
1057
-
State(state): State<AppState>,
1058
-
headers: axum::http::HeaderMap,
1059
-
Json(input): Json<UpdateAccountPasswordInput>,
1060
-
) -> Response {
1061
-
let auth_header = headers.get("Authorization");
1062
-
if auth_header.is_none() {
1063
-
return (
1064
-
StatusCode::UNAUTHORIZED,
1065
-
Json(json!({"error": "AuthenticationRequired"})),
1066
-
)
1067
-
.into_response();
1068
-
}
1069
-
1070
-
let did = input.did.trim();
1071
-
let password = input.password.trim();
1072
-
1073
-
if did.is_empty() || password.is_empty() {
1074
-
return (
1075
-
StatusCode::BAD_REQUEST,
1076
-
Json(json!({"error": "InvalidRequest", "message": "did and password are required"})),
1077
-
)
1078
-
.into_response();
1079
-
}
1080
-
1081
-
let password_hash = match bcrypt::hash(password, bcrypt::DEFAULT_COST) {
1082
-
Ok(h) => h,
1083
-
Err(e) => {
1084
-
error!("Failed to hash password: {:?}", e);
1085
-
return (
1086
-
StatusCode::INTERNAL_SERVER_ERROR,
1087
-
Json(json!({"error": "InternalError"})),
1088
-
)
1089
-
.into_response();
1090
-
}
1091
-
};
1092
-
1093
-
let result = sqlx::query!("UPDATE users SET password_hash = $1 WHERE did = $2", password_hash, did)
1094
-
.execute(&state.db)
1095
-
.await;
1096
-
1097
-
match result {
1098
-
Ok(r) => {
1099
-
if r.rows_affected() == 0 {
1100
-
return (
1101
-
StatusCode::NOT_FOUND,
1102
-
Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
1103
-
)
1104
-
.into_response();
1105
-
}
1106
-
(StatusCode::OK, Json(json!({}))).into_response()
1107
-
}
1108
-
Err(e) => {
1109
-
error!("DB error updating password: {:?}", e);
1110
-
(
1111
-
StatusCode::INTERNAL_SERVER_ERROR,
1112
-
Json(json!({"error": "InternalError"})),
1113
-
)
1114
-
.into_response()
1115
-
}
1116
-
}
1117
-
}
1118
-
1119
-
#[derive(Deserialize)]
1120
-
#[serde(rename_all = "camelCase")]
1121
-
pub struct SendEmailInput {
1122
-
pub recipient_did: String,
1123
-
pub sender_did: String,
1124
-
pub content: String,
1125
-
pub subject: Option<String>,
1126
-
pub comment: Option<String>,
1127
-
}
1128
-
1129
-
#[derive(Serialize)]
1130
-
pub struct SendEmailOutput {
1131
-
pub sent: bool,
1132
-
}
1133
-
1134
-
pub async fn send_email(
1135
-
State(state): State<AppState>,
1136
-
headers: axum::http::HeaderMap,
1137
-
Json(input): Json<SendEmailInput>,
1138
-
) -> Response {
1139
-
let auth_header = headers.get("Authorization");
1140
-
if auth_header.is_none() {
1141
-
return (
1142
-
StatusCode::UNAUTHORIZED,
1143
-
Json(json!({"error": "AuthenticationRequired"})),
1144
-
)
1145
-
.into_response();
1146
-
}
1147
-
1148
-
let recipient_did = input.recipient_did.trim();
1149
-
let content = input.content.trim();
1150
-
1151
-
if recipient_did.is_empty() {
1152
-
return (
1153
-
StatusCode::BAD_REQUEST,
1154
-
Json(json!({"error": "InvalidRequest", "message": "recipientDid is required"})),
1155
-
)
1156
-
.into_response();
1157
-
}
1158
-
1159
-
if content.is_empty() {
1160
-
return (
1161
-
StatusCode::BAD_REQUEST,
1162
-
Json(json!({"error": "InvalidRequest", "message": "content is required"})),
1163
-
)
1164
-
.into_response();
1165
-
}
1166
-
1167
-
let user = sqlx::query!(
1168
-
"SELECT id, email, handle FROM users WHERE did = $1",
1169
-
recipient_did
1170
-
)
1171
-
.fetch_optional(&state.db)
1172
-
.await;
1173
-
1174
-
let (user_id, email, handle) = match user {
1175
-
Ok(Some(row)) => (row.id, row.email, row.handle),
1176
-
Ok(None) => {
1177
-
return (
1178
-
StatusCode::NOT_FOUND,
1179
-
Json(json!({"error": "AccountNotFound", "message": "Recipient account not found"})),
1180
-
)
1181
-
.into_response();
1182
-
}
1183
-
Err(e) => {
1184
-
error!("DB error in send_email: {:?}", e);
1185
-
return (
1186
-
StatusCode::INTERNAL_SERVER_ERROR,
1187
-
Json(json!({"error": "InternalError"})),
1188
-
)
1189
-
.into_response();
1190
-
}
1191
-
};
1192
-
1193
-
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
1194
-
let subject = input
1195
-
.subject
1196
-
.clone()
1197
-
.unwrap_or_else(|| format!("Message from {}", hostname));
1198
-
1199
-
let notification = crate::notifications::NewNotification::email(
1200
-
user_id,
1201
-
crate::notifications::NotificationType::AdminEmail,
1202
-
email,
1203
-
subject,
1204
-
content.to_string(),
1205
-
);
1206
-
1207
-
let result = crate::notifications::enqueue_notification(&state.db, notification).await;
1208
-
1209
-
match result {
1210
-
Ok(_) => {
1211
-
tracing::info!(
1212
-
"Admin email queued for {} ({})",
1213
-
handle,
1214
-
recipient_did
1215
-
);
1216
-
(StatusCode::OK, Json(SendEmailOutput { sent: true })).into_response()
1217
-
}
1218
-
Err(e) => {
1219
-
warn!("Failed to enqueue admin email: {:?}", e);
1220
-
(StatusCode::OK, Json(SendEmailOutput { sent: false })).into_response()
1221
-
}
1222
-
}
1223
-
}
···
1
+
pub mod account;
2
+
pub mod invite;
3
+
pub mod status;
4
5
+
pub use account::{
6
+
delete_account, get_account_info, get_account_infos, send_email, update_account_email,
7
+
update_account_handle, update_account_password,
8
+
};
9
+
pub use invite::{
10
+
disable_account_invites, disable_invite_codes, enable_account_invites, get_invite_codes,
11
+
};
12
+
pub use status::{get_subject_status, update_subject_status};
+356
src/api/admin/status.rs
+356
src/api/admin/status.rs
···
···
1
+
use crate::state::AppState;
2
+
use axum::{
3
+
Json,
4
+
extract::{Query, State},
5
+
http::StatusCode,
6
+
response::{IntoResponse, Response},
7
+
};
8
+
use serde::{Deserialize, Serialize};
9
+
use serde_json::json;
10
+
use tracing::error;
11
+
12
+
#[derive(Deserialize)]
13
+
pub struct GetSubjectStatusParams {
14
+
pub did: Option<String>,
15
+
pub uri: Option<String>,
16
+
pub blob: Option<String>,
17
+
}
18
+
19
+
#[derive(Serialize)]
20
+
pub struct SubjectStatus {
21
+
pub subject: serde_json::Value,
22
+
pub takedown: Option<StatusAttr>,
23
+
pub deactivated: Option<StatusAttr>,
24
+
}
25
+
26
+
#[derive(Serialize)]
27
+
#[serde(rename_all = "camelCase")]
28
+
pub struct StatusAttr {
29
+
pub applied: bool,
30
+
pub r#ref: Option<String>,
31
+
}
32
+
33
+
pub async fn get_subject_status(
34
+
State(state): State<AppState>,
35
+
headers: axum::http::HeaderMap,
36
+
Query(params): Query<GetSubjectStatusParams>,
37
+
) -> Response {
38
+
let auth_header = headers.get("Authorization");
39
+
if auth_header.is_none() {
40
+
return (
41
+
StatusCode::UNAUTHORIZED,
42
+
Json(json!({"error": "AuthenticationRequired"})),
43
+
)
44
+
.into_response();
45
+
}
46
+
47
+
if params.did.is_none() && params.uri.is_none() && params.blob.is_none() {
48
+
return (
49
+
StatusCode::BAD_REQUEST,
50
+
Json(json!({"error": "InvalidRequest", "message": "Must provide did, uri, or blob"})),
51
+
)
52
+
.into_response();
53
+
}
54
+
55
+
if let Some(did) = ¶ms.did {
56
+
let user = sqlx::query!(
57
+
"SELECT did, deactivated_at, takedown_ref FROM users WHERE did = $1",
58
+
did
59
+
)
60
+
.fetch_optional(&state.db)
61
+
.await;
62
+
63
+
match user {
64
+
Ok(Some(row)) => {
65
+
let deactivated = row.deactivated_at.map(|_| StatusAttr {
66
+
applied: true,
67
+
r#ref: None,
68
+
});
69
+
let takedown = row.takedown_ref.as_ref().map(|r| StatusAttr {
70
+
applied: true,
71
+
r#ref: Some(r.clone()),
72
+
});
73
+
74
+
return (
75
+
StatusCode::OK,
76
+
Json(SubjectStatus {
77
+
subject: json!({
78
+
"$type": "com.atproto.admin.defs#repoRef",
79
+
"did": row.did
80
+
}),
81
+
takedown,
82
+
deactivated,
83
+
}),
84
+
)
85
+
.into_response();
86
+
}
87
+
Ok(None) => {
88
+
return (
89
+
StatusCode::NOT_FOUND,
90
+
Json(json!({"error": "SubjectNotFound", "message": "Subject not found"})),
91
+
)
92
+
.into_response();
93
+
}
94
+
Err(e) => {
95
+
error!("DB error in get_subject_status: {:?}", e);
96
+
return (
97
+
StatusCode::INTERNAL_SERVER_ERROR,
98
+
Json(json!({"error": "InternalError"})),
99
+
)
100
+
.into_response();
101
+
}
102
+
}
103
+
}
104
+
105
+
if let Some(uri) = ¶ms.uri {
106
+
let record = sqlx::query!(
107
+
"SELECT r.id, r.takedown_ref FROM records r WHERE r.record_cid = $1",
108
+
uri
109
+
)
110
+
.fetch_optional(&state.db)
111
+
.await;
112
+
113
+
match record {
114
+
Ok(Some(row)) => {
115
+
let takedown = row.takedown_ref.as_ref().map(|r| StatusAttr {
116
+
applied: true,
117
+
r#ref: Some(r.clone()),
118
+
});
119
+
120
+
return (
121
+
StatusCode::OK,
122
+
Json(SubjectStatus {
123
+
subject: json!({
124
+
"$type": "com.atproto.repo.strongRef",
125
+
"uri": uri,
126
+
"cid": uri
127
+
}),
128
+
takedown,
129
+
deactivated: None,
130
+
}),
131
+
)
132
+
.into_response();
133
+
}
134
+
Ok(None) => {
135
+
return (
136
+
StatusCode::NOT_FOUND,
137
+
Json(json!({"error": "SubjectNotFound", "message": "Subject not found"})),
138
+
)
139
+
.into_response();
140
+
}
141
+
Err(e) => {
142
+
error!("DB error in get_subject_status: {:?}", e);
143
+
return (
144
+
StatusCode::INTERNAL_SERVER_ERROR,
145
+
Json(json!({"error": "InternalError"})),
146
+
)
147
+
.into_response();
148
+
}
149
+
}
150
+
}
151
+
152
+
if let Some(blob_cid) = ¶ms.blob {
153
+
let blob = sqlx::query!("SELECT cid, takedown_ref FROM blobs WHERE cid = $1", blob_cid)
154
+
.fetch_optional(&state.db)
155
+
.await;
156
+
157
+
match blob {
158
+
Ok(Some(row)) => {
159
+
let takedown = row.takedown_ref.as_ref().map(|r| StatusAttr {
160
+
applied: true,
161
+
r#ref: Some(r.clone()),
162
+
});
163
+
164
+
return (
165
+
StatusCode::OK,
166
+
Json(SubjectStatus {
167
+
subject: json!({
168
+
"$type": "com.atproto.admin.defs#repoBlobRef",
169
+
"did": "",
170
+
"cid": row.cid
171
+
}),
172
+
takedown,
173
+
deactivated: None,
174
+
}),
175
+
)
176
+
.into_response();
177
+
}
178
+
Ok(None) => {
179
+
return (
180
+
StatusCode::NOT_FOUND,
181
+
Json(json!({"error": "SubjectNotFound", "message": "Subject not found"})),
182
+
)
183
+
.into_response();
184
+
}
185
+
Err(e) => {
186
+
error!("DB error in get_subject_status: {:?}", e);
187
+
return (
188
+
StatusCode::INTERNAL_SERVER_ERROR,
189
+
Json(json!({"error": "InternalError"})),
190
+
)
191
+
.into_response();
192
+
}
193
+
}
194
+
}
195
+
196
+
(
197
+
StatusCode::BAD_REQUEST,
198
+
Json(json!({"error": "InvalidRequest", "message": "Invalid subject type"})),
199
+
)
200
+
.into_response()
201
+
}
202
+
203
+
#[derive(Deserialize)]
204
+
#[serde(rename_all = "camelCase")]
205
+
pub struct UpdateSubjectStatusInput {
206
+
pub subject: serde_json::Value,
207
+
pub takedown: Option<StatusAttrInput>,
208
+
pub deactivated: Option<StatusAttrInput>,
209
+
}
210
+
211
+
#[derive(Deserialize)]
212
+
pub struct StatusAttrInput {
213
+
pub apply: bool,
214
+
pub r#ref: Option<String>,
215
+
}
216
+
217
+
pub async fn update_subject_status(
218
+
State(state): State<AppState>,
219
+
headers: axum::http::HeaderMap,
220
+
Json(input): Json<UpdateSubjectStatusInput>,
221
+
) -> Response {
222
+
let auth_header = headers.get("Authorization");
223
+
if auth_header.is_none() {
224
+
return (
225
+
StatusCode::UNAUTHORIZED,
226
+
Json(json!({"error": "AuthenticationRequired"})),
227
+
)
228
+
.into_response();
229
+
}
230
+
231
+
let subject_type = input.subject.get("$type").and_then(|t| t.as_str());
232
+
233
+
match subject_type {
234
+
Some("com.atproto.admin.defs#repoRef") => {
235
+
let did = input.subject.get("did").and_then(|d| d.as_str());
236
+
if let Some(did) = did {
237
+
if let Some(takedown) = &input.takedown {
238
+
let takedown_ref = if takedown.apply {
239
+
takedown.r#ref.clone()
240
+
} else {
241
+
None
242
+
};
243
+
let _ = sqlx::query!(
244
+
"UPDATE users SET takedown_ref = $1 WHERE did = $2",
245
+
takedown_ref,
246
+
did
247
+
)
248
+
.execute(&state.db)
249
+
.await;
250
+
}
251
+
252
+
if let Some(deactivated) = &input.deactivated {
253
+
if deactivated.apply {
254
+
let _ = sqlx::query!(
255
+
"UPDATE users SET deactivated_at = NOW() WHERE did = $1",
256
+
did
257
+
)
258
+
.execute(&state.db)
259
+
.await;
260
+
} else {
261
+
let _ = sqlx::query!(
262
+
"UPDATE users SET deactivated_at = NULL WHERE did = $1",
263
+
did
264
+
)
265
+
.execute(&state.db)
266
+
.await;
267
+
}
268
+
}
269
+
270
+
return (
271
+
StatusCode::OK,
272
+
Json(json!({
273
+
"subject": input.subject,
274
+
"takedown": input.takedown.as_ref().map(|t| json!({
275
+
"applied": t.apply,
276
+
"ref": t.r#ref
277
+
})),
278
+
"deactivated": input.deactivated.as_ref().map(|d| json!({
279
+
"applied": d.apply
280
+
}))
281
+
})),
282
+
)
283
+
.into_response();
284
+
}
285
+
}
286
+
Some("com.atproto.repo.strongRef") => {
287
+
let uri = input.subject.get("uri").and_then(|u| u.as_str());
288
+
if let Some(uri) = uri {
289
+
if let Some(takedown) = &input.takedown {
290
+
let takedown_ref = if takedown.apply {
291
+
takedown.r#ref.clone()
292
+
} else {
293
+
None
294
+
};
295
+
let _ = sqlx::query!(
296
+
"UPDATE records SET takedown_ref = $1 WHERE record_cid = $2",
297
+
takedown_ref,
298
+
uri
299
+
)
300
+
.execute(&state.db)
301
+
.await;
302
+
}
303
+
304
+
return (
305
+
StatusCode::OK,
306
+
Json(json!({
307
+
"subject": input.subject,
308
+
"takedown": input.takedown.as_ref().map(|t| json!({
309
+
"applied": t.apply,
310
+
"ref": t.r#ref
311
+
}))
312
+
})),
313
+
)
314
+
.into_response();
315
+
}
316
+
}
317
+
Some("com.atproto.admin.defs#repoBlobRef") => {
318
+
let cid = input.subject.get("cid").and_then(|c| c.as_str());
319
+
if let Some(cid) = cid {
320
+
if let Some(takedown) = &input.takedown {
321
+
let takedown_ref = if takedown.apply {
322
+
takedown.r#ref.clone()
323
+
} else {
324
+
None
325
+
};
326
+
let _ = sqlx::query!(
327
+
"UPDATE blobs SET takedown_ref = $1 WHERE cid = $2",
328
+
takedown_ref,
329
+
cid
330
+
)
331
+
.execute(&state.db)
332
+
.await;
333
+
}
334
+
335
+
return (
336
+
StatusCode::OK,
337
+
Json(json!({
338
+
"subject": input.subject,
339
+
"takedown": input.takedown.as_ref().map(|t| json!({
340
+
"applied": t.apply,
341
+
"ref": t.r#ref
342
+
}))
343
+
})),
344
+
)
345
+
.into_response();
346
+
}
347
+
}
348
+
_ => {}
349
+
}
350
+
351
+
(
352
+
StatusCode::BAD_REQUEST,
353
+
Json(json!({"error": "InvalidRequest", "message": "Invalid subject type"})),
354
+
)
355
+
.into_response()
356
+
}
+393
src/api/server/account_status.rs
+393
src/api/server/account_status.rs
···
···
1
+
use crate::state::AppState;
2
+
use axum::{
3
+
Json,
4
+
extract::State,
5
+
http::StatusCode,
6
+
response::{IntoResponse, Response},
7
+
};
8
+
use chrono::{Duration, Utc};
9
+
use serde::{Deserialize, Serialize};
10
+
use serde_json::json;
11
+
use tracing::{error, info, warn};
12
+
use uuid::Uuid;
13
+
14
+
#[derive(Serialize)]
15
+
#[serde(rename_all = "camelCase")]
16
+
pub struct CheckAccountStatusOutput {
17
+
pub activated: bool,
18
+
pub valid_did: bool,
19
+
pub repo_commit: String,
20
+
pub repo_rev: String,
21
+
pub repo_blocks: i64,
22
+
pub indexed_records: i64,
23
+
pub private_state_values: i64,
24
+
pub expected_blobs: i64,
25
+
pub imported_blobs: i64,
26
+
}
27
+
28
+
pub async fn check_account_status(
29
+
State(state): State<AppState>,
30
+
headers: axum::http::HeaderMap,
31
+
) -> Response {
32
+
let auth_header = headers.get("Authorization");
33
+
if auth_header.is_none() {
34
+
return (
35
+
StatusCode::UNAUTHORIZED,
36
+
Json(json!({"error": "AuthenticationRequired"})),
37
+
)
38
+
.into_response();
39
+
}
40
+
41
+
let token = auth_header
42
+
.unwrap()
43
+
.to_str()
44
+
.unwrap_or("")
45
+
.replace("Bearer ", "");
46
+
47
+
let session = sqlx::query!(
48
+
r#"
49
+
SELECT s.did, k.key_bytes, u.id as user_id
50
+
FROM sessions s
51
+
JOIN users u ON s.did = u.did
52
+
JOIN user_keys k ON u.id = k.user_id
53
+
WHERE s.access_jwt = $1
54
+
"#,
55
+
token
56
+
)
57
+
.fetch_optional(&state.db)
58
+
.await;
59
+
60
+
let (did, key_bytes, user_id) = match session {
61
+
Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
62
+
Ok(None) => {
63
+
return (
64
+
StatusCode::UNAUTHORIZED,
65
+
Json(json!({"error": "AuthenticationFailed"})),
66
+
)
67
+
.into_response();
68
+
}
69
+
Err(e) => {
70
+
error!("DB error in check_account_status: {:?}", e);
71
+
return (
72
+
StatusCode::INTERNAL_SERVER_ERROR,
73
+
Json(json!({"error": "InternalError"})),
74
+
)
75
+
.into_response();
76
+
}
77
+
};
78
+
79
+
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
80
+
return (
81
+
StatusCode::UNAUTHORIZED,
82
+
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
83
+
)
84
+
.into_response();
85
+
}
86
+
87
+
let user_status = sqlx::query!("SELECT deactivated_at FROM users WHERE did = $1", did)
88
+
.fetch_optional(&state.db)
89
+
.await;
90
+
91
+
let deactivated_at = match user_status {
92
+
Ok(Some(row)) => row.deactivated_at,
93
+
_ => None,
94
+
};
95
+
96
+
let repo_result = sqlx::query!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id)
97
+
.fetch_optional(&state.db)
98
+
.await;
99
+
100
+
let repo_commit = match repo_result {
101
+
Ok(Some(row)) => row.repo_root_cid,
102
+
_ => String::new(),
103
+
};
104
+
105
+
let record_count: i64 = sqlx::query_scalar!("SELECT COUNT(*) FROM records WHERE repo_id = $1", user_id)
106
+
.fetch_one(&state.db)
107
+
.await
108
+
.unwrap_or(Some(0))
109
+
.unwrap_or(0);
110
+
111
+
let blob_count: i64 =
112
+
sqlx::query_scalar!("SELECT COUNT(*) FROM blobs WHERE created_by_user = $1", user_id)
113
+
.fetch_one(&state.db)
114
+
.await
115
+
.unwrap_or(Some(0))
116
+
.unwrap_or(0);
117
+
118
+
let valid_did = did.starts_with("did:");
119
+
120
+
(
121
+
StatusCode::OK,
122
+
Json(CheckAccountStatusOutput {
123
+
activated: deactivated_at.is_none(),
124
+
valid_did,
125
+
repo_commit: repo_commit.clone(),
126
+
repo_rev: chrono::Utc::now().timestamp_millis().to_string(),
127
+
repo_blocks: 0,
128
+
indexed_records: record_count,
129
+
private_state_values: 0,
130
+
expected_blobs: blob_count,
131
+
imported_blobs: blob_count,
132
+
}),
133
+
)
134
+
.into_response()
135
+
}
136
+
137
+
pub async fn activate_account(
138
+
State(state): State<AppState>,
139
+
headers: axum::http::HeaderMap,
140
+
) -> Response {
141
+
let auth_header = headers.get("Authorization");
142
+
if auth_header.is_none() {
143
+
return (
144
+
StatusCode::UNAUTHORIZED,
145
+
Json(json!({"error": "AuthenticationRequired"})),
146
+
)
147
+
.into_response();
148
+
}
149
+
150
+
let token = auth_header
151
+
.unwrap()
152
+
.to_str()
153
+
.unwrap_or("")
154
+
.replace("Bearer ", "");
155
+
156
+
let session = sqlx::query!(
157
+
r#"
158
+
SELECT s.did, k.key_bytes
159
+
FROM sessions s
160
+
JOIN users u ON s.did = u.did
161
+
JOIN user_keys k ON u.id = k.user_id
162
+
WHERE s.access_jwt = $1
163
+
"#,
164
+
token
165
+
)
166
+
.fetch_optional(&state.db)
167
+
.await;
168
+
169
+
let (did, key_bytes) = match session {
170
+
Ok(Some(row)) => (row.did, row.key_bytes),
171
+
Ok(None) => {
172
+
return (
173
+
StatusCode::UNAUTHORIZED,
174
+
Json(json!({"error": "AuthenticationFailed"})),
175
+
)
176
+
.into_response();
177
+
}
178
+
Err(e) => {
179
+
error!("DB error in activate_account: {:?}", e);
180
+
return (
181
+
StatusCode::INTERNAL_SERVER_ERROR,
182
+
Json(json!({"error": "InternalError"})),
183
+
)
184
+
.into_response();
185
+
}
186
+
};
187
+
188
+
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
189
+
return (
190
+
StatusCode::UNAUTHORIZED,
191
+
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
192
+
)
193
+
.into_response();
194
+
}
195
+
196
+
let result = sqlx::query!("UPDATE users SET deactivated_at = NULL WHERE did = $1", did)
197
+
.execute(&state.db)
198
+
.await;
199
+
200
+
match result {
201
+
Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(),
202
+
Err(e) => {
203
+
error!("DB error activating account: {:?}", e);
204
+
(
205
+
StatusCode::INTERNAL_SERVER_ERROR,
206
+
Json(json!({"error": "InternalError"})),
207
+
)
208
+
.into_response()
209
+
}
210
+
}
211
+
}
212
+
213
+
#[derive(Deserialize)]
214
+
#[serde(rename_all = "camelCase")]
215
+
pub struct DeactivateAccountInput {
216
+
pub delete_after: Option<String>,
217
+
}
218
+
219
+
pub async fn deactivate_account(
220
+
State(state): State<AppState>,
221
+
headers: axum::http::HeaderMap,
222
+
Json(_input): Json<DeactivateAccountInput>,
223
+
) -> Response {
224
+
let auth_header = headers.get("Authorization");
225
+
if auth_header.is_none() {
226
+
return (
227
+
StatusCode::UNAUTHORIZED,
228
+
Json(json!({"error": "AuthenticationRequired"})),
229
+
)
230
+
.into_response();
231
+
}
232
+
233
+
let token = auth_header
234
+
.unwrap()
235
+
.to_str()
236
+
.unwrap_or("")
237
+
.replace("Bearer ", "");
238
+
239
+
let session = sqlx::query!(
240
+
r#"
241
+
SELECT s.did, k.key_bytes
242
+
FROM sessions s
243
+
JOIN users u ON s.did = u.did
244
+
JOIN user_keys k ON u.id = k.user_id
245
+
WHERE s.access_jwt = $1
246
+
"#,
247
+
token
248
+
)
249
+
.fetch_optional(&state.db)
250
+
.await;
251
+
252
+
let (did, key_bytes) = match session {
253
+
Ok(Some(row)) => (row.did, row.key_bytes),
254
+
Ok(None) => {
255
+
return (
256
+
StatusCode::UNAUTHORIZED,
257
+
Json(json!({"error": "AuthenticationFailed"})),
258
+
)
259
+
.into_response();
260
+
}
261
+
Err(e) => {
262
+
error!("DB error in deactivate_account: {:?}", e);
263
+
return (
264
+
StatusCode::INTERNAL_SERVER_ERROR,
265
+
Json(json!({"error": "InternalError"})),
266
+
)
267
+
.into_response();
268
+
}
269
+
};
270
+
271
+
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
272
+
return (
273
+
StatusCode::UNAUTHORIZED,
274
+
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
275
+
)
276
+
.into_response();
277
+
}
278
+
279
+
let result = sqlx::query!("UPDATE users SET deactivated_at = NOW() WHERE did = $1", did)
280
+
.execute(&state.db)
281
+
.await;
282
+
283
+
match result {
284
+
Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(),
285
+
Err(e) => {
286
+
error!("DB error deactivating account: {:?}", e);
287
+
(
288
+
StatusCode::INTERNAL_SERVER_ERROR,
289
+
Json(json!({"error": "InternalError"})),
290
+
)
291
+
.into_response()
292
+
}
293
+
}
294
+
}
295
+
296
+
pub async fn request_account_delete(
297
+
State(state): State<AppState>,
298
+
headers: axum::http::HeaderMap,
299
+
) -> Response {
300
+
let auth_header = headers.get("Authorization");
301
+
if auth_header.is_none() {
302
+
return (
303
+
StatusCode::UNAUTHORIZED,
304
+
Json(json!({"error": "AuthenticationRequired"})),
305
+
)
306
+
.into_response();
307
+
}
308
+
309
+
let token = auth_header
310
+
.unwrap()
311
+
.to_str()
312
+
.unwrap_or("")
313
+
.replace("Bearer ", "");
314
+
315
+
let session = sqlx::query!(
316
+
r#"
317
+
SELECT s.did, u.id as user_id, u.email, u.handle, k.key_bytes
318
+
FROM sessions s
319
+
JOIN users u ON s.did = u.did
320
+
JOIN user_keys k ON u.id = k.user_id
321
+
WHERE s.access_jwt = $1
322
+
"#,
323
+
token
324
+
)
325
+
.fetch_optional(&state.db)
326
+
.await;
327
+
328
+
let (did, user_id, email, handle, key_bytes) = match session {
329
+
Ok(Some(row)) => (row.did, row.user_id, row.email, row.handle, row.key_bytes),
330
+
Ok(None) => {
331
+
return (
332
+
StatusCode::UNAUTHORIZED,
333
+
Json(json!({"error": "AuthenticationFailed"})),
334
+
)
335
+
.into_response();
336
+
}
337
+
Err(e) => {
338
+
error!("DB error in request_account_delete: {:?}", e);
339
+
return (
340
+
StatusCode::INTERNAL_SERVER_ERROR,
341
+
Json(json!({"error": "InternalError"})),
342
+
)
343
+
.into_response();
344
+
}
345
+
};
346
+
347
+
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
348
+
return (
349
+
StatusCode::UNAUTHORIZED,
350
+
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
351
+
)
352
+
.into_response();
353
+
}
354
+
355
+
let confirmation_token = Uuid::new_v4().to_string();
356
+
let expires_at = Utc::now() + Duration::minutes(15);
357
+
358
+
let insert = sqlx::query!(
359
+
"INSERT INTO account_deletion_requests (token, did, expires_at) VALUES ($1, $2, $3)",
360
+
confirmation_token,
361
+
did,
362
+
expires_at
363
+
)
364
+
.execute(&state.db)
365
+
.await;
366
+
367
+
if let Err(e) = insert {
368
+
error!("DB error creating deletion token: {:?}", e);
369
+
return (
370
+
StatusCode::INTERNAL_SERVER_ERROR,
371
+
Json(json!({"error": "InternalError"})),
372
+
)
373
+
.into_response();
374
+
}
375
+
376
+
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
377
+
if let Err(e) = crate::notifications::enqueue_account_deletion(
378
+
&state.db,
379
+
user_id,
380
+
&email,
381
+
&handle,
382
+
&confirmation_token,
383
+
&hostname,
384
+
)
385
+
.await
386
+
{
387
+
warn!("Failed to enqueue account deletion notification: {:?}", e);
388
+
}
389
+
390
+
info!("Account deletion requested for user {}", did);
391
+
392
+
(StatusCode::OK, Json(json!({}))).into_response()
393
+
}
+366
src/api/server/app_password.rs
+366
src/api/server/app_password.rs
···
···
1
+
use crate::state::AppState;
2
+
use axum::{
3
+
Json,
4
+
extract::State,
5
+
http::StatusCode,
6
+
response::{IntoResponse, Response},
7
+
};
8
+
use serde::{Deserialize, Serialize};
9
+
use serde_json::json;
10
+
use tracing::error;
11
+
12
+
#[derive(Serialize)]
13
+
#[serde(rename_all = "camelCase")]
14
+
pub struct AppPassword {
15
+
pub name: String,
16
+
pub created_at: String,
17
+
pub privileged: bool,
18
+
}
19
+
20
+
#[derive(Serialize)]
21
+
pub struct ListAppPasswordsOutput {
22
+
pub passwords: Vec<AppPassword>,
23
+
}
24
+
25
+
pub async fn list_app_passwords(
26
+
State(state): State<AppState>,
27
+
headers: axum::http::HeaderMap,
28
+
) -> Response {
29
+
let auth_header = headers.get("Authorization");
30
+
if auth_header.is_none() {
31
+
return (
32
+
StatusCode::UNAUTHORIZED,
33
+
Json(json!({"error": "AuthenticationRequired"})),
34
+
)
35
+
.into_response();
36
+
}
37
+
38
+
let token = auth_header
39
+
.unwrap()
40
+
.to_str()
41
+
.unwrap_or("")
42
+
.replace("Bearer ", "");
43
+
44
+
let session = sqlx::query!(
45
+
r#"
46
+
SELECT s.did, k.key_bytes, u.id as user_id
47
+
FROM sessions s
48
+
JOIN users u ON s.did = u.did
49
+
JOIN user_keys k ON u.id = k.user_id
50
+
WHERE s.access_jwt = $1
51
+
"#,
52
+
token
53
+
)
54
+
.fetch_optional(&state.db)
55
+
.await;
56
+
57
+
let (_did, key_bytes, user_id) = match session {
58
+
Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
59
+
Ok(None) => {
60
+
return (
61
+
StatusCode::UNAUTHORIZED,
62
+
Json(json!({"error": "AuthenticationFailed"})),
63
+
)
64
+
.into_response();
65
+
}
66
+
Err(e) => {
67
+
error!("DB error in list_app_passwords: {:?}", e);
68
+
return (
69
+
StatusCode::INTERNAL_SERVER_ERROR,
70
+
Json(json!({"error": "InternalError"})),
71
+
)
72
+
.into_response();
73
+
}
74
+
};
75
+
76
+
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
77
+
return (
78
+
StatusCode::UNAUTHORIZED,
79
+
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
80
+
)
81
+
.into_response();
82
+
}
83
+
84
+
let result = sqlx::query!("SELECT name, created_at, privileged FROM app_passwords WHERE user_id = $1 ORDER BY created_at DESC", user_id)
85
+
.fetch_all(&state.db)
86
+
.await;
87
+
88
+
match result {
89
+
Ok(rows) => {
90
+
let passwords: Vec<AppPassword> = rows
91
+
.iter()
92
+
.map(|row| {
93
+
AppPassword {
94
+
name: row.name.clone(),
95
+
created_at: row.created_at.to_rfc3339(),
96
+
privileged: row.privileged,
97
+
}
98
+
})
99
+
.collect();
100
+
101
+
(StatusCode::OK, Json(ListAppPasswordsOutput { passwords })).into_response()
102
+
}
103
+
Err(e) => {
104
+
error!("DB error listing app passwords: {:?}", e);
105
+
(
106
+
StatusCode::INTERNAL_SERVER_ERROR,
107
+
Json(json!({"error": "InternalError"})),
108
+
)
109
+
.into_response()
110
+
}
111
+
}
112
+
}
113
+
114
+
#[derive(Deserialize)]
115
+
pub struct CreateAppPasswordInput {
116
+
pub name: String,
117
+
pub privileged: Option<bool>,
118
+
}
119
+
120
+
#[derive(Serialize)]
121
+
#[serde(rename_all = "camelCase")]
122
+
pub struct CreateAppPasswordOutput {
123
+
pub name: String,
124
+
pub password: String,
125
+
pub created_at: String,
126
+
pub privileged: bool,
127
+
}
128
+
129
+
pub async fn create_app_password(
130
+
State(state): State<AppState>,
131
+
headers: axum::http::HeaderMap,
132
+
Json(input): Json<CreateAppPasswordInput>,
133
+
) -> Response {
134
+
let auth_header = headers.get("Authorization");
135
+
if auth_header.is_none() {
136
+
return (
137
+
StatusCode::UNAUTHORIZED,
138
+
Json(json!({"error": "AuthenticationRequired"})),
139
+
)
140
+
.into_response();
141
+
}
142
+
143
+
let token = auth_header
144
+
.unwrap()
145
+
.to_str()
146
+
.unwrap_or("")
147
+
.replace("Bearer ", "");
148
+
149
+
let session = sqlx::query!(
150
+
r#"
151
+
SELECT s.did, k.key_bytes, u.id as user_id
152
+
FROM sessions s
153
+
JOIN users u ON s.did = u.did
154
+
JOIN user_keys k ON u.id = k.user_id
155
+
WHERE s.access_jwt = $1
156
+
"#,
157
+
token
158
+
)
159
+
.fetch_optional(&state.db)
160
+
.await;
161
+
162
+
let (_did, key_bytes, user_id) = match session {
163
+
Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
164
+
Ok(None) => {
165
+
return (
166
+
StatusCode::UNAUTHORIZED,
167
+
Json(json!({"error": "AuthenticationFailed"})),
168
+
)
169
+
.into_response();
170
+
}
171
+
Err(e) => {
172
+
error!("DB error in create_app_password: {:?}", e);
173
+
return (
174
+
StatusCode::INTERNAL_SERVER_ERROR,
175
+
Json(json!({"error": "InternalError"})),
176
+
)
177
+
.into_response();
178
+
}
179
+
};
180
+
181
+
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
182
+
return (
183
+
StatusCode::UNAUTHORIZED,
184
+
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
185
+
)
186
+
.into_response();
187
+
}
188
+
189
+
let name = input.name.trim();
190
+
if name.is_empty() {
191
+
return (
192
+
StatusCode::BAD_REQUEST,
193
+
Json(json!({"error": "InvalidRequest", "message": "name is required"})),
194
+
)
195
+
.into_response();
196
+
}
197
+
198
+
let existing = sqlx::query!("SELECT id FROM app_passwords WHERE user_id = $1 AND name = $2", user_id, name)
199
+
.fetch_optional(&state.db)
200
+
.await;
201
+
202
+
if let Ok(Some(_)) = existing {
203
+
return (
204
+
StatusCode::BAD_REQUEST,
205
+
Json(json!({"error": "DuplicateAppPassword", "message": "App password with this name already exists"})),
206
+
)
207
+
.into_response();
208
+
}
209
+
210
+
let password: String = (0..4)
211
+
.map(|_| {
212
+
use rand::Rng;
213
+
let mut rng = rand::thread_rng();
214
+
let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz234567".chars().collect();
215
+
(0..4).map(|_| chars[rng.gen_range(0..chars.len())]).collect::<String>()
216
+
})
217
+
.collect::<Vec<String>>()
218
+
.join("-");
219
+
220
+
let password_hash = match bcrypt::hash(&password, bcrypt::DEFAULT_COST) {
221
+
Ok(h) => h,
222
+
Err(e) => {
223
+
error!("Failed to hash password: {:?}", e);
224
+
return (
225
+
StatusCode::INTERNAL_SERVER_ERROR,
226
+
Json(json!({"error": "InternalError"})),
227
+
)
228
+
.into_response();
229
+
}
230
+
};
231
+
232
+
let privileged = input.privileged.unwrap_or(false);
233
+
let created_at = chrono::Utc::now();
234
+
235
+
let result = sqlx::query!(
236
+
"INSERT INTO app_passwords (user_id, name, password_hash, created_at, privileged) VALUES ($1, $2, $3, $4, $5)",
237
+
user_id,
238
+
name,
239
+
password_hash,
240
+
created_at,
241
+
privileged
242
+
)
243
+
.execute(&state.db)
244
+
.await;
245
+
246
+
match result {
247
+
Ok(_) => (
248
+
StatusCode::OK,
249
+
Json(CreateAppPasswordOutput {
250
+
name: name.to_string(),
251
+
password,
252
+
created_at: created_at.to_rfc3339(),
253
+
privileged,
254
+
}),
255
+
)
256
+
.into_response(),
257
+
Err(e) => {
258
+
error!("DB error creating app password: {:?}", e);
259
+
(
260
+
StatusCode::INTERNAL_SERVER_ERROR,
261
+
Json(json!({"error": "InternalError"})),
262
+
)
263
+
.into_response()
264
+
}
265
+
}
266
+
}
267
+
268
+
#[derive(Deserialize)]
269
+
pub struct RevokeAppPasswordInput {
270
+
pub name: String,
271
+
}
272
+
273
+
pub async fn revoke_app_password(
274
+
State(state): State<AppState>,
275
+
headers: axum::http::HeaderMap,
276
+
Json(input): Json<RevokeAppPasswordInput>,
277
+
) -> Response {
278
+
let auth_header = headers.get("Authorization");
279
+
if auth_header.is_none() {
280
+
return (
281
+
StatusCode::UNAUTHORIZED,
282
+
Json(json!({"error": "AuthenticationRequired"})),
283
+
)
284
+
.into_response();
285
+
}
286
+
287
+
let token = auth_header
288
+
.unwrap()
289
+
.to_str()
290
+
.unwrap_or("")
291
+
.replace("Bearer ", "");
292
+
293
+
let session = sqlx::query!(
294
+
r#"
295
+
SELECT s.did, k.key_bytes, u.id as user_id
296
+
FROM sessions s
297
+
JOIN users u ON s.did = u.did
298
+
JOIN user_keys k ON u.id = k.user_id
299
+
WHERE s.access_jwt = $1
300
+
"#,
301
+
token
302
+
)
303
+
.fetch_optional(&state.db)
304
+
.await;
305
+
306
+
let (_did, key_bytes, user_id) = match session {
307
+
Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
308
+
Ok(None) => {
309
+
return (
310
+
StatusCode::UNAUTHORIZED,
311
+
Json(json!({"error": "AuthenticationFailed"})),
312
+
)
313
+
.into_response();
314
+
}
315
+
Err(e) => {
316
+
error!("DB error in revoke_app_password: {:?}", e);
317
+
return (
318
+
StatusCode::INTERNAL_SERVER_ERROR,
319
+
Json(json!({"error": "InternalError"})),
320
+
)
321
+
.into_response();
322
+
}
323
+
};
324
+
325
+
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
326
+
return (
327
+
StatusCode::UNAUTHORIZED,
328
+
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
329
+
)
330
+
.into_response();
331
+
}
332
+
333
+
let name = input.name.trim();
334
+
if name.is_empty() {
335
+
return (
336
+
StatusCode::BAD_REQUEST,
337
+
Json(json!({"error": "InvalidRequest", "message": "name is required"})),
338
+
)
339
+
.into_response();
340
+
}
341
+
342
+
let result = sqlx::query!("DELETE FROM app_passwords WHERE user_id = $1 AND name = $2", user_id, name)
343
+
.execute(&state.db)
344
+
.await;
345
+
346
+
match result {
347
+
Ok(r) => {
348
+
if r.rows_affected() == 0 {
349
+
return (
350
+
StatusCode::NOT_FOUND,
351
+
Json(json!({"error": "AppPasswordNotFound", "message": "App password not found"})),
352
+
)
353
+
.into_response();
354
+
}
355
+
(StatusCode::OK, Json(json!({}))).into_response()
356
+
}
357
+
Err(e) => {
358
+
error!("DB error revoking app password: {:?}", e);
359
+
(
360
+
StatusCode::INTERNAL_SERVER_ERROR,
361
+
Json(json!({"error": "InternalError"})),
362
+
)
363
+
.into_response()
364
+
}
365
+
}
366
+
}
+288
src/api/server/email.rs
+288
src/api/server/email.rs
···
···
1
+
use crate::state::AppState;
2
+
use axum::{
3
+
Json,
4
+
extract::State,
5
+
http::StatusCode,
6
+
response::{IntoResponse, Response},
7
+
};
8
+
use chrono::{Duration, Utc};
9
+
use rand::Rng;
10
+
use serde::Deserialize;
11
+
use serde_json::json;
12
+
use tracing::{error, info, warn};
13
+
14
+
fn generate_confirmation_code() -> String {
15
+
let mut rng = rand::thread_rng();
16
+
let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz234567".chars().collect();
17
+
let part1: String = (0..5).map(|_| chars[rng.gen_range(0..chars.len())]).collect();
18
+
let part2: String = (0..5).map(|_| chars[rng.gen_range(0..chars.len())]).collect();
19
+
format!("{}-{}", part1, part2)
20
+
}
21
+
22
+
#[derive(Deserialize)]
23
+
#[serde(rename_all = "camelCase")]
24
+
pub struct RequestEmailUpdateInput {
25
+
pub email: String,
26
+
}
27
+
28
+
pub async fn request_email_update(
29
+
State(state): State<AppState>,
30
+
headers: axum::http::HeaderMap,
31
+
Json(input): Json<RequestEmailUpdateInput>,
32
+
) -> Response {
33
+
let auth_header = headers.get("Authorization");
34
+
if auth_header.is_none() {
35
+
return (
36
+
StatusCode::UNAUTHORIZED,
37
+
Json(json!({"error": "AuthenticationRequired"})),
38
+
)
39
+
.into_response();
40
+
}
41
+
42
+
let token = auth_header
43
+
.unwrap()
44
+
.to_str()
45
+
.unwrap_or("")
46
+
.replace("Bearer ", "");
47
+
48
+
let session = sqlx::query!(
49
+
r#"
50
+
SELECT s.did, k.key_bytes, u.id as user_id, u.handle
51
+
FROM sessions s
52
+
JOIN users u ON s.did = u.did
53
+
JOIN user_keys k ON u.id = k.user_id
54
+
WHERE s.access_jwt = $1
55
+
"#,
56
+
token
57
+
)
58
+
.fetch_optional(&state.db)
59
+
.await;
60
+
61
+
let (_did, key_bytes, user_id, handle) = match session {
62
+
Ok(Some(row)) => (row.did, row.key_bytes, row.user_id, row.handle),
63
+
Ok(None) => {
64
+
return (
65
+
StatusCode::UNAUTHORIZED,
66
+
Json(json!({"error": "AuthenticationFailed"})),
67
+
)
68
+
.into_response();
69
+
}
70
+
Err(e) => {
71
+
error!("DB error in request_email_update: {:?}", e);
72
+
return (
73
+
StatusCode::INTERNAL_SERVER_ERROR,
74
+
Json(json!({"error": "InternalError"})),
75
+
)
76
+
.into_response();
77
+
}
78
+
};
79
+
80
+
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
81
+
return (
82
+
StatusCode::UNAUTHORIZED,
83
+
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
84
+
)
85
+
.into_response();
86
+
}
87
+
88
+
let email = input.email.trim().to_lowercase();
89
+
if email.is_empty() {
90
+
return (
91
+
StatusCode::BAD_REQUEST,
92
+
Json(json!({"error": "InvalidRequest", "message": "email is required"})),
93
+
)
94
+
.into_response();
95
+
}
96
+
97
+
let exists = sqlx::query!("SELECT 1 as one FROM users WHERE LOWER(email) = $1", email)
98
+
.fetch_optional(&state.db)
99
+
.await;
100
+
101
+
if let Ok(Some(_)) = exists {
102
+
return (
103
+
StatusCode::BAD_REQUEST,
104
+
Json(json!({"error": "EmailTaken", "message": "Email already taken"})),
105
+
)
106
+
.into_response();
107
+
}
108
+
109
+
let code = generate_confirmation_code();
110
+
let expires_at = Utc::now() + Duration::minutes(10);
111
+
112
+
let update = sqlx::query!(
113
+
"UPDATE users SET email_pending_verification = $1, email_confirmation_code = $2, email_confirmation_code_expires_at = $3 WHERE id = $4",
114
+
email,
115
+
code,
116
+
expires_at,
117
+
user_id
118
+
)
119
+
.execute(&state.db)
120
+
.await;
121
+
122
+
if let Err(e) = update {
123
+
error!("DB error setting email update code: {:?}", e);
124
+
return (
125
+
StatusCode::INTERNAL_SERVER_ERROR,
126
+
Json(json!({"error": "InternalError"})),
127
+
)
128
+
.into_response();
129
+
}
130
+
131
+
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
132
+
if let Err(e) = crate::notifications::enqueue_email_update(
133
+
&state.db,
134
+
user_id,
135
+
&email,
136
+
&handle,
137
+
&code,
138
+
&hostname,
139
+
)
140
+
.await
141
+
{
142
+
warn!("Failed to enqueue email update notification: {:?}", e);
143
+
}
144
+
145
+
info!("Email update requested for user {}", user_id);
146
+
147
+
(StatusCode::OK, Json(json!({ "tokenRequired": true }))).into_response()
148
+
}
149
+
150
+
#[derive(Deserialize)]
151
+
#[serde(rename_all = "camelCase")]
152
+
pub struct ConfirmEmailInput {
153
+
pub email: String,
154
+
pub token: String,
155
+
}
156
+
157
+
pub async fn confirm_email(
158
+
State(state): State<AppState>,
159
+
headers: axum::http::HeaderMap,
160
+
Json(input): Json<ConfirmEmailInput>,
161
+
) -> Response {
162
+
let auth_header = headers.get("Authorization");
163
+
if auth_header.is_none() {
164
+
return (
165
+
StatusCode::UNAUTHORIZED,
166
+
Json(json!({"error": "AuthenticationRequired"})),
167
+
)
168
+
.into_response();
169
+
}
170
+
171
+
let token = auth_header
172
+
.unwrap()
173
+
.to_str()
174
+
.unwrap_or("")
175
+
.replace("Bearer ", "");
176
+
177
+
let session = sqlx::query!(
178
+
r#"
179
+
SELECT s.did, k.key_bytes, u.id as user_id, u.email_confirmation_code, u.email_confirmation_code_expires_at, u.email_pending_verification
180
+
FROM sessions s
181
+
JOIN users u ON s.did = u.did
182
+
JOIN user_keys k ON u.id = k.user_id
183
+
WHERE s.access_jwt = $1
184
+
"#,
185
+
token
186
+
)
187
+
.fetch_optional(&state.db)
188
+
.await;
189
+
190
+
let (_did, key_bytes, user_id, stored_code, expires_at, email_pending_verification) = match session {
191
+
Ok(Some(row)) => (
192
+
row.did,
193
+
row.key_bytes,
194
+
row.user_id,
195
+
row.email_confirmation_code,
196
+
row.email_confirmation_code_expires_at,
197
+
row.email_pending_verification,
198
+
),
199
+
Ok(None) => {
200
+
return (
201
+
StatusCode::UNAUTHORIZED,
202
+
Json(json!({"error": "AuthenticationFailed"})),
203
+
)
204
+
.into_response();
205
+
}
206
+
Err(e) => {
207
+
error!("DB error in confirm_email: {:?}", e);
208
+
return (
209
+
StatusCode::INTERNAL_SERVER_ERROR,
210
+
Json(json!({"error": "InternalError"})),
211
+
)
212
+
.into_response();
213
+
}
214
+
};
215
+
216
+
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
217
+
return (
218
+
StatusCode::UNAUTHORIZED,
219
+
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
220
+
)
221
+
.into_response();
222
+
}
223
+
224
+
let email = input.email.trim().to_lowercase();
225
+
let confirmation_code = input.token.trim();
226
+
227
+
if email_pending_verification.is_none() || stored_code.is_none() || expires_at.is_none() {
228
+
return (
229
+
StatusCode::BAD_REQUEST,
230
+
Json(json!({"error": "InvalidRequest", "message": "No pending email update found"})),
231
+
)
232
+
.into_response();
233
+
}
234
+
235
+
let email_pending_verification = email_pending_verification.unwrap();
236
+
if email_pending_verification != email {
237
+
return (
238
+
StatusCode::BAD_REQUEST,
239
+
Json(json!({"error": "InvalidRequest", "message": "Email does not match pending update"})),
240
+
)
241
+
.into_response();
242
+
}
243
+
244
+
if stored_code.unwrap() != confirmation_code {
245
+
return (
246
+
StatusCode::BAD_REQUEST,
247
+
Json(json!({"error": "InvalidToken", "message": "Invalid token"})),
248
+
)
249
+
.into_response();
250
+
}
251
+
252
+
if Utc::now() > expires_at.unwrap() {
253
+
return (
254
+
StatusCode::BAD_REQUEST,
255
+
Json(json!({"error": "ExpiredToken", "message": "Token has expired"})),
256
+
)
257
+
.into_response();
258
+
}
259
+
260
+
let update = sqlx::query!(
261
+
"UPDATE users SET email = $1, email_pending_verification = NULL, email_confirmation_code = NULL, email_confirmation_code_expires_at = NULL WHERE id = $2",
262
+
email_pending_verification,
263
+
user_id
264
+
)
265
+
.execute(&state.db)
266
+
.await;
267
+
268
+
if let Err(e) = update {
269
+
error!("DB error finalizing email update: {:?}", e);
270
+
if e.as_database_error().map(|db_err| db_err.is_unique_violation()).unwrap_or(false) {
271
+
return (
272
+
StatusCode::BAD_REQUEST,
273
+
Json(json!({"error": "EmailTaken", "message": "Email already taken"})),
274
+
)
275
+
.into_response();
276
+
}
277
+
278
+
return (
279
+
StatusCode::INTERNAL_SERVER_ERROR,
280
+
Json(json!({"error": "InternalError"})),
281
+
)
282
+
.into_response();
283
+
}
284
+
285
+
info!("Email updated for user {}", user_id);
286
+
287
+
(StatusCode::OK, Json(json!({}))).into_response()
288
+
}
+11
-4
src/api/server/mod.rs
+11
-4
src/api/server/mod.rs
···
1
pub mod invite;
2
pub mod meta;
3
pub mod session;
4
5
pub use invite::{create_invite_code, create_invite_codes, get_account_invite_codes};
6
pub use meta::{describe_server, health};
7
pub use session::{
8
-
activate_account, check_account_status, confirm_email, create_app_password, create_session,
9
-
deactivate_account, delete_session, get_service_auth, get_session, list_app_passwords,
10
-
refresh_session, request_account_delete, request_email_update, request_password_reset,
11
-
reset_password, revoke_app_password,
12
};
···
1
+
pub mod account_status;
2
+
pub mod app_password;
3
+
pub mod email;
4
pub mod invite;
5
pub mod meta;
6
+
pub mod password;
7
pub mod session;
8
9
+
pub use account_status::{
10
+
activate_account, check_account_status, deactivate_account, request_account_delete,
11
+
};
12
+
pub use app_password::{create_app_password, list_app_passwords, revoke_app_password};
13
+
pub use email::{confirm_email, request_email_update};
14
pub use invite::{create_invite_code, create_invite_codes, get_account_invite_codes};
15
pub use meta::{describe_server, health};
16
+
pub use password::{request_password_reset, reset_password};
17
pub use session::{
18
+
create_session, delete_session, get_service_auth, get_session, refresh_session,
19
};
+221
src/api/server/password.rs
+221
src/api/server/password.rs
···
···
1
+
use crate::state::AppState;
2
+
use axum::{
3
+
Json,
4
+
extract::State,
5
+
http::StatusCode,
6
+
response::{IntoResponse, Response},
7
+
};
8
+
use bcrypt::{hash, DEFAULT_COST};
9
+
use chrono::{Duration, Utc};
10
+
use rand::Rng;
11
+
use serde::Deserialize;
12
+
use serde_json::json;
13
+
use tracing::{error, info, warn};
14
+
15
+
fn generate_reset_code() -> String {
16
+
let mut rng = rand::thread_rng();
17
+
let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz234567".chars().collect();
18
+
let part1: String = (0..5).map(|_| chars[rng.gen_range(0..chars.len())]).collect();
19
+
let part2: String = (0..5).map(|_| chars[rng.gen_range(0..chars.len())]).collect();
20
+
format!("{}-{}", part1, part2)
21
+
}
22
+
23
+
#[derive(Deserialize)]
24
+
pub struct RequestPasswordResetInput {
25
+
pub email: String,
26
+
}
27
+
28
+
pub async fn request_password_reset(
29
+
State(state): State<AppState>,
30
+
Json(input): Json<RequestPasswordResetInput>,
31
+
) -> Response {
32
+
let email = input.email.trim().to_lowercase();
33
+
if email.is_empty() {
34
+
return (
35
+
StatusCode::BAD_REQUEST,
36
+
Json(json!({"error": "InvalidRequest", "message": "email is required"})),
37
+
)
38
+
.into_response();
39
+
}
40
+
41
+
let user = sqlx::query!(
42
+
"SELECT id, handle FROM users WHERE LOWER(email) = $1",
43
+
email
44
+
)
45
+
.fetch_optional(&state.db)
46
+
.await;
47
+
48
+
let (user_id, handle) = match user {
49
+
Ok(Some(row)) => (row.id, row.handle),
50
+
Ok(None) => {
51
+
info!("Password reset requested for unknown email: {}", email);
52
+
return (StatusCode::OK, Json(json!({}))).into_response();
53
+
}
54
+
Err(e) => {
55
+
error!("DB error in request_password_reset: {:?}", e);
56
+
return (
57
+
StatusCode::INTERNAL_SERVER_ERROR,
58
+
Json(json!({"error": "InternalError"})),
59
+
)
60
+
.into_response();
61
+
}
62
+
};
63
+
64
+
let code = generate_reset_code();
65
+
let expires_at = Utc::now() + Duration::minutes(10);
66
+
67
+
let update = sqlx::query!(
68
+
"UPDATE users SET password_reset_code = $1, password_reset_code_expires_at = $2 WHERE id = $3",
69
+
code,
70
+
expires_at,
71
+
user_id
72
+
)
73
+
.execute(&state.db)
74
+
.await;
75
+
76
+
if let Err(e) = update {
77
+
error!("DB error setting reset code: {:?}", e);
78
+
return (
79
+
StatusCode::INTERNAL_SERVER_ERROR,
80
+
Json(json!({"error": "InternalError"})),
81
+
)
82
+
.into_response();
83
+
}
84
+
85
+
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
86
+
if let Err(e) = crate::notifications::enqueue_password_reset(
87
+
&state.db,
88
+
user_id,
89
+
&email,
90
+
&handle,
91
+
&code,
92
+
&hostname,
93
+
)
94
+
.await
95
+
{
96
+
warn!("Failed to enqueue password reset notification: {:?}", e);
97
+
}
98
+
99
+
info!("Password reset requested for user {}", user_id);
100
+
101
+
(StatusCode::OK, Json(json!({}))).into_response()
102
+
}
103
+
104
+
#[derive(Deserialize)]
105
+
pub struct ResetPasswordInput {
106
+
pub token: String,
107
+
pub password: String,
108
+
}
109
+
110
+
pub async fn reset_password(
111
+
State(state): State<AppState>,
112
+
Json(input): Json<ResetPasswordInput>,
113
+
) -> Response {
114
+
let token = input.token.trim();
115
+
let password = &input.password;
116
+
117
+
if token.is_empty() {
118
+
return (
119
+
StatusCode::BAD_REQUEST,
120
+
Json(json!({"error": "InvalidToken", "message": "token is required"})),
121
+
)
122
+
.into_response();
123
+
}
124
+
125
+
if password.is_empty() {
126
+
return (
127
+
StatusCode::BAD_REQUEST,
128
+
Json(json!({"error": "InvalidRequest", "message": "password is required"})),
129
+
)
130
+
.into_response();
131
+
}
132
+
133
+
let user = sqlx::query!(
134
+
"SELECT id, password_reset_code, password_reset_code_expires_at FROM users WHERE password_reset_code = $1",
135
+
token
136
+
)
137
+
.fetch_optional(&state.db)
138
+
.await;
139
+
140
+
let (user_id, expires_at) = match user {
141
+
Ok(Some(row)) => {
142
+
let expires = row.password_reset_code_expires_at;
143
+
(row.id, expires)
144
+
}
145
+
Ok(None) => {
146
+
return (
147
+
StatusCode::BAD_REQUEST,
148
+
Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})),
149
+
)
150
+
.into_response();
151
+
}
152
+
Err(e) => {
153
+
error!("DB error in reset_password: {:?}", e);
154
+
return (
155
+
StatusCode::INTERNAL_SERVER_ERROR,
156
+
Json(json!({"error": "InternalError"})),
157
+
)
158
+
.into_response();
159
+
}
160
+
};
161
+
162
+
if let Some(exp) = expires_at {
163
+
if Utc::now() > exp {
164
+
let _ = sqlx::query!(
165
+
"UPDATE users SET password_reset_code = NULL, password_reset_code_expires_at = NULL WHERE id = $1",
166
+
user_id
167
+
)
168
+
.execute(&state.db)
169
+
.await;
170
+
171
+
return (
172
+
StatusCode::BAD_REQUEST,
173
+
Json(json!({"error": "ExpiredToken", "message": "Token has expired"})),
174
+
)
175
+
.into_response();
176
+
}
177
+
} else {
178
+
return (
179
+
StatusCode::BAD_REQUEST,
180
+
Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})),
181
+
)
182
+
.into_response();
183
+
}
184
+
185
+
let password_hash = match hash(password, DEFAULT_COST) {
186
+
Ok(h) => h,
187
+
Err(e) => {
188
+
error!("Failed to hash password: {:?}", e);
189
+
return (
190
+
StatusCode::INTERNAL_SERVER_ERROR,
191
+
Json(json!({"error": "InternalError"})),
192
+
)
193
+
.into_response();
194
+
}
195
+
};
196
+
197
+
let update = sqlx::query!(
198
+
"UPDATE users SET password_hash = $1, password_reset_code = NULL, password_reset_code_expires_at = NULL WHERE id = $2",
199
+
password_hash,
200
+
user_id
201
+
)
202
+
.execute(&state.db)
203
+
.await;
204
+
205
+
if let Err(e) = update {
206
+
error!("DB error updating password: {:?}", e);
207
+
return (
208
+
StatusCode::INTERNAL_SERVER_ERROR,
209
+
Json(json!({"error": "InternalError"})),
210
+
)
211
+
.into_response();
212
+
}
213
+
214
+
let _ = sqlx::query!("DELETE FROM sessions WHERE did = (SELECT did FROM users WHERE id = $1)", user_id)
215
+
.execute(&state.db)
216
+
.await;
217
+
218
+
info!("Password reset completed for user {}", user_id);
219
+
220
+
(StatusCode::OK, Json(json!({}))).into_response()
221
+
}
+1
-1217
src/api/server/session.rs
+1
-1217
src/api/server/session.rs
···
5
http::StatusCode,
6
response::{IntoResponse, Response},
7
};
8
-
use bcrypt::{hash, verify, DEFAULT_COST};
9
-
use chrono::{Duration, Utc};
10
-
use rand::Rng;
11
use serde::{Deserialize, Serialize};
12
use serde_json::json;
13
use tracing::{error, info, warn};
14
-
use uuid::Uuid;
15
16
#[derive(Deserialize)]
17
pub struct GetServiceAuthParams {
···
343
.into_response()
344
}
345
346
-
pub async fn request_account_delete(
347
-
State(state): State<AppState>,
348
-
headers: axum::http::HeaderMap,
349
-
) -> Response {
350
-
let auth_header = headers.get("Authorization");
351
-
if auth_header.is_none() {
352
-
return (
353
-
StatusCode::UNAUTHORIZED,
354
-
Json(json!({"error": "AuthenticationRequired"})),
355
-
)
356
-
.into_response();
357
-
}
358
-
359
-
let token = auth_header
360
-
.unwrap()
361
-
.to_str()
362
-
.unwrap_or("")
363
-
.replace("Bearer ", "");
364
-
365
-
let session = sqlx::query!(
366
-
r#"
367
-
SELECT s.did, u.id as user_id, u.email, u.handle, k.key_bytes
368
-
FROM sessions s
369
-
JOIN users u ON s.did = u.did
370
-
JOIN user_keys k ON u.id = k.user_id
371
-
WHERE s.access_jwt = $1
372
-
"#,
373
-
token
374
-
)
375
-
.fetch_optional(&state.db)
376
-
.await;
377
-
378
-
let (did, user_id, email, handle, key_bytes) = match session {
379
-
Ok(Some(row)) => (row.did, row.user_id, row.email, row.handle, row.key_bytes),
380
-
Ok(None) => {
381
-
return (
382
-
StatusCode::UNAUTHORIZED,
383
-
Json(json!({"error": "AuthenticationFailed"})),
384
-
)
385
-
.into_response();
386
-
}
387
-
Err(e) => {
388
-
error!("DB error in request_account_delete: {:?}", e);
389
-
return (
390
-
StatusCode::INTERNAL_SERVER_ERROR,
391
-
Json(json!({"error": "InternalError"})),
392
-
)
393
-
.into_response();
394
-
}
395
-
};
396
-
397
-
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
398
-
return (
399
-
StatusCode::UNAUTHORIZED,
400
-
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
401
-
)
402
-
.into_response();
403
-
}
404
-
405
-
let confirmation_token = Uuid::new_v4().to_string();
406
-
let expires_at = Utc::now() + Duration::minutes(15);
407
-
408
-
let insert = sqlx::query!(
409
-
"INSERT INTO account_deletion_requests (token, did, expires_at) VALUES ($1, $2, $3)",
410
-
confirmation_token,
411
-
did,
412
-
expires_at
413
-
)
414
-
.execute(&state.db)
415
-
.await;
416
-
417
-
if let Err(e) = insert {
418
-
error!("DB error creating deletion token: {:?}", e);
419
-
return (
420
-
StatusCode::INTERNAL_SERVER_ERROR,
421
-
Json(json!({"error": "InternalError"})),
422
-
)
423
-
.into_response();
424
-
}
425
-
426
-
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
427
-
if let Err(e) = crate::notifications::enqueue_account_deletion(
428
-
&state.db,
429
-
user_id,
430
-
&email,
431
-
&handle,
432
-
&confirmation_token,
433
-
&hostname,
434
-
)
435
-
.await
436
-
{
437
-
warn!("Failed to enqueue account deletion notification: {:?}", e);
438
-
}
439
-
440
-
info!("Account deletion requested for user {}", did);
441
-
442
-
(StatusCode::OK, Json(json!({}))).into_response()
443
-
}
444
-
445
pub async fn refresh_session(
446
State(state): State<AppState>,
447
headers: axum::http::HeaderMap,
···
573
}
574
}
575
}
576
-
577
-
#[derive(Serialize)]
578
-
#[serde(rename_all = "camelCase")]
579
-
pub struct CheckAccountStatusOutput {
580
-
pub activated: bool,
581
-
pub valid_did: bool,
582
-
pub repo_commit: String,
583
-
pub repo_rev: String,
584
-
pub repo_blocks: i64,
585
-
pub indexed_records: i64,
586
-
pub private_state_values: i64,
587
-
pub expected_blobs: i64,
588
-
pub imported_blobs: i64,
589
-
}
590
-
591
-
pub async fn check_account_status(
592
-
State(state): State<AppState>,
593
-
headers: axum::http::HeaderMap,
594
-
) -> Response {
595
-
let auth_header = headers.get("Authorization");
596
-
if auth_header.is_none() {
597
-
return (
598
-
StatusCode::UNAUTHORIZED,
599
-
Json(json!({"error": "AuthenticationRequired"})),
600
-
)
601
-
.into_response();
602
-
}
603
-
604
-
let token = auth_header
605
-
.unwrap()
606
-
.to_str()
607
-
.unwrap_or("")
608
-
.replace("Bearer ", "");
609
-
610
-
let session = sqlx::query!(
611
-
r#"
612
-
SELECT s.did, k.key_bytes, u.id as user_id
613
-
FROM sessions s
614
-
JOIN users u ON s.did = u.did
615
-
JOIN user_keys k ON u.id = k.user_id
616
-
WHERE s.access_jwt = $1
617
-
"#,
618
-
token
619
-
)
620
-
.fetch_optional(&state.db)
621
-
.await;
622
-
623
-
let (did, key_bytes, user_id) = match session {
624
-
Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
625
-
Ok(None) => {
626
-
return (
627
-
StatusCode::UNAUTHORIZED,
628
-
Json(json!({"error": "AuthenticationFailed"})),
629
-
)
630
-
.into_response();
631
-
}
632
-
Err(e) => {
633
-
error!("DB error in check_account_status: {:?}", e);
634
-
return (
635
-
StatusCode::INTERNAL_SERVER_ERROR,
636
-
Json(json!({"error": "InternalError"})),
637
-
)
638
-
.into_response();
639
-
}
640
-
};
641
-
642
-
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
643
-
return (
644
-
StatusCode::UNAUTHORIZED,
645
-
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
646
-
)
647
-
.into_response();
648
-
}
649
-
650
-
let user_status = sqlx::query!("SELECT deactivated_at FROM users WHERE did = $1", did)
651
-
.fetch_optional(&state.db)
652
-
.await;
653
-
654
-
let deactivated_at = match user_status {
655
-
Ok(Some(row)) => row.deactivated_at,
656
-
_ => None,
657
-
};
658
-
659
-
let repo_result = sqlx::query!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id)
660
-
.fetch_optional(&state.db)
661
-
.await;
662
-
663
-
let repo_commit = match repo_result {
664
-
Ok(Some(row)) => row.repo_root_cid,
665
-
_ => String::new(),
666
-
};
667
-
668
-
let record_count: i64 = sqlx::query_scalar!("SELECT COUNT(*) FROM records WHERE repo_id = $1", user_id)
669
-
.fetch_one(&state.db)
670
-
.await
671
-
.unwrap_or(Some(0))
672
-
.unwrap_or(0);
673
-
674
-
let blob_count: i64 =
675
-
sqlx::query_scalar!("SELECT COUNT(*) FROM blobs WHERE created_by_user = $1", user_id)
676
-
.fetch_one(&state.db)
677
-
.await
678
-
.unwrap_or(Some(0))
679
-
.unwrap_or(0);
680
-
681
-
let valid_did = did.starts_with("did:");
682
-
683
-
(
684
-
StatusCode::OK,
685
-
Json(CheckAccountStatusOutput {
686
-
activated: deactivated_at.is_none(),
687
-
valid_did,
688
-
repo_commit: repo_commit.clone(),
689
-
repo_rev: chrono::Utc::now().timestamp_millis().to_string(),
690
-
repo_blocks: 0,
691
-
indexed_records: record_count,
692
-
private_state_values: 0,
693
-
expected_blobs: blob_count,
694
-
imported_blobs: blob_count,
695
-
}),
696
-
)
697
-
.into_response()
698
-
}
699
-
700
-
pub async fn activate_account(
701
-
State(state): State<AppState>,
702
-
headers: axum::http::HeaderMap,
703
-
) -> Response {
704
-
let auth_header = headers.get("Authorization");
705
-
if auth_header.is_none() {
706
-
return (
707
-
StatusCode::UNAUTHORIZED,
708
-
Json(json!({"error": "AuthenticationRequired"})),
709
-
)
710
-
.into_response();
711
-
}
712
-
713
-
let token = auth_header
714
-
.unwrap()
715
-
.to_str()
716
-
.unwrap_or("")
717
-
.replace("Bearer ", "");
718
-
719
-
let session = sqlx::query!(
720
-
r#"
721
-
SELECT s.did, k.key_bytes
722
-
FROM sessions s
723
-
JOIN users u ON s.did = u.did
724
-
JOIN user_keys k ON u.id = k.user_id
725
-
WHERE s.access_jwt = $1
726
-
"#,
727
-
token
728
-
)
729
-
.fetch_optional(&state.db)
730
-
.await;
731
-
732
-
let (did, key_bytes) = match session {
733
-
Ok(Some(row)) => (row.did, row.key_bytes),
734
-
Ok(None) => {
735
-
return (
736
-
StatusCode::UNAUTHORIZED,
737
-
Json(json!({"error": "AuthenticationFailed"})),
738
-
)
739
-
.into_response();
740
-
}
741
-
Err(e) => {
742
-
error!("DB error in activate_account: {:?}", e);
743
-
return (
744
-
StatusCode::INTERNAL_SERVER_ERROR,
745
-
Json(json!({"error": "InternalError"})),
746
-
)
747
-
.into_response();
748
-
}
749
-
};
750
-
751
-
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
752
-
return (
753
-
StatusCode::UNAUTHORIZED,
754
-
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
755
-
)
756
-
.into_response();
757
-
}
758
-
759
-
let result = sqlx::query!("UPDATE users SET deactivated_at = NULL WHERE did = $1", did)
760
-
.execute(&state.db)
761
-
.await;
762
-
763
-
match result {
764
-
Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(),
765
-
Err(e) => {
766
-
error!("DB error activating account: {:?}", e);
767
-
(
768
-
StatusCode::INTERNAL_SERVER_ERROR,
769
-
Json(json!({"error": "InternalError"})),
770
-
)
771
-
.into_response()
772
-
}
773
-
}
774
-
}
775
-
776
-
#[derive(Deserialize)]
777
-
#[serde(rename_all = "camelCase")]
778
-
pub struct DeactivateAccountInput {
779
-
pub delete_after: Option<String>,
780
-
}
781
-
782
-
pub async fn deactivate_account(
783
-
State(state): State<AppState>,
784
-
headers: axum::http::HeaderMap,
785
-
Json(_input): Json<DeactivateAccountInput>,
786
-
) -> Response {
787
-
let auth_header = headers.get("Authorization");
788
-
if auth_header.is_none() {
789
-
return (
790
-
StatusCode::UNAUTHORIZED,
791
-
Json(json!({"error": "AuthenticationRequired"})),
792
-
)
793
-
.into_response();
794
-
}
795
-
796
-
let token = auth_header
797
-
.unwrap()
798
-
.to_str()
799
-
.unwrap_or("")
800
-
.replace("Bearer ", "");
801
-
802
-
let session = sqlx::query!(
803
-
r#"
804
-
SELECT s.did, k.key_bytes
805
-
FROM sessions s
806
-
JOIN users u ON s.did = u.did
807
-
JOIN user_keys k ON u.id = k.user_id
808
-
WHERE s.access_jwt = $1
809
-
"#,
810
-
token
811
-
)
812
-
.fetch_optional(&state.db)
813
-
.await;
814
-
815
-
let (did, key_bytes) = match session {
816
-
Ok(Some(row)) => (row.did, row.key_bytes),
817
-
Ok(None) => {
818
-
return (
819
-
StatusCode::UNAUTHORIZED,
820
-
Json(json!({"error": "AuthenticationFailed"})),
821
-
)
822
-
.into_response();
823
-
}
824
-
Err(e) => {
825
-
error!("DB error in deactivate_account: {:?}", e);
826
-
return (
827
-
StatusCode::INTERNAL_SERVER_ERROR,
828
-
Json(json!({"error": "InternalError"})),
829
-
)
830
-
.into_response();
831
-
}
832
-
};
833
-
834
-
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
835
-
return (
836
-
StatusCode::UNAUTHORIZED,
837
-
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
838
-
)
839
-
.into_response();
840
-
}
841
-
842
-
let result = sqlx::query!("UPDATE users SET deactivated_at = NOW() WHERE did = $1", did)
843
-
.execute(&state.db)
844
-
.await;
845
-
846
-
match result {
847
-
Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(),
848
-
Err(e) => {
849
-
error!("DB error deactivating account: {:?}", e);
850
-
(
851
-
StatusCode::INTERNAL_SERVER_ERROR,
852
-
Json(json!({"error": "InternalError"})),
853
-
)
854
-
.into_response()
855
-
}
856
-
}
857
-
}
858
-
859
-
#[derive(Serialize)]
860
-
#[serde(rename_all = "camelCase")]
861
-
pub struct AppPassword {
862
-
pub name: String,
863
-
pub created_at: String,
864
-
pub privileged: bool,
865
-
}
866
-
867
-
#[derive(Serialize)]
868
-
pub struct ListAppPasswordsOutput {
869
-
pub passwords: Vec<AppPassword>,
870
-
}
871
-
872
-
pub async fn list_app_passwords(
873
-
State(state): State<AppState>,
874
-
headers: axum::http::HeaderMap,
875
-
) -> Response {
876
-
let auth_header = headers.get("Authorization");
877
-
if auth_header.is_none() {
878
-
return (
879
-
StatusCode::UNAUTHORIZED,
880
-
Json(json!({"error": "AuthenticationRequired"})),
881
-
)
882
-
.into_response();
883
-
}
884
-
885
-
let token = auth_header
886
-
.unwrap()
887
-
.to_str()
888
-
.unwrap_or("")
889
-
.replace("Bearer ", "");
890
-
891
-
let session = sqlx::query!(
892
-
r#"
893
-
SELECT s.did, k.key_bytes, u.id as user_id
894
-
FROM sessions s
895
-
JOIN users u ON s.did = u.did
896
-
JOIN user_keys k ON u.id = k.user_id
897
-
WHERE s.access_jwt = $1
898
-
"#,
899
-
token
900
-
)
901
-
.fetch_optional(&state.db)
902
-
.await;
903
-
904
-
let (_did, key_bytes, user_id) = match session {
905
-
Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
906
-
Ok(None) => {
907
-
return (
908
-
StatusCode::UNAUTHORIZED,
909
-
Json(json!({"error": "AuthenticationFailed"})),
910
-
)
911
-
.into_response();
912
-
}
913
-
Err(e) => {
914
-
error!("DB error in list_app_passwords: {:?}", e);
915
-
return (
916
-
StatusCode::INTERNAL_SERVER_ERROR,
917
-
Json(json!({"error": "InternalError"})),
918
-
)
919
-
.into_response();
920
-
}
921
-
};
922
-
923
-
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
924
-
return (
925
-
StatusCode::UNAUTHORIZED,
926
-
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
927
-
)
928
-
.into_response();
929
-
}
930
-
931
-
let result = sqlx::query!("SELECT name, created_at, privileged FROM app_passwords WHERE user_id = $1 ORDER BY created_at DESC", user_id)
932
-
.fetch_all(&state.db)
933
-
.await;
934
-
935
-
match result {
936
-
Ok(rows) => {
937
-
let passwords: Vec<AppPassword> = rows
938
-
.iter()
939
-
.map(|row| {
940
-
AppPassword {
941
-
name: row.name.clone(),
942
-
created_at: row.created_at.to_rfc3339(),
943
-
privileged: row.privileged,
944
-
}
945
-
})
946
-
.collect();
947
-
948
-
(StatusCode::OK, Json(ListAppPasswordsOutput { passwords })).into_response()
949
-
}
950
-
Err(e) => {
951
-
error!("DB error listing app passwords: {:?}", e);
952
-
(
953
-
StatusCode::INTERNAL_SERVER_ERROR,
954
-
Json(json!({"error": "InternalError"})),
955
-
)
956
-
.into_response()
957
-
}
958
-
}
959
-
}
960
-
961
-
#[derive(Deserialize)]
962
-
pub struct CreateAppPasswordInput {
963
-
pub name: String,
964
-
pub privileged: Option<bool>,
965
-
}
966
-
967
-
#[derive(Serialize)]
968
-
#[serde(rename_all = "camelCase")]
969
-
pub struct CreateAppPasswordOutput {
970
-
pub name: String,
971
-
pub password: String,
972
-
pub created_at: String,
973
-
pub privileged: bool,
974
-
}
975
-
976
-
pub async fn create_app_password(
977
-
State(state): State<AppState>,
978
-
headers: axum::http::HeaderMap,
979
-
Json(input): Json<CreateAppPasswordInput>,
980
-
) -> Response {
981
-
let auth_header = headers.get("Authorization");
982
-
if auth_header.is_none() {
983
-
return (
984
-
StatusCode::UNAUTHORIZED,
985
-
Json(json!({"error": "AuthenticationRequired"})),
986
-
)
987
-
.into_response();
988
-
}
989
-
990
-
let token = auth_header
991
-
.unwrap()
992
-
.to_str()
993
-
.unwrap_or("")
994
-
.replace("Bearer ", "");
995
-
996
-
let session = sqlx::query!(
997
-
r#"
998
-
SELECT s.did, k.key_bytes, u.id as user_id
999
-
FROM sessions s
1000
-
JOIN users u ON s.did = u.did
1001
-
JOIN user_keys k ON u.id = k.user_id
1002
-
WHERE s.access_jwt = $1
1003
-
"#,
1004
-
token
1005
-
)
1006
-
.fetch_optional(&state.db)
1007
-
.await;
1008
-
1009
-
let (_did, key_bytes, user_id) = match session {
1010
-
Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
1011
-
Ok(None) => {
1012
-
return (
1013
-
StatusCode::UNAUTHORIZED,
1014
-
Json(json!({"error": "AuthenticationFailed"})),
1015
-
)
1016
-
.into_response();
1017
-
}
1018
-
Err(e) => {
1019
-
error!("DB error in create_app_password: {:?}", e);
1020
-
return (
1021
-
StatusCode::INTERNAL_SERVER_ERROR,
1022
-
Json(json!({"error": "InternalError"})),
1023
-
)
1024
-
.into_response();
1025
-
}
1026
-
};
1027
-
1028
-
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
1029
-
return (
1030
-
StatusCode::UNAUTHORIZED,
1031
-
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
1032
-
)
1033
-
.into_response();
1034
-
}
1035
-
1036
-
let name = input.name.trim();
1037
-
if name.is_empty() {
1038
-
return (
1039
-
StatusCode::BAD_REQUEST,
1040
-
Json(json!({"error": "InvalidRequest", "message": "name is required"})),
1041
-
)
1042
-
.into_response();
1043
-
}
1044
-
1045
-
let existing = sqlx::query!("SELECT id FROM app_passwords WHERE user_id = $1 AND name = $2", user_id, name)
1046
-
.fetch_optional(&state.db)
1047
-
.await;
1048
-
1049
-
if let Ok(Some(_)) = existing {
1050
-
return (
1051
-
StatusCode::BAD_REQUEST,
1052
-
Json(json!({"error": "DuplicateAppPassword", "message": "App password with this name already exists"})),
1053
-
)
1054
-
.into_response();
1055
-
}
1056
-
1057
-
let password: String = (0..4)
1058
-
.map(|_| {
1059
-
use rand::Rng;
1060
-
let mut rng = rand::thread_rng();
1061
-
let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz234567".chars().collect();
1062
-
(0..4).map(|_| chars[rng.gen_range(0..chars.len())]).collect::<String>()
1063
-
})
1064
-
.collect::<Vec<String>>()
1065
-
.join("-");
1066
-
1067
-
let password_hash = match bcrypt::hash(&password, bcrypt::DEFAULT_COST) {
1068
-
Ok(h) => h,
1069
-
Err(e) => {
1070
-
error!("Failed to hash password: {:?}", e);
1071
-
return (
1072
-
StatusCode::INTERNAL_SERVER_ERROR,
1073
-
Json(json!({"error": "InternalError"})),
1074
-
)
1075
-
.into_response();
1076
-
}
1077
-
};
1078
-
1079
-
let privileged = input.privileged.unwrap_or(false);
1080
-
let created_at = chrono::Utc::now();
1081
-
1082
-
let result = sqlx::query!(
1083
-
"INSERT INTO app_passwords (user_id, name, password_hash, created_at, privileged) VALUES ($1, $2, $3, $4, $5)",
1084
-
user_id,
1085
-
name,
1086
-
password_hash,
1087
-
created_at,
1088
-
privileged
1089
-
)
1090
-
.execute(&state.db)
1091
-
.await;
1092
-
1093
-
match result {
1094
-
Ok(_) => (
1095
-
StatusCode::OK,
1096
-
Json(CreateAppPasswordOutput {
1097
-
name: name.to_string(),
1098
-
password,
1099
-
created_at: created_at.to_rfc3339(),
1100
-
privileged,
1101
-
}),
1102
-
)
1103
-
.into_response(),
1104
-
Err(e) => {
1105
-
error!("DB error creating app password: {:?}", e);
1106
-
(
1107
-
StatusCode::INTERNAL_SERVER_ERROR,
1108
-
Json(json!({"error": "InternalError"})),
1109
-
)
1110
-
.into_response()
1111
-
}
1112
-
}
1113
-
}
1114
-
1115
-
#[derive(Deserialize)]
1116
-
pub struct RevokeAppPasswordInput {
1117
-
pub name: String,
1118
-
}
1119
-
1120
-
pub async fn revoke_app_password(
1121
-
State(state): State<AppState>,
1122
-
headers: axum::http::HeaderMap,
1123
-
Json(input): Json<RevokeAppPasswordInput>,
1124
-
) -> Response {
1125
-
let auth_header = headers.get("Authorization");
1126
-
if auth_header.is_none() {
1127
-
return (
1128
-
StatusCode::UNAUTHORIZED,
1129
-
Json(json!({"error": "AuthenticationRequired"})),
1130
-
)
1131
-
.into_response();
1132
-
}
1133
-
1134
-
let token = auth_header
1135
-
.unwrap()
1136
-
.to_str()
1137
-
.unwrap_or("")
1138
-
.replace("Bearer ", "");
1139
-
1140
-
let session = sqlx::query!(
1141
-
r#"
1142
-
SELECT s.did, k.key_bytes, u.id as user_id
1143
-
FROM sessions s
1144
-
JOIN users u ON s.did = u.did
1145
-
JOIN user_keys k ON u.id = k.user_id
1146
-
WHERE s.access_jwt = $1
1147
-
"#,
1148
-
token
1149
-
)
1150
-
.fetch_optional(&state.db)
1151
-
.await;
1152
-
1153
-
let (_did, key_bytes, user_id) = match session {
1154
-
Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
1155
-
Ok(None) => {
1156
-
return (
1157
-
StatusCode::UNAUTHORIZED,
1158
-
Json(json!({"error": "AuthenticationFailed"})),
1159
-
)
1160
-
.into_response();
1161
-
}
1162
-
Err(e) => {
1163
-
error!("DB error in revoke_app_password: {:?}", e);
1164
-
return (
1165
-
StatusCode::INTERNAL_SERVER_ERROR,
1166
-
Json(json!({"error": "InternalError"})),
1167
-
)
1168
-
.into_response();
1169
-
}
1170
-
};
1171
-
1172
-
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
1173
-
return (
1174
-
StatusCode::UNAUTHORIZED,
1175
-
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
1176
-
)
1177
-
.into_response();
1178
-
}
1179
-
1180
-
let name = input.name.trim();
1181
-
if name.is_empty() {
1182
-
return (
1183
-
StatusCode::BAD_REQUEST,
1184
-
Json(json!({"error": "InvalidRequest", "message": "name is required"})),
1185
-
)
1186
-
.into_response();
1187
-
}
1188
-
1189
-
let result = sqlx::query!("DELETE FROM app_passwords WHERE user_id = $1 AND name = $2", user_id, name)
1190
-
.execute(&state.db)
1191
-
.await;
1192
-
1193
-
match result {
1194
-
Ok(r) => {
1195
-
if r.rows_affected() == 0 {
1196
-
return (
1197
-
StatusCode::NOT_FOUND,
1198
-
Json(json!({"error": "AppPasswordNotFound", "message": "App password not found"})),
1199
-
)
1200
-
.into_response();
1201
-
}
1202
-
(StatusCode::OK, Json(json!({}))).into_response()
1203
-
}
1204
-
Err(e) => {
1205
-
error!("DB error revoking app password: {:?}", e);
1206
-
(
1207
-
StatusCode::INTERNAL_SERVER_ERROR,
1208
-
Json(json!({"error": "InternalError"})),
1209
-
)
1210
-
.into_response()
1211
-
}
1212
-
}
1213
-
}
1214
-
1215
-
fn generate_reset_code() -> String {
1216
-
let mut rng = rand::thread_rng();
1217
-
let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz234567".chars().collect();
1218
-
let part1: String = (0..5).map(|_| chars[rng.gen_range(0..chars.len())]).collect();
1219
-
let part2: String = (0..5).map(|_| chars[rng.gen_range(0..chars.len())]).collect();
1220
-
format!("{}-{}", part1, part2)
1221
-
}
1222
-
1223
-
#[derive(Deserialize)]
1224
-
pub struct RequestPasswordResetInput {
1225
-
pub email: String,
1226
-
}
1227
-
1228
-
pub async fn request_password_reset(
1229
-
State(state): State<AppState>,
1230
-
Json(input): Json<RequestPasswordResetInput>,
1231
-
) -> Response {
1232
-
let email = input.email.trim().to_lowercase();
1233
-
if email.is_empty() {
1234
-
return (
1235
-
StatusCode::BAD_REQUEST,
1236
-
Json(json!({"error": "InvalidRequest", "message": "email is required"})),
1237
-
)
1238
-
.into_response();
1239
-
}
1240
-
1241
-
let user = sqlx::query!(
1242
-
"SELECT id, handle FROM users WHERE LOWER(email) = $1",
1243
-
email
1244
-
)
1245
-
.fetch_optional(&state.db)
1246
-
.await;
1247
-
1248
-
let (user_id, handle) = match user {
1249
-
Ok(Some(row)) => (row.id, row.handle),
1250
-
Ok(None) => {
1251
-
info!("Password reset requested for unknown email: {}", email);
1252
-
return (StatusCode::OK, Json(json!({}))).into_response();
1253
-
}
1254
-
Err(e) => {
1255
-
error!("DB error in request_password_reset: {:?}", e);
1256
-
return (
1257
-
StatusCode::INTERNAL_SERVER_ERROR,
1258
-
Json(json!({"error": "InternalError"})),
1259
-
)
1260
-
.into_response();
1261
-
}
1262
-
};
1263
-
1264
-
let code = generate_reset_code();
1265
-
let expires_at = Utc::now() + Duration::minutes(10);
1266
-
1267
-
let update = sqlx::query!(
1268
-
"UPDATE users SET password_reset_code = $1, password_reset_code_expires_at = $2 WHERE id = $3",
1269
-
code,
1270
-
expires_at,
1271
-
user_id
1272
-
)
1273
-
.execute(&state.db)
1274
-
.await;
1275
-
1276
-
if let Err(e) = update {
1277
-
error!("DB error setting reset code: {:?}", e);
1278
-
return (
1279
-
StatusCode::INTERNAL_SERVER_ERROR,
1280
-
Json(json!({"error": "InternalError"})),
1281
-
)
1282
-
.into_response();
1283
-
}
1284
-
1285
-
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
1286
-
if let Err(e) = crate::notifications::enqueue_password_reset(
1287
-
&state.db,
1288
-
user_id,
1289
-
&email,
1290
-
&handle,
1291
-
&code,
1292
-
&hostname,
1293
-
)
1294
-
.await
1295
-
{
1296
-
warn!("Failed to enqueue password reset notification: {:?}", e);
1297
-
}
1298
-
1299
-
info!("Password reset requested for user {}", user_id);
1300
-
1301
-
(StatusCode::OK, Json(json!({}))).into_response()
1302
-
}
1303
-
1304
-
#[derive(Deserialize)]
1305
-
pub struct ResetPasswordInput {
1306
-
pub token: String,
1307
-
pub password: String,
1308
-
}
1309
-
1310
-
pub async fn reset_password(
1311
-
State(state): State<AppState>,
1312
-
Json(input): Json<ResetPasswordInput>,
1313
-
) -> Response {
1314
-
let token = input.token.trim();
1315
-
let password = &input.password;
1316
-
1317
-
if token.is_empty() {
1318
-
return (
1319
-
StatusCode::BAD_REQUEST,
1320
-
Json(json!({"error": "InvalidToken", "message": "token is required"})),
1321
-
)
1322
-
.into_response();
1323
-
}
1324
-
1325
-
if password.is_empty() {
1326
-
return (
1327
-
StatusCode::BAD_REQUEST,
1328
-
Json(json!({"error": "InvalidRequest", "message": "password is required"})),
1329
-
)
1330
-
.into_response();
1331
-
}
1332
-
1333
-
let user = sqlx::query!(
1334
-
"SELECT id, password_reset_code, password_reset_code_expires_at FROM users WHERE password_reset_code = $1",
1335
-
token
1336
-
)
1337
-
.fetch_optional(&state.db)
1338
-
.await;
1339
-
1340
-
let (user_id, expires_at) = match user {
1341
-
Ok(Some(row)) => {
1342
-
let expires = row.password_reset_code_expires_at;
1343
-
(row.id, expires)
1344
-
}
1345
-
Ok(None) => {
1346
-
return (
1347
-
StatusCode::BAD_REQUEST,
1348
-
Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})),
1349
-
)
1350
-
.into_response();
1351
-
}
1352
-
Err(e) => {
1353
-
error!("DB error in reset_password: {:?}", e);
1354
-
return (
1355
-
StatusCode::INTERNAL_SERVER_ERROR,
1356
-
Json(json!({"error": "InternalError"})),
1357
-
)
1358
-
.into_response();
1359
-
}
1360
-
};
1361
-
1362
-
if let Some(exp) = expires_at {
1363
-
if Utc::now() > exp {
1364
-
let _ = sqlx::query!(
1365
-
"UPDATE users SET password_reset_code = NULL, password_reset_code_expires_at = NULL WHERE id = $1",
1366
-
user_id
1367
-
)
1368
-
.execute(&state.db)
1369
-
.await;
1370
-
1371
-
return (
1372
-
StatusCode::BAD_REQUEST,
1373
-
Json(json!({"error": "ExpiredToken", "message": "Token has expired"})),
1374
-
)
1375
-
.into_response();
1376
-
}
1377
-
} else {
1378
-
return (
1379
-
StatusCode::BAD_REQUEST,
1380
-
Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})),
1381
-
)
1382
-
.into_response();
1383
-
}
1384
-
1385
-
let password_hash = match hash(password, DEFAULT_COST) {
1386
-
Ok(h) => h,
1387
-
Err(e) => {
1388
-
error!("Failed to hash password: {:?}", e);
1389
-
return (
1390
-
StatusCode::INTERNAL_SERVER_ERROR,
1391
-
Json(json!({"error": "InternalError"})),
1392
-
)
1393
-
.into_response();
1394
-
}
1395
-
};
1396
-
1397
-
let update = sqlx::query!(
1398
-
"UPDATE users SET password_hash = $1, password_reset_code = NULL, password_reset_code_expires_at = NULL WHERE id = $2",
1399
-
password_hash,
1400
-
user_id
1401
-
)
1402
-
.execute(&state.db)
1403
-
.await;
1404
-
1405
-
if let Err(e) = update {
1406
-
error!("DB error updating password: {:?}", e);
1407
-
return (
1408
-
StatusCode::INTERNAL_SERVER_ERROR,
1409
-
Json(json!({"error": "InternalError"})),
1410
-
)
1411
-
.into_response();
1412
-
}
1413
-
1414
-
let _ = sqlx::query!("DELETE FROM sessions WHERE did = (SELECT did FROM users WHERE id = $1)", user_id)
1415
-
.execute(&state.db)
1416
-
.await;
1417
-
1418
-
info!("Password reset completed for user {}", user_id);
1419
-
1420
-
(StatusCode::OK, Json(json!({}))).into_response()
1421
-
}
1422
-
1423
-
#[derive(Deserialize)]
1424
-
#[serde(rename_all = "camelCase")]
1425
-
pub struct RequestEmailUpdateInput {
1426
-
pub email: String,
1427
-
}
1428
-
1429
-
pub async fn request_email_update(
1430
-
State(state): State<AppState>,
1431
-
headers: axum::http::HeaderMap,
1432
-
Json(input): Json<RequestEmailUpdateInput>,
1433
-
) -> Response {
1434
-
let auth_header = headers.get("Authorization");
1435
-
if auth_header.is_none() {
1436
-
return (
1437
-
StatusCode::UNAUTHORIZED,
1438
-
Json(json!({"error": "AuthenticationRequired"})),
1439
-
)
1440
-
.into_response();
1441
-
}
1442
-
1443
-
let token = auth_header
1444
-
.unwrap()
1445
-
.to_str()
1446
-
.unwrap_or("")
1447
-
.replace("Bearer ", "");
1448
-
1449
-
let session = sqlx::query!(
1450
-
r#"
1451
-
SELECT s.did, k.key_bytes, u.id as user_id, u.handle
1452
-
FROM sessions s
1453
-
JOIN users u ON s.did = u.did
1454
-
JOIN user_keys k ON u.id = k.user_id
1455
-
WHERE s.access_jwt = $1
1456
-
"#,
1457
-
token
1458
-
)
1459
-
.fetch_optional(&state.db)
1460
-
.await;
1461
-
1462
-
let (_did, key_bytes, user_id, handle) = match session {
1463
-
Ok(Some(row)) => (row.did, row.key_bytes, row.user_id, row.handle),
1464
-
Ok(None) => {
1465
-
return (
1466
-
StatusCode::UNAUTHORIZED,
1467
-
Json(json!({"error": "AuthenticationFailed"})),
1468
-
)
1469
-
.into_response();
1470
-
}
1471
-
Err(e) => {
1472
-
error!("DB error in request_email_update: {:?}", e);
1473
-
return (
1474
-
StatusCode::INTERNAL_SERVER_ERROR,
1475
-
Json(json!({"error": "InternalError"})),
1476
-
)
1477
-
.into_response();
1478
-
}
1479
-
};
1480
-
1481
-
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
1482
-
return (
1483
-
StatusCode::UNAUTHORIZED,
1484
-
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
1485
-
)
1486
-
.into_response();
1487
-
}
1488
-
1489
-
let email = input.email.trim().to_lowercase();
1490
-
if email.is_empty() {
1491
-
return (
1492
-
StatusCode::BAD_REQUEST,
1493
-
Json(json!({"error": "InvalidRequest", "message": "email is required"})),
1494
-
)
1495
-
.into_response();
1496
-
}
1497
-
1498
-
let exists = sqlx::query!("SELECT 1 as one FROM users WHERE LOWER(email) = $1", email)
1499
-
.fetch_optional(&state.db)
1500
-
.await;
1501
-
1502
-
if let Ok(Some(_)) = exists {
1503
-
return (
1504
-
StatusCode::BAD_REQUEST,
1505
-
Json(json!({"error": "EmailTaken", "message": "Email already taken"})),
1506
-
)
1507
-
.into_response();
1508
-
}
1509
-
1510
-
let code = generate_reset_code();
1511
-
let expires_at = Utc::now() + Duration::minutes(10);
1512
-
1513
-
let update = sqlx::query!(
1514
-
"UPDATE users SET email_pending_verification = $1, email_confirmation_code = $2, email_confirmation_code_expires_at = $3 WHERE id = $4",
1515
-
email,
1516
-
code,
1517
-
expires_at,
1518
-
user_id
1519
-
)
1520
-
.execute(&state.db)
1521
-
.await;
1522
-
1523
-
if let Err(e) = update {
1524
-
error!("DB error setting email update code: {:?}", e);
1525
-
return (
1526
-
StatusCode::INTERNAL_SERVER_ERROR,
1527
-
Json(json!({"error": "InternalError"})),
1528
-
)
1529
-
.into_response();
1530
-
}
1531
-
1532
-
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
1533
-
if let Err(e) = crate::notifications::enqueue_email_update(
1534
-
&state.db,
1535
-
user_id,
1536
-
&email,
1537
-
&handle,
1538
-
&code,
1539
-
&hostname,
1540
-
)
1541
-
.await
1542
-
{
1543
-
warn!("Failed to enqueue email update notification: {:?}", e);
1544
-
}
1545
-
1546
-
info!("Email update requested for user {}", user_id);
1547
-
1548
-
(StatusCode::OK, Json(json!({ "tokenRequired": true }))).into_response()
1549
-
}
1550
-
1551
-
#[derive(Deserialize)]
1552
-
#[serde(rename_all = "camelCase")]
1553
-
pub struct ConfirmEmailInput {
1554
-
pub email: String,
1555
-
pub token: String,
1556
-
}
1557
-
1558
-
pub async fn confirm_email(
1559
-
State(state): State<AppState>,
1560
-
headers: axum::http::HeaderMap,
1561
-
Json(input): Json<ConfirmEmailInput>,
1562
-
) -> Response {
1563
-
let auth_header = headers.get("Authorization");
1564
-
if auth_header.is_none() {
1565
-
return (
1566
-
StatusCode::UNAUTHORIZED,
1567
-
Json(json!({"error": "AuthenticationRequired"})),
1568
-
)
1569
-
.into_response();
1570
-
}
1571
-
1572
-
let token = auth_header
1573
-
.unwrap()
1574
-
.to_str()
1575
-
.unwrap_or("")
1576
-
.replace("Bearer ", "");
1577
-
1578
-
let session = sqlx::query!(
1579
-
r#"
1580
-
SELECT s.did, k.key_bytes, u.id as user_id, u.email_confirmation_code, u.email_confirmation_code_expires_at, u.email_pending_verification
1581
-
FROM sessions s
1582
-
JOIN users u ON s.did = u.did
1583
-
JOIN user_keys k ON u.id = k.user_id
1584
-
WHERE s.access_jwt = $1
1585
-
"#,
1586
-
token
1587
-
)
1588
-
.fetch_optional(&state.db)
1589
-
.await;
1590
-
1591
-
let (_did, key_bytes, user_id, stored_code, expires_at, email_pending_verification) = match session {
1592
-
Ok(Some(row)) => (
1593
-
row.did,
1594
-
row.key_bytes,
1595
-
row.user_id,
1596
-
row.email_confirmation_code,
1597
-
row.email_confirmation_code_expires_at,
1598
-
row.email_pending_verification,
1599
-
),
1600
-
Ok(None) => {
1601
-
return (
1602
-
StatusCode::UNAUTHORIZED,
1603
-
Json(json!({"error": "AuthenticationFailed"})),
1604
-
)
1605
-
.into_response();
1606
-
}
1607
-
Err(e) => {
1608
-
error!("DB error in confirm_email: {:?}", e);
1609
-
return (
1610
-
StatusCode::INTERNAL_SERVER_ERROR,
1611
-
Json(json!({"error": "InternalError"})),
1612
-
)
1613
-
.into_response();
1614
-
}
1615
-
};
1616
-
1617
-
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
1618
-
return (
1619
-
StatusCode::UNAUTHORIZED,
1620
-
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
1621
-
)
1622
-
.into_response();
1623
-
}
1624
-
1625
-
let email = input.email.trim().to_lowercase();
1626
-
let confirmation_code = input.token.trim();
1627
-
1628
-
if email_pending_verification.is_none() || stored_code.is_none() || expires_at.is_none() {
1629
-
return (
1630
-
StatusCode::BAD_REQUEST,
1631
-
Json(json!({"error": "InvalidRequest", "message": "No pending email update found"})),
1632
-
)
1633
-
.into_response();
1634
-
}
1635
-
1636
-
let email_pending_verification = email_pending_verification.unwrap();
1637
-
if email_pending_verification != email {
1638
-
return (
1639
-
StatusCode::BAD_REQUEST,
1640
-
Json(json!({"error": "InvalidRequest", "message": "Email does not match pending update"})),
1641
-
)
1642
-
.into_response();
1643
-
}
1644
-
1645
-
if stored_code.unwrap() != confirmation_code {
1646
-
return (
1647
-
StatusCode::BAD_REQUEST,
1648
-
Json(json!({"error": "InvalidToken", "message": "Invalid token"})),
1649
-
)
1650
-
.into_response();
1651
-
}
1652
-
1653
-
if Utc::now() > expires_at.unwrap() {
1654
-
return (
1655
-
StatusCode::BAD_REQUEST,
1656
-
Json(json!({"error": "ExpiredToken", "message": "Token has expired"})),
1657
-
)
1658
-
.into_response();
1659
-
}
1660
-
1661
-
let update = sqlx::query!(
1662
-
"UPDATE users SET email = $1, email_pending_verification = NULL, email_confirmation_code = NULL, email_confirmation_code_expires_at = NULL WHERE id = $2",
1663
-
email_pending_verification,
1664
-
user_id
1665
-
)
1666
-
.execute(&state.db)
1667
-
.await;
1668
-
1669
-
if let Err(e) = update {
1670
-
error!("DB error finalizing email update: {:?}", e);
1671
-
if e.as_database_error().map(|db_err| db_err.is_unique_violation()).unwrap_or(false) {
1672
-
return (
1673
-
StatusCode::BAD_REQUEST,
1674
-
Json(json!({"error": "EmailTaken", "message": "Email already taken"})),
1675
-
)
1676
-
.into_response();
1677
-
}
1678
-
1679
-
return (
1680
-
StatusCode::INTERNAL_SERVER_ERROR,
1681
-
Json(json!({"error": "InternalError"})),
1682
-
)
1683
-
.into_response();
1684
-
}
1685
-
1686
-
info!("Email updated for user {}", user_id);
1687
-
1688
-
(StatusCode::OK, Json(json!({}))).into_response()
1689
-
}
···
5
http::StatusCode,
6
response::{IntoResponse, Response},
7
};
8
+
use bcrypt::verify;
9
use serde::{Deserialize, Serialize};
10
use serde_json::json;
11
use tracing::{error, info, warn};
12
13
#[derive(Deserialize)]
14
pub struct GetServiceAuthParams {
···
340
.into_response()
341
}
342
343
pub async fn refresh_session(
344
State(state): State<AppState>,
345
headers: axum::http::HeaderMap,
···
471
}
472
}
473
}
+229
src/sync/blob.rs
+229
src/sync/blob.rs
···
···
1
+
use crate::state::AppState;
2
+
use axum::{
3
+
Json,
4
+
body::Body,
5
+
extract::{Query, State},
6
+
http::StatusCode,
7
+
http::header,
8
+
response::{IntoResponse, Response},
9
+
};
10
+
use serde::{Deserialize, Serialize};
11
+
use serde_json::json;
12
+
use tracing::error;
13
+
14
+
#[derive(Deserialize)]
15
+
pub struct GetBlobParams {
16
+
pub did: String,
17
+
pub cid: String,
18
+
}
19
+
20
+
pub async fn get_blob(
21
+
State(state): State<AppState>,
22
+
Query(params): Query<GetBlobParams>,
23
+
) -> Response {
24
+
let did = params.did.trim();
25
+
let cid = params.cid.trim();
26
+
27
+
if did.is_empty() {
28
+
return (
29
+
StatusCode::BAD_REQUEST,
30
+
Json(json!({"error": "InvalidRequest", "message": "did is required"})),
31
+
)
32
+
.into_response();
33
+
}
34
+
35
+
if cid.is_empty() {
36
+
return (
37
+
StatusCode::BAD_REQUEST,
38
+
Json(json!({"error": "InvalidRequest", "message": "cid is required"})),
39
+
)
40
+
.into_response();
41
+
}
42
+
43
+
let user_exists = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
44
+
.fetch_optional(&state.db)
45
+
.await;
46
+
47
+
match user_exists {
48
+
Ok(None) => {
49
+
return (
50
+
StatusCode::NOT_FOUND,
51
+
Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})),
52
+
)
53
+
.into_response();
54
+
}
55
+
Err(e) => {
56
+
error!("DB error in get_blob: {:?}", e);
57
+
return (
58
+
StatusCode::INTERNAL_SERVER_ERROR,
59
+
Json(json!({"error": "InternalError"})),
60
+
)
61
+
.into_response();
62
+
}
63
+
Ok(Some(_)) => {}
64
+
}
65
+
66
+
let blob_result = sqlx::query!("SELECT storage_key, mime_type FROM blobs WHERE cid = $1", cid)
67
+
.fetch_optional(&state.db)
68
+
.await;
69
+
70
+
match blob_result {
71
+
Ok(Some(row)) => {
72
+
let storage_key = &row.storage_key;
73
+
let mime_type = &row.mime_type;
74
+
75
+
match state.blob_store.get(&storage_key).await {
76
+
Ok(data) => Response::builder()
77
+
.status(StatusCode::OK)
78
+
.header(header::CONTENT_TYPE, mime_type)
79
+
.body(Body::from(data))
80
+
.unwrap(),
81
+
Err(e) => {
82
+
error!("Failed to fetch blob from storage: {:?}", e);
83
+
(
84
+
StatusCode::NOT_FOUND,
85
+
Json(json!({"error": "BlobNotFound", "message": "Blob not found in storage"})),
86
+
)
87
+
.into_response()
88
+
}
89
+
}
90
+
}
91
+
Ok(None) => (
92
+
StatusCode::NOT_FOUND,
93
+
Json(json!({"error": "BlobNotFound", "message": "Blob not found"})),
94
+
)
95
+
.into_response(),
96
+
Err(e) => {
97
+
error!("DB error in get_blob: {:?}", e);
98
+
(
99
+
StatusCode::INTERNAL_SERVER_ERROR,
100
+
Json(json!({"error": "InternalError"})),
101
+
)
102
+
.into_response()
103
+
}
104
+
}
105
+
}
106
+
107
+
#[derive(Deserialize)]
108
+
pub struct ListBlobsParams {
109
+
pub did: String,
110
+
pub since: Option<String>,
111
+
pub limit: Option<i64>,
112
+
pub cursor: Option<String>,
113
+
}
114
+
115
+
#[derive(Serialize)]
116
+
pub struct ListBlobsOutput {
117
+
pub cursor: Option<String>,
118
+
pub cids: Vec<String>,
119
+
}
120
+
121
+
pub async fn list_blobs(
122
+
State(state): State<AppState>,
123
+
Query(params): Query<ListBlobsParams>,
124
+
) -> Response {
125
+
let did = params.did.trim();
126
+
127
+
if did.is_empty() {
128
+
return (
129
+
StatusCode::BAD_REQUEST,
130
+
Json(json!({"error": "InvalidRequest", "message": "did is required"})),
131
+
)
132
+
.into_response();
133
+
}
134
+
135
+
let limit = params.limit.unwrap_or(500).min(1000);
136
+
let cursor_cid = params.cursor.as_deref().unwrap_or("");
137
+
138
+
let user_result = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
139
+
.fetch_optional(&state.db)
140
+
.await;
141
+
142
+
let user_id = match user_result {
143
+
Ok(Some(row)) => row.id,
144
+
Ok(None) => {
145
+
return (
146
+
StatusCode::NOT_FOUND,
147
+
Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})),
148
+
)
149
+
.into_response();
150
+
}
151
+
Err(e) => {
152
+
error!("DB error in list_blobs: {:?}", e);
153
+
return (
154
+
StatusCode::INTERNAL_SERVER_ERROR,
155
+
Json(json!({"error": "InternalError"})),
156
+
)
157
+
.into_response();
158
+
}
159
+
};
160
+
161
+
let cids_result: Result<Vec<String>, sqlx::Error> = if let Some(since) = ¶ms.since {
162
+
let since_time = chrono::DateTime::parse_from_rfc3339(since)
163
+
.map(|dt| dt.with_timezone(&chrono::Utc))
164
+
.unwrap_or_else(|_| chrono::Utc::now());
165
+
sqlx::query!(
166
+
r#"
167
+
SELECT cid FROM blobs
168
+
WHERE created_by_user = $1 AND cid > $2 AND created_at > $3
169
+
ORDER BY cid ASC
170
+
LIMIT $4
171
+
"#,
172
+
user_id,
173
+
cursor_cid,
174
+
since_time,
175
+
limit + 1
176
+
)
177
+
.fetch_all(&state.db)
178
+
.await
179
+
.map(|rows| rows.into_iter().map(|r| r.cid).collect())
180
+
} else {
181
+
sqlx::query!(
182
+
r#"
183
+
SELECT cid FROM blobs
184
+
WHERE created_by_user = $1 AND cid > $2
185
+
ORDER BY cid ASC
186
+
LIMIT $3
187
+
"#,
188
+
user_id,
189
+
cursor_cid,
190
+
limit + 1
191
+
)
192
+
.fetch_all(&state.db)
193
+
.await
194
+
.map(|rows| rows.into_iter().map(|r| r.cid).collect())
195
+
};
196
+
197
+
match cids_result {
198
+
Ok(cids) => {
199
+
let has_more = cids.len() as i64 > limit;
200
+
let cids: Vec<String> = cids
201
+
.into_iter()
202
+
.take(limit as usize)
203
+
.collect();
204
+
205
+
let next_cursor = if has_more {
206
+
cids.last().cloned()
207
+
} else {
208
+
None
209
+
};
210
+
211
+
(
212
+
StatusCode::OK,
213
+
Json(ListBlobsOutput {
214
+
cursor: next_cursor,
215
+
cids,
216
+
}),
217
+
)
218
+
.into_response()
219
+
}
220
+
Err(e) => {
221
+
error!("DB error in list_blobs: {:?}", e);
222
+
(
223
+
StatusCode::INTERNAL_SERVER_ERROR,
224
+
Json(json!({"error": "InternalError"})),
225
+
)
226
+
.into_response()
227
+
}
228
+
}
229
+
}
+32
src/sync/car.rs
+32
src/sync/car.rs
···
···
1
+
use cid::Cid;
2
+
use std::io::Write;
3
+
4
+
pub fn write_varint<W: Write>(mut writer: W, mut value: u64) -> std::io::Result<()> {
5
+
loop {
6
+
let mut byte = (value & 0x7F) as u8;
7
+
value >>= 7;
8
+
if value != 0 {
9
+
byte |= 0x80;
10
+
}
11
+
writer.write_all(&[byte])?;
12
+
if value == 0 {
13
+
break;
14
+
}
15
+
}
16
+
Ok(())
17
+
}
18
+
19
+
pub fn ld_write<W: Write>(mut writer: W, data: &[u8]) -> std::io::Result<()> {
20
+
write_varint(&mut writer, data.len() as u64)?;
21
+
writer.write_all(data)?;
22
+
Ok(())
23
+
}
24
+
25
+
pub fn encode_car_header(root_cid: &Cid) -> Vec<u8> {
26
+
let header = serde_ipld_dagcbor::to_vec(&serde_json::json!({
27
+
"version": 1u64,
28
+
"roots": [root_cid.to_bytes()]
29
+
}))
30
+
.unwrap_or_default();
31
+
header
32
+
}
+227
src/sync/commit.rs
+227
src/sync/commit.rs
···
···
1
+
use crate::state::AppState;
2
+
use axum::{
3
+
Json,
4
+
extract::{Query, State},
5
+
http::StatusCode,
6
+
response::{IntoResponse, Response},
7
+
};
8
+
use serde::{Deserialize, Serialize};
9
+
use serde_json::json;
10
+
use tracing::error;
11
+
12
+
#[derive(Deserialize)]
13
+
pub struct GetLatestCommitParams {
14
+
pub did: String,
15
+
}
16
+
17
+
#[derive(Serialize)]
18
+
pub struct GetLatestCommitOutput {
19
+
pub cid: String,
20
+
pub rev: String,
21
+
}
22
+
23
+
pub async fn get_latest_commit(
24
+
State(state): State<AppState>,
25
+
Query(params): Query<GetLatestCommitParams>,
26
+
) -> Response {
27
+
let did = params.did.trim();
28
+
29
+
if did.is_empty() {
30
+
return (
31
+
StatusCode::BAD_REQUEST,
32
+
Json(json!({"error": "InvalidRequest", "message": "did is required"})),
33
+
)
34
+
.into_response();
35
+
}
36
+
37
+
let result = sqlx::query!(
38
+
r#"
39
+
SELECT r.repo_root_cid
40
+
FROM repos r
41
+
JOIN users u ON r.user_id = u.id
42
+
WHERE u.did = $1
43
+
"#,
44
+
did
45
+
)
46
+
.fetch_optional(&state.db)
47
+
.await;
48
+
49
+
match result {
50
+
Ok(Some(row)) => {
51
+
(
52
+
StatusCode::OK,
53
+
Json(GetLatestCommitOutput {
54
+
cid: row.repo_root_cid,
55
+
rev: chrono::Utc::now().timestamp_millis().to_string(),
56
+
}),
57
+
)
58
+
.into_response()
59
+
}
60
+
Ok(None) => (
61
+
StatusCode::NOT_FOUND,
62
+
Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})),
63
+
)
64
+
.into_response(),
65
+
Err(e) => {
66
+
error!("DB error in get_latest_commit: {:?}", e);
67
+
(
68
+
StatusCode::INTERNAL_SERVER_ERROR,
69
+
Json(json!({"error": "InternalError"})),
70
+
)
71
+
.into_response()
72
+
}
73
+
}
74
+
}
75
+
76
+
#[derive(Deserialize)]
77
+
pub struct ListReposParams {
78
+
pub limit: Option<i64>,
79
+
pub cursor: Option<String>,
80
+
}
81
+
82
+
#[derive(Serialize)]
83
+
#[serde(rename_all = "camelCase")]
84
+
pub struct RepoInfo {
85
+
pub did: String,
86
+
pub head: String,
87
+
pub rev: String,
88
+
pub active: bool,
89
+
}
90
+
91
+
#[derive(Serialize)]
92
+
pub struct ListReposOutput {
93
+
pub cursor: Option<String>,
94
+
pub repos: Vec<RepoInfo>,
95
+
}
96
+
97
+
pub async fn list_repos(
98
+
State(state): State<AppState>,
99
+
Query(params): Query<ListReposParams>,
100
+
) -> Response {
101
+
let limit = params.limit.unwrap_or(50).min(1000);
102
+
let cursor_did = params.cursor.as_deref().unwrap_or("");
103
+
104
+
let result = sqlx::query!(
105
+
r#"
106
+
SELECT u.did, r.repo_root_cid
107
+
FROM repos r
108
+
JOIN users u ON r.user_id = u.id
109
+
WHERE u.did > $1
110
+
ORDER BY u.did ASC
111
+
LIMIT $2
112
+
"#,
113
+
cursor_did,
114
+
limit + 1
115
+
)
116
+
.fetch_all(&state.db)
117
+
.await;
118
+
119
+
match result {
120
+
Ok(rows) => {
121
+
let has_more = rows.len() as i64 > limit;
122
+
let repos: Vec<RepoInfo> = rows
123
+
.iter()
124
+
.take(limit as usize)
125
+
.map(|row| {
126
+
RepoInfo {
127
+
did: row.did.clone(),
128
+
head: row.repo_root_cid.clone(),
129
+
rev: chrono::Utc::now().timestamp_millis().to_string(),
130
+
active: true,
131
+
}
132
+
})
133
+
.collect();
134
+
135
+
let next_cursor = if has_more {
136
+
repos.last().map(|r| r.did.clone())
137
+
} else {
138
+
None
139
+
};
140
+
141
+
(
142
+
StatusCode::OK,
143
+
Json(ListReposOutput {
144
+
cursor: next_cursor,
145
+
repos,
146
+
}),
147
+
)
148
+
.into_response()
149
+
}
150
+
Err(e) => {
151
+
error!("DB error in list_repos: {:?}", e);
152
+
(
153
+
StatusCode::INTERNAL_SERVER_ERROR,
154
+
Json(json!({"error": "InternalError"})),
155
+
)
156
+
.into_response()
157
+
}
158
+
}
159
+
}
160
+
161
+
#[derive(Deserialize)]
162
+
pub struct GetRepoStatusParams {
163
+
pub did: String,
164
+
}
165
+
166
+
#[derive(Serialize)]
167
+
pub struct GetRepoStatusOutput {
168
+
pub did: String,
169
+
pub active: bool,
170
+
pub rev: Option<String>,
171
+
}
172
+
173
+
pub async fn get_repo_status(
174
+
State(state): State<AppState>,
175
+
Query(params): Query<GetRepoStatusParams>,
176
+
) -> Response {
177
+
let did = params.did.trim();
178
+
179
+
if did.is_empty() {
180
+
return (
181
+
StatusCode::BAD_REQUEST,
182
+
Json(json!({"error": "InvalidRequest", "message": "did is required"})),
183
+
)
184
+
.into_response();
185
+
}
186
+
187
+
let result = sqlx::query!(
188
+
r#"
189
+
SELECT u.did, r.repo_root_cid
190
+
FROM users u
191
+
LEFT JOIN repos r ON u.id = r.user_id
192
+
WHERE u.did = $1
193
+
"#,
194
+
did
195
+
)
196
+
.fetch_optional(&state.db)
197
+
.await;
198
+
199
+
match result {
200
+
Ok(Some(row)) => {
201
+
let rev = Some(chrono::Utc::now().timestamp_millis().to_string());
202
+
203
+
(
204
+
StatusCode::OK,
205
+
Json(GetRepoStatusOutput {
206
+
did: row.did,
207
+
active: true,
208
+
rev,
209
+
}),
210
+
)
211
+
.into_response()
212
+
}
213
+
Ok(None) => (
214
+
StatusCode::NOT_FOUND,
215
+
Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})),
216
+
)
217
+
.into_response(),
218
+
Err(e) => {
219
+
error!("DB error in get_repo_status: {:?}", e);
220
+
(
221
+
StatusCode::INTERNAL_SERVER_ERROR,
222
+
Json(json!({"error": "InternalError"})),
223
+
)
224
+
.into_response()
225
+
}
226
+
}
227
+
}
+40
src/sync/crawl.rs
+40
src/sync/crawl.rs
···
···
1
+
use crate::state::AppState;
2
+
use axum::{
3
+
Json,
4
+
extract::{Query, State},
5
+
http::StatusCode,
6
+
response::{IntoResponse, Response},
7
+
};
8
+
use serde::Deserialize;
9
+
use serde_json::json;
10
+
use tracing::info;
11
+
12
+
#[derive(Deserialize)]
13
+
pub struct NotifyOfUpdateParams {
14
+
pub hostname: String,
15
+
}
16
+
17
+
pub async fn notify_of_update(
18
+
State(_state): State<AppState>,
19
+
Query(params): Query<NotifyOfUpdateParams>,
20
+
) -> Response {
21
+
info!("Received notifyOfUpdate from hostname: {}", params.hostname);
22
+
info!("TODO: Queue job for notifyOfUpdate (not implemented)");
23
+
24
+
(StatusCode::OK, Json(json!({}))).into_response()
25
+
}
26
+
27
+
#[derive(Deserialize)]
28
+
pub struct RequestCrawlInput {
29
+
pub hostname: String,
30
+
}
31
+
32
+
pub async fn request_crawl(
33
+
State(_state): State<AppState>,
34
+
Json(input): Json<RequestCrawlInput>,
35
+
) -> Response {
36
+
info!("Received requestCrawl for hostname: {}", input.hostname);
37
+
info!("TODO: Queue job for requestCrawl (not implemented)");
38
+
39
+
(StatusCode::OK, Json(json!({}))).into_response()
40
+
}
+9
-1050
src/sync/mod.rs
+9
-1050
src/sync/mod.rs
···
1
-
use crate::state::AppState;
2
-
use axum::{
3
-
Json,
4
-
body::Body,
5
-
extract::{Query, State},
6
-
http::StatusCode,
7
-
http::header,
8
-
response::{IntoResponse, Response},
9
-
};
10
-
use bytes::Bytes;
11
-
use cid::Cid;
12
-
use jacquard_repo::{commit::Commit, storage::BlockStore};
13
-
use serde::{Deserialize, Serialize};
14
-
use serde_json::json;
15
-
use std::collections::HashSet;
16
-
use std::io::Write;
17
-
use tracing::{error, info};
18
-
19
-
fn write_varint<W: Write>(mut writer: W, mut value: u64) -> std::io::Result<()> {
20
-
loop {
21
-
let mut byte = (value & 0x7F) as u8;
22
-
value >>= 7;
23
-
if value != 0 {
24
-
byte |= 0x80;
25
-
}
26
-
writer.write_all(&[byte])?;
27
-
if value == 0 {
28
-
break;
29
-
}
30
-
}
31
-
Ok(())
32
-
}
33
-
34
-
fn ld_write<W: Write>(mut writer: W, data: &[u8]) -> std::io::Result<()> {
35
-
write_varint(&mut writer, data.len() as u64)?;
36
-
writer.write_all(data)?;
37
-
Ok(())
38
-
}
39
-
40
-
fn encode_car_header(root_cid: &Cid) -> Vec<u8> {
41
-
let header = serde_ipld_dagcbor::to_vec(&serde_json::json!({
42
-
"version": 1u64,
43
-
"roots": [root_cid.to_bytes()]
44
-
}))
45
-
.unwrap_or_default();
46
-
header
47
-
}
48
-
49
-
#[derive(Deserialize)]
50
-
pub struct GetLatestCommitParams {
51
-
pub did: String,
52
-
}
53
-
54
-
#[derive(Serialize)]
55
-
pub struct GetLatestCommitOutput {
56
-
pub cid: String,
57
-
pub rev: String,
58
-
}
59
-
60
-
pub async fn get_latest_commit(
61
-
State(state): State<AppState>,
62
-
Query(params): Query<GetLatestCommitParams>,
63
-
) -> Response {
64
-
let did = params.did.trim();
65
-
66
-
if did.is_empty() {
67
-
return (
68
-
StatusCode::BAD_REQUEST,
69
-
Json(json!({"error": "InvalidRequest", "message": "did is required"})),
70
-
)
71
-
.into_response();
72
-
}
73
-
74
-
let result = sqlx::query!(
75
-
r#"
76
-
SELECT r.repo_root_cid
77
-
FROM repos r
78
-
JOIN users u ON r.user_id = u.id
79
-
WHERE u.did = $1
80
-
"#,
81
-
did
82
-
)
83
-
.fetch_optional(&state.db)
84
-
.await;
85
-
86
-
match result {
87
-
Ok(Some(row)) => {
88
-
(
89
-
StatusCode::OK,
90
-
Json(GetLatestCommitOutput {
91
-
cid: row.repo_root_cid,
92
-
rev: chrono::Utc::now().timestamp_millis().to_string(),
93
-
}),
94
-
)
95
-
.into_response()
96
-
}
97
-
Ok(None) => (
98
-
StatusCode::NOT_FOUND,
99
-
Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})),
100
-
)
101
-
.into_response(),
102
-
Err(e) => {
103
-
error!("DB error in get_latest_commit: {:?}", e);
104
-
(
105
-
StatusCode::INTERNAL_SERVER_ERROR,
106
-
Json(json!({"error": "InternalError"})),
107
-
)
108
-
.into_response()
109
-
}
110
-
}
111
-
}
112
-
113
-
#[derive(Deserialize)]
114
-
pub struct ListReposParams {
115
-
pub limit: Option<i64>,
116
-
pub cursor: Option<String>,
117
-
}
118
-
119
-
#[derive(Serialize)]
120
-
#[serde(rename_all = "camelCase")]
121
-
pub struct RepoInfo {
122
-
pub did: String,
123
-
pub head: String,
124
-
pub rev: String,
125
-
pub active: bool,
126
-
}
127
-
128
-
#[derive(Serialize)]
129
-
pub struct ListReposOutput {
130
-
pub cursor: Option<String>,
131
-
pub repos: Vec<RepoInfo>,
132
-
}
133
-
134
-
pub async fn list_repos(
135
-
State(state): State<AppState>,
136
-
Query(params): Query<ListReposParams>,
137
-
) -> Response {
138
-
let limit = params.limit.unwrap_or(50).min(1000);
139
-
let cursor_did = params.cursor.as_deref().unwrap_or("");
140
-
141
-
let result = sqlx::query!(
142
-
r#"
143
-
SELECT u.did, r.repo_root_cid
144
-
FROM repos r
145
-
JOIN users u ON r.user_id = u.id
146
-
WHERE u.did > $1
147
-
ORDER BY u.did ASC
148
-
LIMIT $2
149
-
"#,
150
-
cursor_did,
151
-
limit + 1
152
-
)
153
-
.fetch_all(&state.db)
154
-
.await;
155
-
156
-
match result {
157
-
Ok(rows) => {
158
-
let has_more = rows.len() as i64 > limit;
159
-
let repos: Vec<RepoInfo> = rows
160
-
.iter()
161
-
.take(limit as usize)
162
-
.map(|row| {
163
-
RepoInfo {
164
-
did: row.did.clone(),
165
-
head: row.repo_root_cid.clone(),
166
-
rev: chrono::Utc::now().timestamp_millis().to_string(),
167
-
active: true,
168
-
}
169
-
})
170
-
.collect();
171
-
172
-
let next_cursor = if has_more {
173
-
repos.last().map(|r| r.did.clone())
174
-
} else {
175
-
None
176
-
};
177
-
178
-
(
179
-
StatusCode::OK,
180
-
Json(ListReposOutput {
181
-
cursor: next_cursor,
182
-
repos,
183
-
}),
184
-
)
185
-
.into_response()
186
-
}
187
-
Err(e) => {
188
-
error!("DB error in list_repos: {:?}", e);
189
-
(
190
-
StatusCode::INTERNAL_SERVER_ERROR,
191
-
Json(json!({"error": "InternalError"})),
192
-
)
193
-
.into_response()
194
-
}
195
-
}
196
-
}
197
-
198
-
#[derive(Deserialize)]
199
-
pub struct GetBlobParams {
200
-
pub did: String,
201
-
pub cid: String,
202
-
}
203
-
204
-
pub async fn get_blob(
205
-
State(state): State<AppState>,
206
-
Query(params): Query<GetBlobParams>,
207
-
) -> Response {
208
-
let did = params.did.trim();
209
-
let cid = params.cid.trim();
210
-
211
-
if did.is_empty() {
212
-
return (
213
-
StatusCode::BAD_REQUEST,
214
-
Json(json!({"error": "InvalidRequest", "message": "did is required"})),
215
-
)
216
-
.into_response();
217
-
}
218
-
219
-
if cid.is_empty() {
220
-
return (
221
-
StatusCode::BAD_REQUEST,
222
-
Json(json!({"error": "InvalidRequest", "message": "cid is required"})),
223
-
)
224
-
.into_response();
225
-
}
226
-
227
-
let user_exists = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
228
-
.fetch_optional(&state.db)
229
-
.await;
230
-
231
-
match user_exists {
232
-
Ok(None) => {
233
-
return (
234
-
StatusCode::NOT_FOUND,
235
-
Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})),
236
-
)
237
-
.into_response();
238
-
}
239
-
Err(e) => {
240
-
error!("DB error in get_blob: {:?}", e);
241
-
return (
242
-
StatusCode::INTERNAL_SERVER_ERROR,
243
-
Json(json!({"error": "InternalError"})),
244
-
)
245
-
.into_response();
246
-
}
247
-
Ok(Some(_)) => {}
248
-
}
249
-
250
-
let blob_result = sqlx::query!("SELECT storage_key, mime_type FROM blobs WHERE cid = $1", cid)
251
-
.fetch_optional(&state.db)
252
-
.await;
253
-
254
-
match blob_result {
255
-
Ok(Some(row)) => {
256
-
let storage_key = &row.storage_key;
257
-
let mime_type = &row.mime_type;
258
-
259
-
match state.blob_store.get(&storage_key).await {
260
-
Ok(data) => Response::builder()
261
-
.status(StatusCode::OK)
262
-
.header(header::CONTENT_TYPE, mime_type)
263
-
.body(Body::from(data))
264
-
.unwrap(),
265
-
Err(e) => {
266
-
error!("Failed to fetch blob from storage: {:?}", e);
267
-
(
268
-
StatusCode::NOT_FOUND,
269
-
Json(json!({"error": "BlobNotFound", "message": "Blob not found in storage"})),
270
-
)
271
-
.into_response()
272
-
}
273
-
}
274
-
}
275
-
Ok(None) => (
276
-
StatusCode::NOT_FOUND,
277
-
Json(json!({"error": "BlobNotFound", "message": "Blob not found"})),
278
-
)
279
-
.into_response(),
280
-
Err(e) => {
281
-
error!("DB error in get_blob: {:?}", e);
282
-
(
283
-
StatusCode::INTERNAL_SERVER_ERROR,
284
-
Json(json!({"error": "InternalError"})),
285
-
)
286
-
.into_response()
287
-
}
288
-
}
289
-
}
290
291
-
#[derive(Deserialize)]
292
-
pub struct ListBlobsParams {
293
-
pub did: String,
294
-
pub since: Option<String>,
295
-
pub limit: Option<i64>,
296
-
pub cursor: Option<String>,
297
-
}
298
-
299
-
#[derive(Serialize)]
300
-
pub struct ListBlobsOutput {
301
-
pub cursor: Option<String>,
302
-
pub cids: Vec<String>,
303
-
}
304
-
305
-
pub async fn list_blobs(
306
-
State(state): State<AppState>,
307
-
Query(params): Query<ListBlobsParams>,
308
-
) -> Response {
309
-
let did = params.did.trim();
310
-
311
-
if did.is_empty() {
312
-
return (
313
-
StatusCode::BAD_REQUEST,
314
-
Json(json!({"error": "InvalidRequest", "message": "did is required"})),
315
-
)
316
-
.into_response();
317
-
}
318
-
319
-
let limit = params.limit.unwrap_or(500).min(1000);
320
-
let cursor_cid = params.cursor.as_deref().unwrap_or("");
321
-
322
-
let user_result = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
323
-
.fetch_optional(&state.db)
324
-
.await;
325
-
326
-
let user_id = match user_result {
327
-
Ok(Some(row)) => row.id,
328
-
Ok(None) => {
329
-
return (
330
-
StatusCode::NOT_FOUND,
331
-
Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})),
332
-
)
333
-
.into_response();
334
-
}
335
-
Err(e) => {
336
-
error!("DB error in list_blobs: {:?}", e);
337
-
return (
338
-
StatusCode::INTERNAL_SERVER_ERROR,
339
-
Json(json!({"error": "InternalError"})),
340
-
)
341
-
.into_response();
342
-
}
343
-
};
344
-
345
-
let cids_result: Result<Vec<String>, sqlx::Error> = if let Some(since) = ¶ms.since {
346
-
let since_time = chrono::DateTime::parse_from_rfc3339(since)
347
-
.map(|dt| dt.with_timezone(&chrono::Utc))
348
-
.unwrap_or_else(|_| chrono::Utc::now());
349
-
sqlx::query!(
350
-
r#"
351
-
SELECT cid FROM blobs
352
-
WHERE created_by_user = $1 AND cid > $2 AND created_at > $3
353
-
ORDER BY cid ASC
354
-
LIMIT $4
355
-
"#,
356
-
user_id,
357
-
cursor_cid,
358
-
since_time,
359
-
limit + 1
360
-
)
361
-
.fetch_all(&state.db)
362
-
.await
363
-
.map(|rows| rows.into_iter().map(|r| r.cid).collect())
364
-
} else {
365
-
sqlx::query!(
366
-
r#"
367
-
SELECT cid FROM blobs
368
-
WHERE created_by_user = $1 AND cid > $2
369
-
ORDER BY cid ASC
370
-
LIMIT $3
371
-
"#,
372
-
user_id,
373
-
cursor_cid,
374
-
limit + 1
375
-
)
376
-
.fetch_all(&state.db)
377
-
.await
378
-
.map(|rows| rows.into_iter().map(|r| r.cid).collect())
379
-
};
380
-
381
-
match cids_result {
382
-
Ok(cids) => {
383
-
let has_more = cids.len() as i64 > limit;
384
-
let cids: Vec<String> = cids
385
-
.into_iter()
386
-
.take(limit as usize)
387
-
.collect();
388
-
389
-
let next_cursor = if has_more {
390
-
cids.last().cloned()
391
-
} else {
392
-
None
393
-
};
394
-
395
-
(
396
-
StatusCode::OK,
397
-
Json(ListBlobsOutput {
398
-
cursor: next_cursor,
399
-
cids,
400
-
}),
401
-
)
402
-
.into_response()
403
-
}
404
-
Err(e) => {
405
-
error!("DB error in list_blobs: {:?}", e);
406
-
(
407
-
StatusCode::INTERNAL_SERVER_ERROR,
408
-
Json(json!({"error": "InternalError"})),
409
-
)
410
-
.into_response()
411
-
}
412
-
}
413
-
}
414
-
415
-
#[derive(Deserialize)]
416
-
pub struct GetRepoStatusParams {
417
-
pub did: String,
418
-
}
419
-
420
-
#[derive(Serialize)]
421
-
pub struct GetRepoStatusOutput {
422
-
pub did: String,
423
-
pub active: bool,
424
-
pub rev: Option<String>,
425
-
}
426
-
427
-
pub async fn get_repo_status(
428
-
State(state): State<AppState>,
429
-
Query(params): Query<GetRepoStatusParams>,
430
-
) -> Response {
431
-
let did = params.did.trim();
432
-
433
-
if did.is_empty() {
434
-
return (
435
-
StatusCode::BAD_REQUEST,
436
-
Json(json!({"error": "InvalidRequest", "message": "did is required"})),
437
-
)
438
-
.into_response();
439
-
}
440
-
441
-
let result = sqlx::query!(
442
-
r#"
443
-
SELECT u.did, r.repo_root_cid
444
-
FROM users u
445
-
LEFT JOIN repos r ON u.id = r.user_id
446
-
WHERE u.did = $1
447
-
"#,
448
-
did
449
-
)
450
-
.fetch_optional(&state.db)
451
-
.await;
452
-
453
-
match result {
454
-
Ok(Some(row)) => {
455
-
let rev = Some(chrono::Utc::now().timestamp_millis().to_string());
456
-
457
-
(
458
-
StatusCode::OK,
459
-
Json(GetRepoStatusOutput {
460
-
did: row.did,
461
-
active: true,
462
-
rev,
463
-
}),
464
-
)
465
-
.into_response()
466
-
}
467
-
Ok(None) => (
468
-
StatusCode::NOT_FOUND,
469
-
Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})),
470
-
)
471
-
.into_response(),
472
-
Err(e) => {
473
-
error!("DB error in get_repo_status: {:?}", e);
474
-
(
475
-
StatusCode::INTERNAL_SERVER_ERROR,
476
-
Json(json!({"error": "InternalError"})),
477
-
)
478
-
.into_response()
479
-
}
480
-
}
481
-
}
482
-
483
-
#[derive(Deserialize)]
484
-
pub struct NotifyOfUpdateParams {
485
-
pub hostname: String,
486
-
}
487
-
488
-
pub async fn notify_of_update(
489
-
State(_state): State<AppState>,
490
-
Query(params): Query<NotifyOfUpdateParams>,
491
-
) -> Response {
492
-
info!("Received notifyOfUpdate from hostname: {}", params.hostname);
493
-
// TODO: Queue job for crawler interaction or relay notification
494
-
info!("TODO: Queue job for notifyOfUpdate (not implemented)");
495
-
496
-
(StatusCode::OK, Json(json!({}))).into_response()
497
-
}
498
-
499
-
#[derive(Deserialize)]
500
-
pub struct RequestCrawlInput {
501
-
pub hostname: String,
502
-
}
503
-
504
-
pub async fn request_crawl(
505
-
State(_state): State<AppState>,
506
-
Json(input): Json<RequestCrawlInput>,
507
-
) -> Response {
508
-
info!("Received requestCrawl for hostname: {}", input.hostname);
509
-
info!("TODO: Queue job for requestCrawl (not implemented)");
510
-
511
-
(StatusCode::OK, Json(json!({}))).into_response()
512
-
}
513
-
514
-
#[derive(Deserialize)]
515
-
pub struct GetBlocksParams {
516
-
pub did: String,
517
-
pub cids: String,
518
-
}
519
-
520
-
pub async fn get_blocks(
521
-
State(state): State<AppState>,
522
-
Query(params): Query<GetBlocksParams>,
523
-
) -> Response {
524
-
let did = params.did.trim();
525
-
526
-
if did.is_empty() {
527
-
return (
528
-
StatusCode::BAD_REQUEST,
529
-
Json(json!({"error": "InvalidRequest", "message": "did is required"})),
530
-
)
531
-
.into_response();
532
-
}
533
-
534
-
let cid_strings: Vec<&str> = params.cids.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect();
535
-
536
-
if cid_strings.is_empty() {
537
-
return (
538
-
StatusCode::BAD_REQUEST,
539
-
Json(json!({"error": "InvalidRequest", "message": "cids is required"})),
540
-
)
541
-
.into_response();
542
-
}
543
-
544
-
let repo_result = sqlx::query!(
545
-
r#"
546
-
SELECT r.repo_root_cid
547
-
FROM repos r
548
-
JOIN users u ON r.user_id = u.id
549
-
WHERE u.did = $1
550
-
"#,
551
-
did
552
-
)
553
-
.fetch_optional(&state.db)
554
-
.await;
555
-
556
-
let repo_root_cid_str = match repo_result {
557
-
Ok(Some(row)) => row.repo_root_cid,
558
-
Ok(None) => {
559
-
return (
560
-
StatusCode::NOT_FOUND,
561
-
Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})),
562
-
)
563
-
.into_response();
564
-
}
565
-
Err(e) => {
566
-
error!("DB error in get_blocks: {:?}", e);
567
-
return (
568
-
StatusCode::INTERNAL_SERVER_ERROR,
569
-
Json(json!({"error": "InternalError"})),
570
-
)
571
-
.into_response();
572
-
}
573
-
};
574
-
575
-
let root_cid = match repo_root_cid_str.parse::<Cid>() {
576
-
Ok(c) => c,
577
-
Err(e) => {
578
-
error!("Failed to parse root CID: {:?}", e);
579
-
return (
580
-
StatusCode::INTERNAL_SERVER_ERROR,
581
-
Json(json!({"error": "InternalError"})),
582
-
)
583
-
.into_response();
584
-
}
585
-
};
586
-
587
-
let mut requested_cids: Vec<Cid> = Vec::new();
588
-
for cid_str in &cid_strings {
589
-
match cid_str.parse::<Cid>() {
590
-
Ok(c) => requested_cids.push(c),
591
-
Err(e) => {
592
-
error!("Failed to parse CID '{}': {:?}", cid_str, e);
593
-
return (
594
-
StatusCode::BAD_REQUEST,
595
-
Json(json!({"error": "InvalidRequest", "message": format!("Invalid CID: {}", cid_str)})),
596
-
)
597
-
.into_response();
598
-
}
599
-
}
600
-
}
601
-
602
-
let mut buf = Vec::new();
603
-
let header = encode_car_header(&root_cid);
604
-
if let Err(e) = ld_write(&mut buf, &header) {
605
-
error!("Failed to write CAR header: {:?}", e);
606
-
return (
607
-
StatusCode::INTERNAL_SERVER_ERROR,
608
-
Json(json!({"error": "InternalError"})),
609
-
)
610
-
.into_response();
611
-
}
612
-
613
-
for cid in &requested_cids {
614
-
let cid_bytes = cid.to_bytes();
615
-
let block_result = sqlx::query!(
616
-
"SELECT data FROM blocks WHERE cid = $1",
617
-
&cid_bytes
618
-
)
619
-
.fetch_optional(&state.db)
620
-
.await;
621
-
622
-
match block_result {
623
-
Ok(Some(row)) => {
624
-
let mut block_data = Vec::new();
625
-
block_data.extend_from_slice(&cid_bytes);
626
-
block_data.extend_from_slice(&row.data);
627
-
if let Err(e) = ld_write(&mut buf, &block_data) {
628
-
error!("Failed to write block: {:?}", e);
629
-
return (
630
-
StatusCode::INTERNAL_SERVER_ERROR,
631
-
Json(json!({"error": "InternalError"})),
632
-
)
633
-
.into_response();
634
-
}
635
-
}
636
-
Ok(None) => {
637
-
return (
638
-
StatusCode::NOT_FOUND,
639
-
Json(json!({"error": "BlockNotFound", "message": format!("Block not found: {}", cid)})),
640
-
)
641
-
.into_response();
642
-
}
643
-
Err(e) => {
644
-
error!("DB error fetching block: {:?}", e);
645
-
return (
646
-
StatusCode::INTERNAL_SERVER_ERROR,
647
-
Json(json!({"error": "InternalError"})),
648
-
)
649
-
.into_response();
650
-
}
651
-
}
652
-
}
653
-
654
-
Response::builder()
655
-
.status(StatusCode::OK)
656
-
.header(header::CONTENT_TYPE, "application/vnd.ipld.car")
657
-
.body(Body::from(buf))
658
-
.unwrap()
659
-
}
660
-
661
-
#[derive(Deserialize)]
662
-
pub struct GetRepoParams {
663
-
pub did: String,
664
-
pub since: Option<String>,
665
-
}
666
-
667
-
pub async fn get_repo(
668
-
State(state): State<AppState>,
669
-
Query(params): Query<GetRepoParams>,
670
-
) -> Response {
671
-
let did = params.did.trim();
672
-
673
-
if did.is_empty() {
674
-
return (
675
-
StatusCode::BAD_REQUEST,
676
-
Json(json!({"error": "InvalidRequest", "message": "did is required"})),
677
-
)
678
-
.into_response();
679
-
}
680
-
681
-
let user_result = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
682
-
.fetch_optional(&state.db)
683
-
.await;
684
-
685
-
let user_id = match user_result {
686
-
Ok(Some(row)) => row.id,
687
-
Ok(None) => {
688
-
return (
689
-
StatusCode::NOT_FOUND,
690
-
Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})),
691
-
)
692
-
.into_response();
693
-
}
694
-
Err(e) => {
695
-
error!("DB error in get_repo: {:?}", e);
696
-
return (
697
-
StatusCode::INTERNAL_SERVER_ERROR,
698
-
Json(json!({"error": "InternalError"})),
699
-
)
700
-
.into_response();
701
-
}
702
-
};
703
-
704
-
let repo_result = sqlx::query!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id)
705
-
.fetch_optional(&state.db)
706
-
.await;
707
-
708
-
let repo_root_cid_str = match repo_result {
709
-
Ok(Some(row)) => row.repo_root_cid,
710
-
Ok(None) => {
711
-
return (
712
-
StatusCode::NOT_FOUND,
713
-
Json(json!({"error": "RepoNotFound", "message": "Repository not initialized"})),
714
-
)
715
-
.into_response();
716
-
}
717
-
Err(e) => {
718
-
error!("DB error in get_repo: {:?}", e);
719
-
return (
720
-
StatusCode::INTERNAL_SERVER_ERROR,
721
-
Json(json!({"error": "InternalError"})),
722
-
)
723
-
.into_response();
724
-
}
725
-
};
726
-
727
-
let root_cid = match repo_root_cid_str.parse::<Cid>() {
728
-
Ok(c) => c,
729
-
Err(e) => {
730
-
error!("Failed to parse root CID: {:?}", e);
731
-
return (
732
-
StatusCode::INTERNAL_SERVER_ERROR,
733
-
Json(json!({"error": "InternalError"})),
734
-
)
735
-
.into_response();
736
-
}
737
-
};
738
-
739
-
let commit_bytes = match state.block_store.get(&root_cid).await {
740
-
Ok(Some(b)) => b,
741
-
Ok(None) => {
742
-
error!("Commit block not found: {}", root_cid);
743
-
return (
744
-
StatusCode::INTERNAL_SERVER_ERROR,
745
-
Json(json!({"error": "InternalError"})),
746
-
)
747
-
.into_response();
748
-
}
749
-
Err(e) => {
750
-
error!("Failed to load commit block: {:?}", e);
751
-
return (
752
-
StatusCode::INTERNAL_SERVER_ERROR,
753
-
Json(json!({"error": "InternalError"})),
754
-
)
755
-
.into_response();
756
-
}
757
-
};
758
-
759
-
let commit = match Commit::from_cbor(&commit_bytes) {
760
-
Ok(c) => c,
761
-
Err(e) => {
762
-
error!("Failed to parse commit: {:?}", e);
763
-
return (
764
-
StatusCode::INTERNAL_SERVER_ERROR,
765
-
Json(json!({"error": "InternalError"})),
766
-
)
767
-
.into_response();
768
-
}
769
-
};
770
-
771
-
let mut collected_blocks: Vec<(Cid, Bytes)> = Vec::new();
772
-
let mut visited: HashSet<Vec<u8>> = HashSet::new();
773
-
774
-
collected_blocks.push((root_cid, commit_bytes.clone()));
775
-
visited.insert(root_cid.to_bytes());
776
-
777
-
let mst_root_cid = commit.data;
778
-
if !visited.contains(&mst_root_cid.to_bytes()) {
779
-
visited.insert(mst_root_cid.to_bytes());
780
-
if let Ok(Some(data)) = state.block_store.get(&mst_root_cid).await {
781
-
collected_blocks.push((mst_root_cid, data));
782
-
}
783
-
}
784
-
785
-
let records = sqlx::query!("SELECT record_cid FROM records WHERE repo_id = $1", user_id)
786
-
.fetch_all(&state.db)
787
-
.await
788
-
.unwrap_or_default();
789
-
790
-
for record in records {
791
-
if let Ok(cid) = record.record_cid.parse::<Cid>() {
792
-
if !visited.contains(&cid.to_bytes()) {
793
-
visited.insert(cid.to_bytes());
794
-
if let Ok(Some(data)) = state.block_store.get(&cid).await {
795
-
collected_blocks.push((cid, data));
796
-
}
797
-
}
798
-
}
799
-
}
800
-
801
-
let mut buf = Vec::new();
802
-
let header = encode_car_header(&root_cid);
803
-
if let Err(e) = ld_write(&mut buf, &header) {
804
-
error!("Failed to write CAR header: {:?}", e);
805
-
return (
806
-
StatusCode::INTERNAL_SERVER_ERROR,
807
-
Json(json!({"error": "InternalError"})),
808
-
)
809
-
.into_response();
810
-
}
811
-
812
-
for (cid, data) in &collected_blocks {
813
-
let mut block_data = Vec::new();
814
-
block_data.extend_from_slice(&cid.to_bytes());
815
-
block_data.extend_from_slice(data);
816
-
if let Err(e) = ld_write(&mut buf, &block_data) {
817
-
error!("Failed to write block: {:?}", e);
818
-
return (
819
-
StatusCode::INTERNAL_SERVER_ERROR,
820
-
Json(json!({"error": "InternalError"})),
821
-
)
822
-
.into_response();
823
-
}
824
-
}
825
-
826
-
Response::builder()
827
-
.status(StatusCode::OK)
828
-
.header(header::CONTENT_TYPE, "application/vnd.ipld.car")
829
-
.body(Body::from(buf))
830
-
.unwrap()
831
-
}
832
-
833
-
#[derive(Deserialize)]
834
-
pub struct GetRecordParams {
835
-
pub did: String,
836
-
pub collection: String,
837
-
pub rkey: String,
838
-
}
839
-
840
-
pub async fn get_record(
841
-
State(state): State<AppState>,
842
-
Query(params): Query<GetRecordParams>,
843
-
) -> Response {
844
-
let did = params.did.trim();
845
-
let collection = params.collection.trim();
846
-
let rkey = params.rkey.trim();
847
-
848
-
if did.is_empty() {
849
-
return (
850
-
StatusCode::BAD_REQUEST,
851
-
Json(json!({"error": "InvalidRequest", "message": "did is required"})),
852
-
)
853
-
.into_response();
854
-
}
855
-
856
-
if collection.is_empty() {
857
-
return (
858
-
StatusCode::BAD_REQUEST,
859
-
Json(json!({"error": "InvalidRequest", "message": "collection is required"})),
860
-
)
861
-
.into_response();
862
-
}
863
-
864
-
if rkey.is_empty() {
865
-
return (
866
-
StatusCode::BAD_REQUEST,
867
-
Json(json!({"error": "InvalidRequest", "message": "rkey is required"})),
868
-
)
869
-
.into_response();
870
-
}
871
-
872
-
let user_result = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
873
-
.fetch_optional(&state.db)
874
-
.await;
875
-
876
-
let user_id = match user_result {
877
-
Ok(Some(row)) => row.id,
878
-
Ok(None) => {
879
-
return (
880
-
StatusCode::NOT_FOUND,
881
-
Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})),
882
-
)
883
-
.into_response();
884
-
}
885
-
Err(e) => {
886
-
error!("DB error in sync get_record: {:?}", e);
887
-
return (
888
-
StatusCode::INTERNAL_SERVER_ERROR,
889
-
Json(json!({"error": "InternalError"})),
890
-
)
891
-
.into_response();
892
-
}
893
-
};
894
-
895
-
let record_result = sqlx::query!(
896
-
"SELECT record_cid FROM records WHERE repo_id = $1 AND collection = $2 AND rkey = $3",
897
-
user_id,
898
-
collection,
899
-
rkey
900
-
)
901
-
.fetch_optional(&state.db)
902
-
.await;
903
-
904
-
let record_cid_str = match record_result {
905
-
Ok(Some(row)) => row.record_cid,
906
-
Ok(None) => {
907
-
return (
908
-
StatusCode::NOT_FOUND,
909
-
Json(json!({"error": "RecordNotFound", "message": "Record not found"})),
910
-
)
911
-
.into_response();
912
-
}
913
-
Err(e) => {
914
-
error!("DB error in sync get_record: {:?}", e);
915
-
return (
916
-
StatusCode::INTERNAL_SERVER_ERROR,
917
-
Json(json!({"error": "InternalError"})),
918
-
)
919
-
.into_response();
920
-
}
921
-
};
922
-
923
-
let record_cid = match record_cid_str.parse::<Cid>() {
924
-
Ok(c) => c,
925
-
Err(e) => {
926
-
error!("Failed to parse record CID: {:?}", e);
927
-
return (
928
-
StatusCode::INTERNAL_SERVER_ERROR,
929
-
Json(json!({"error": "InternalError"})),
930
-
)
931
-
.into_response();
932
-
}
933
-
};
934
-
935
-
let repo_result = sqlx::query!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id)
936
-
.fetch_optional(&state.db)
937
-
.await;
938
-
939
-
let repo_root_cid_str = match repo_result {
940
-
Ok(Some(row)) => row.repo_root_cid,
941
-
Ok(None) => {
942
-
return (
943
-
StatusCode::NOT_FOUND,
944
-
Json(json!({"error": "RepoNotFound", "message": "Repository not initialized"})),
945
-
)
946
-
.into_response();
947
-
}
948
-
Err(e) => {
949
-
error!("DB error in sync get_record: {:?}", e);
950
-
return (
951
-
StatusCode::INTERNAL_SERVER_ERROR,
952
-
Json(json!({"error": "InternalError"})),
953
-
)
954
-
.into_response();
955
-
}
956
-
};
957
-
958
-
let root_cid = match repo_root_cid_str.parse::<Cid>() {
959
-
Ok(c) => c,
960
-
Err(e) => {
961
-
error!("Failed to parse root CID: {:?}", e);
962
-
return (
963
-
StatusCode::INTERNAL_SERVER_ERROR,
964
-
Json(json!({"error": "InternalError"})),
965
-
)
966
-
.into_response();
967
-
}
968
-
};
969
-
970
-
let mut collected_blocks: Vec<(Cid, Bytes)> = Vec::new();
971
-
972
-
let commit_bytes = match state.block_store.get(&root_cid).await {
973
-
Ok(Some(b)) => b,
974
-
Ok(None) => {
975
-
error!("Commit block not found: {}", root_cid);
976
-
return (
977
-
StatusCode::INTERNAL_SERVER_ERROR,
978
-
Json(json!({"error": "InternalError"})),
979
-
)
980
-
.into_response();
981
-
}
982
-
Err(e) => {
983
-
error!("Failed to load commit block: {:?}", e);
984
-
return (
985
-
StatusCode::INTERNAL_SERVER_ERROR,
986
-
Json(json!({"error": "InternalError"})),
987
-
)
988
-
.into_response();
989
-
}
990
-
};
991
-
992
-
collected_blocks.push((root_cid, commit_bytes.clone()));
993
-
994
-
let commit = match Commit::from_cbor(&commit_bytes) {
995
-
Ok(c) => c,
996
-
Err(e) => {
997
-
error!("Failed to parse commit: {:?}", e);
998
-
return (
999
-
StatusCode::INTERNAL_SERVER_ERROR,
1000
-
Json(json!({"error": "InternalError"})),
1001
-
)
1002
-
.into_response();
1003
-
}
1004
-
};
1005
-
1006
-
let mst_root_cid = commit.data;
1007
-
if let Ok(Some(data)) = state.block_store.get(&mst_root_cid).await {
1008
-
collected_blocks.push((mst_root_cid, data));
1009
-
}
1010
-
1011
-
if let Ok(Some(data)) = state.block_store.get(&record_cid).await {
1012
-
collected_blocks.push((record_cid, data));
1013
-
} else {
1014
-
return (
1015
-
StatusCode::NOT_FOUND,
1016
-
Json(json!({"error": "RecordNotFound", "message": "Record block not found"})),
1017
-
)
1018
-
.into_response();
1019
-
}
1020
-
1021
-
let mut buf = Vec::new();
1022
-
let header = encode_car_header(&root_cid);
1023
-
if let Err(e) = ld_write(&mut buf, &header) {
1024
-
error!("Failed to write CAR header: {:?}", e);
1025
-
return (
1026
-
StatusCode::INTERNAL_SERVER_ERROR,
1027
-
Json(json!({"error": "InternalError"})),
1028
-
)
1029
-
.into_response();
1030
-
}
1031
-
1032
-
for (cid, data) in &collected_blocks {
1033
-
let mut block_data = Vec::new();
1034
-
block_data.extend_from_slice(&cid.to_bytes());
1035
-
block_data.extend_from_slice(data);
1036
-
if let Err(e) = ld_write(&mut buf, &block_data) {
1037
-
error!("Failed to write block: {:?}", e);
1038
-
return (
1039
-
StatusCode::INTERNAL_SERVER_ERROR,
1040
-
Json(json!({"error": "InternalError"})),
1041
-
)
1042
-
.into_response();
1043
-
}
1044
-
}
1045
-
1046
-
Response::builder()
1047
-
.status(StatusCode::OK)
1048
-
.header(header::CONTENT_TYPE, "application/vnd.ipld.car")
1049
-
.body(Body::from(buf))
1050
-
.unwrap()
1051
-
}
···
1
+
pub mod blob;
2
+
pub mod car;
3
+
pub mod commit;
4
+
pub mod crawl;
5
+
pub mod repo;
6
7
+
pub use blob::{get_blob, list_blobs};
8
+
pub use commit::{get_latest_commit, get_repo_status, list_repos};
9
+
pub use crawl::{notify_of_update, request_crawl};
10
+
pub use repo::{get_blocks, get_record, get_repo};
+556
src/sync/repo.rs
+556
src/sync/repo.rs
···
···
1
+
use crate::state::AppState;
2
+
use crate::sync::car::{encode_car_header, ld_write};
3
+
use axum::{
4
+
Json,
5
+
body::Body,
6
+
extract::{Query, State},
7
+
http::StatusCode,
8
+
http::header,
9
+
response::{IntoResponse, Response},
10
+
};
11
+
use bytes::Bytes;
12
+
use cid::Cid;
13
+
use jacquard_repo::{commit::Commit, storage::BlockStore};
14
+
use serde::Deserialize;
15
+
use serde_json::json;
16
+
use std::collections::HashSet;
17
+
use tracing::error;
18
+
19
+
#[derive(Deserialize)]
20
+
pub struct GetBlocksParams {
21
+
pub did: String,
22
+
pub cids: String,
23
+
}
24
+
25
+
pub async fn get_blocks(
26
+
State(state): State<AppState>,
27
+
Query(params): Query<GetBlocksParams>,
28
+
) -> Response {
29
+
let did = params.did.trim();
30
+
31
+
if did.is_empty() {
32
+
return (
33
+
StatusCode::BAD_REQUEST,
34
+
Json(json!({"error": "InvalidRequest", "message": "did is required"})),
35
+
)
36
+
.into_response();
37
+
}
38
+
39
+
let cid_strings: Vec<&str> = params.cids.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect();
40
+
41
+
if cid_strings.is_empty() {
42
+
return (
43
+
StatusCode::BAD_REQUEST,
44
+
Json(json!({"error": "InvalidRequest", "message": "cids is required"})),
45
+
)
46
+
.into_response();
47
+
}
48
+
49
+
let repo_result = sqlx::query!(
50
+
r#"
51
+
SELECT r.repo_root_cid
52
+
FROM repos r
53
+
JOIN users u ON r.user_id = u.id
54
+
WHERE u.did = $1
55
+
"#,
56
+
did
57
+
)
58
+
.fetch_optional(&state.db)
59
+
.await;
60
+
61
+
let repo_root_cid_str = match repo_result {
62
+
Ok(Some(row)) => row.repo_root_cid,
63
+
Ok(None) => {
64
+
return (
65
+
StatusCode::NOT_FOUND,
66
+
Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})),
67
+
)
68
+
.into_response();
69
+
}
70
+
Err(e) => {
71
+
error!("DB error in get_blocks: {:?}", e);
72
+
return (
73
+
StatusCode::INTERNAL_SERVER_ERROR,
74
+
Json(json!({"error": "InternalError"})),
75
+
)
76
+
.into_response();
77
+
}
78
+
};
79
+
80
+
let root_cid = match repo_root_cid_str.parse::<Cid>() {
81
+
Ok(c) => c,
82
+
Err(e) => {
83
+
error!("Failed to parse root CID: {:?}", e);
84
+
return (
85
+
StatusCode::INTERNAL_SERVER_ERROR,
86
+
Json(json!({"error": "InternalError"})),
87
+
)
88
+
.into_response();
89
+
}
90
+
};
91
+
92
+
let mut requested_cids: Vec<Cid> = Vec::new();
93
+
for cid_str in &cid_strings {
94
+
match cid_str.parse::<Cid>() {
95
+
Ok(c) => requested_cids.push(c),
96
+
Err(e) => {
97
+
error!("Failed to parse CID '{}': {:?}", cid_str, e);
98
+
return (
99
+
StatusCode::BAD_REQUEST,
100
+
Json(json!({"error": "InvalidRequest", "message": format!("Invalid CID: {}", cid_str)})),
101
+
)
102
+
.into_response();
103
+
}
104
+
}
105
+
}
106
+
107
+
let mut buf = Vec::new();
108
+
let car_header = encode_car_header(&root_cid);
109
+
if let Err(e) = ld_write(&mut buf, &car_header) {
110
+
error!("Failed to write CAR header: {:?}", e);
111
+
return (
112
+
StatusCode::INTERNAL_SERVER_ERROR,
113
+
Json(json!({"error": "InternalError"})),
114
+
)
115
+
.into_response();
116
+
}
117
+
118
+
for cid in &requested_cids {
119
+
let cid_bytes = cid.to_bytes();
120
+
let block_result = sqlx::query!(
121
+
"SELECT data FROM blocks WHERE cid = $1",
122
+
&cid_bytes
123
+
)
124
+
.fetch_optional(&state.db)
125
+
.await;
126
+
127
+
match block_result {
128
+
Ok(Some(row)) => {
129
+
let mut block_data = Vec::new();
130
+
block_data.extend_from_slice(&cid_bytes);
131
+
block_data.extend_from_slice(&row.data);
132
+
if let Err(e) = ld_write(&mut buf, &block_data) {
133
+
error!("Failed to write block: {:?}", e);
134
+
return (
135
+
StatusCode::INTERNAL_SERVER_ERROR,
136
+
Json(json!({"error": "InternalError"})),
137
+
)
138
+
.into_response();
139
+
}
140
+
}
141
+
Ok(None) => {
142
+
return (
143
+
StatusCode::NOT_FOUND,
144
+
Json(json!({"error": "BlockNotFound", "message": format!("Block not found: {}", cid)})),
145
+
)
146
+
.into_response();
147
+
}
148
+
Err(e) => {
149
+
error!("DB error fetching block: {:?}", e);
150
+
return (
151
+
StatusCode::INTERNAL_SERVER_ERROR,
152
+
Json(json!({"error": "InternalError"})),
153
+
)
154
+
.into_response();
155
+
}
156
+
}
157
+
}
158
+
159
+
Response::builder()
160
+
.status(StatusCode::OK)
161
+
.header(header::CONTENT_TYPE, "application/vnd.ipld.car")
162
+
.body(Body::from(buf))
163
+
.unwrap()
164
+
}
165
+
166
+
#[derive(Deserialize)]
167
+
pub struct GetRepoParams {
168
+
pub did: String,
169
+
pub since: Option<String>,
170
+
}
171
+
172
+
pub async fn get_repo(
173
+
State(state): State<AppState>,
174
+
Query(params): Query<GetRepoParams>,
175
+
) -> Response {
176
+
let did = params.did.trim();
177
+
178
+
if did.is_empty() {
179
+
return (
180
+
StatusCode::BAD_REQUEST,
181
+
Json(json!({"error": "InvalidRequest", "message": "did is required"})),
182
+
)
183
+
.into_response();
184
+
}
185
+
186
+
let user_result = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
187
+
.fetch_optional(&state.db)
188
+
.await;
189
+
190
+
let user_id = match user_result {
191
+
Ok(Some(row)) => row.id,
192
+
Ok(None) => {
193
+
return (
194
+
StatusCode::NOT_FOUND,
195
+
Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})),
196
+
)
197
+
.into_response();
198
+
}
199
+
Err(e) => {
200
+
error!("DB error in get_repo: {:?}", e);
201
+
return (
202
+
StatusCode::INTERNAL_SERVER_ERROR,
203
+
Json(json!({"error": "InternalError"})),
204
+
)
205
+
.into_response();
206
+
}
207
+
};
208
+
209
+
let repo_result = sqlx::query!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id)
210
+
.fetch_optional(&state.db)
211
+
.await;
212
+
213
+
let repo_root_cid_str = match repo_result {
214
+
Ok(Some(row)) => row.repo_root_cid,
215
+
Ok(None) => {
216
+
return (
217
+
StatusCode::NOT_FOUND,
218
+
Json(json!({"error": "RepoNotFound", "message": "Repository not initialized"})),
219
+
)
220
+
.into_response();
221
+
}
222
+
Err(e) => {
223
+
error!("DB error in get_repo: {:?}", e);
224
+
return (
225
+
StatusCode::INTERNAL_SERVER_ERROR,
226
+
Json(json!({"error": "InternalError"})),
227
+
)
228
+
.into_response();
229
+
}
230
+
};
231
+
232
+
let root_cid = match repo_root_cid_str.parse::<Cid>() {
233
+
Ok(c) => c,
234
+
Err(e) => {
235
+
error!("Failed to parse root CID: {:?}", e);
236
+
return (
237
+
StatusCode::INTERNAL_SERVER_ERROR,
238
+
Json(json!({"error": "InternalError"})),
239
+
)
240
+
.into_response();
241
+
}
242
+
};
243
+
244
+
let commit_bytes = match state.block_store.get(&root_cid).await {
245
+
Ok(Some(b)) => b,
246
+
Ok(None) => {
247
+
error!("Commit block not found: {}", root_cid);
248
+
return (
249
+
StatusCode::INTERNAL_SERVER_ERROR,
250
+
Json(json!({"error": "InternalError"})),
251
+
)
252
+
.into_response();
253
+
}
254
+
Err(e) => {
255
+
error!("Failed to load commit block: {:?}", e);
256
+
return (
257
+
StatusCode::INTERNAL_SERVER_ERROR,
258
+
Json(json!({"error": "InternalError"})),
259
+
)
260
+
.into_response();
261
+
}
262
+
};
263
+
264
+
let commit = match Commit::from_cbor(&commit_bytes) {
265
+
Ok(c) => c,
266
+
Err(e) => {
267
+
error!("Failed to parse commit: {:?}", e);
268
+
return (
269
+
StatusCode::INTERNAL_SERVER_ERROR,
270
+
Json(json!({"error": "InternalError"})),
271
+
)
272
+
.into_response();
273
+
}
274
+
};
275
+
276
+
let mut collected_blocks: Vec<(Cid, Bytes)> = Vec::new();
277
+
let mut visited: HashSet<Vec<u8>> = HashSet::new();
278
+
279
+
collected_blocks.push((root_cid, commit_bytes.clone()));
280
+
visited.insert(root_cid.to_bytes());
281
+
282
+
let mst_root_cid = commit.data;
283
+
if !visited.contains(&mst_root_cid.to_bytes()) {
284
+
visited.insert(mst_root_cid.to_bytes());
285
+
if let Ok(Some(data)) = state.block_store.get(&mst_root_cid).await {
286
+
collected_blocks.push((mst_root_cid, data));
287
+
}
288
+
}
289
+
290
+
let records = sqlx::query!("SELECT record_cid FROM records WHERE repo_id = $1", user_id)
291
+
.fetch_all(&state.db)
292
+
.await
293
+
.unwrap_or_default();
294
+
295
+
for record in records {
296
+
if let Ok(cid) = record.record_cid.parse::<Cid>() {
297
+
if !visited.contains(&cid.to_bytes()) {
298
+
visited.insert(cid.to_bytes());
299
+
if let Ok(Some(data)) = state.block_store.get(&cid).await {
300
+
collected_blocks.push((cid, data));
301
+
}
302
+
}
303
+
}
304
+
}
305
+
306
+
let mut buf = Vec::new();
307
+
let car_header = encode_car_header(&root_cid);
308
+
if let Err(e) = ld_write(&mut buf, &car_header) {
309
+
error!("Failed to write CAR header: {:?}", e);
310
+
return (
311
+
StatusCode::INTERNAL_SERVER_ERROR,
312
+
Json(json!({"error": "InternalError"})),
313
+
)
314
+
.into_response();
315
+
}
316
+
317
+
for (cid, data) in &collected_blocks {
318
+
let mut block_data = Vec::new();
319
+
block_data.extend_from_slice(&cid.to_bytes());
320
+
block_data.extend_from_slice(data);
321
+
if let Err(e) = ld_write(&mut buf, &block_data) {
322
+
error!("Failed to write block: {:?}", e);
323
+
return (
324
+
StatusCode::INTERNAL_SERVER_ERROR,
325
+
Json(json!({"error": "InternalError"})),
326
+
)
327
+
.into_response();
328
+
}
329
+
}
330
+
331
+
Response::builder()
332
+
.status(StatusCode::OK)
333
+
.header(header::CONTENT_TYPE, "application/vnd.ipld.car")
334
+
.body(Body::from(buf))
335
+
.unwrap()
336
+
}
337
+
338
+
#[derive(Deserialize)]
339
+
pub struct GetRecordParams {
340
+
pub did: String,
341
+
pub collection: String,
342
+
pub rkey: String,
343
+
}
344
+
345
+
pub async fn get_record(
346
+
State(state): State<AppState>,
347
+
Query(params): Query<GetRecordParams>,
348
+
) -> Response {
349
+
let did = params.did.trim();
350
+
let collection = params.collection.trim();
351
+
let rkey = params.rkey.trim();
352
+
353
+
if did.is_empty() {
354
+
return (
355
+
StatusCode::BAD_REQUEST,
356
+
Json(json!({"error": "InvalidRequest", "message": "did is required"})),
357
+
)
358
+
.into_response();
359
+
}
360
+
361
+
if collection.is_empty() {
362
+
return (
363
+
StatusCode::BAD_REQUEST,
364
+
Json(json!({"error": "InvalidRequest", "message": "collection is required"})),
365
+
)
366
+
.into_response();
367
+
}
368
+
369
+
if rkey.is_empty() {
370
+
return (
371
+
StatusCode::BAD_REQUEST,
372
+
Json(json!({"error": "InvalidRequest", "message": "rkey is required"})),
373
+
)
374
+
.into_response();
375
+
}
376
+
377
+
let user_result = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
378
+
.fetch_optional(&state.db)
379
+
.await;
380
+
381
+
let user_id = match user_result {
382
+
Ok(Some(row)) => row.id,
383
+
Ok(None) => {
384
+
return (
385
+
StatusCode::NOT_FOUND,
386
+
Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})),
387
+
)
388
+
.into_response();
389
+
}
390
+
Err(e) => {
391
+
error!("DB error in sync get_record: {:?}", e);
392
+
return (
393
+
StatusCode::INTERNAL_SERVER_ERROR,
394
+
Json(json!({"error": "InternalError"})),
395
+
)
396
+
.into_response();
397
+
}
398
+
};
399
+
400
+
let record_result = sqlx::query!(
401
+
"SELECT record_cid FROM records WHERE repo_id = $1 AND collection = $2 AND rkey = $3",
402
+
user_id,
403
+
collection,
404
+
rkey
405
+
)
406
+
.fetch_optional(&state.db)
407
+
.await;
408
+
409
+
let record_cid_str = match record_result {
410
+
Ok(Some(row)) => row.record_cid,
411
+
Ok(None) => {
412
+
return (
413
+
StatusCode::NOT_FOUND,
414
+
Json(json!({"error": "RecordNotFound", "message": "Record not found"})),
415
+
)
416
+
.into_response();
417
+
}
418
+
Err(e) => {
419
+
error!("DB error in sync get_record: {:?}", e);
420
+
return (
421
+
StatusCode::INTERNAL_SERVER_ERROR,
422
+
Json(json!({"error": "InternalError"})),
423
+
)
424
+
.into_response();
425
+
}
426
+
};
427
+
428
+
let record_cid = match record_cid_str.parse::<Cid>() {
429
+
Ok(c) => c,
430
+
Err(e) => {
431
+
error!("Failed to parse record CID: {:?}", e);
432
+
return (
433
+
StatusCode::INTERNAL_SERVER_ERROR,
434
+
Json(json!({"error": "InternalError"})),
435
+
)
436
+
.into_response();
437
+
}
438
+
};
439
+
440
+
let repo_result = sqlx::query!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id)
441
+
.fetch_optional(&state.db)
442
+
.await;
443
+
444
+
let repo_root_cid_str = match repo_result {
445
+
Ok(Some(row)) => row.repo_root_cid,
446
+
Ok(None) => {
447
+
return (
448
+
StatusCode::NOT_FOUND,
449
+
Json(json!({"error": "RepoNotFound", "message": "Repository not initialized"})),
450
+
)
451
+
.into_response();
452
+
}
453
+
Err(e) => {
454
+
error!("DB error in sync get_record: {:?}", e);
455
+
return (
456
+
StatusCode::INTERNAL_SERVER_ERROR,
457
+
Json(json!({"error": "InternalError"})),
458
+
)
459
+
.into_response();
460
+
}
461
+
};
462
+
463
+
let root_cid = match repo_root_cid_str.parse::<Cid>() {
464
+
Ok(c) => c,
465
+
Err(e) => {
466
+
error!("Failed to parse root CID: {:?}", e);
467
+
return (
468
+
StatusCode::INTERNAL_SERVER_ERROR,
469
+
Json(json!({"error": "InternalError"})),
470
+
)
471
+
.into_response();
472
+
}
473
+
};
474
+
475
+
let mut collected_blocks: Vec<(Cid, Bytes)> = Vec::new();
476
+
477
+
let commit_bytes = match state.block_store.get(&root_cid).await {
478
+
Ok(Some(b)) => b,
479
+
Ok(None) => {
480
+
error!("Commit block not found: {}", root_cid);
481
+
return (
482
+
StatusCode::INTERNAL_SERVER_ERROR,
483
+
Json(json!({"error": "InternalError"})),
484
+
)
485
+
.into_response();
486
+
}
487
+
Err(e) => {
488
+
error!("Failed to load commit block: {:?}", e);
489
+
return (
490
+
StatusCode::INTERNAL_SERVER_ERROR,
491
+
Json(json!({"error": "InternalError"})),
492
+
)
493
+
.into_response();
494
+
}
495
+
};
496
+
497
+
collected_blocks.push((root_cid, commit_bytes.clone()));
498
+
499
+
let commit = match Commit::from_cbor(&commit_bytes) {
500
+
Ok(c) => c,
501
+
Err(e) => {
502
+
error!("Failed to parse commit: {:?}", e);
503
+
return (
504
+
StatusCode::INTERNAL_SERVER_ERROR,
505
+
Json(json!({"error": "InternalError"})),
506
+
)
507
+
.into_response();
508
+
}
509
+
};
510
+
511
+
let mst_root_cid = commit.data;
512
+
if let Ok(Some(data)) = state.block_store.get(&mst_root_cid).await {
513
+
collected_blocks.push((mst_root_cid, data));
514
+
}
515
+
516
+
if let Ok(Some(data)) = state.block_store.get(&record_cid).await {
517
+
collected_blocks.push((record_cid, data));
518
+
} else {
519
+
return (
520
+
StatusCode::NOT_FOUND,
521
+
Json(json!({"error": "RecordNotFound", "message": "Record block not found"})),
522
+
)
523
+
.into_response();
524
+
}
525
+
526
+
let mut buf = Vec::new();
527
+
let car_header = encode_car_header(&root_cid);
528
+
if let Err(e) = ld_write(&mut buf, &car_header) {
529
+
error!("Failed to write CAR header: {:?}", e);
530
+
return (
531
+
StatusCode::INTERNAL_SERVER_ERROR,
532
+
Json(json!({"error": "InternalError"})),
533
+
)
534
+
.into_response();
535
+
}
536
+
537
+
for (cid, data) in &collected_blocks {
538
+
let mut block_data = Vec::new();
539
+
block_data.extend_from_slice(&cid.to_bytes());
540
+
block_data.extend_from_slice(data);
541
+
if let Err(e) = ld_write(&mut buf, &block_data) {
542
+
error!("Failed to write block: {:?}", e);
543
+
return (
544
+
StatusCode::INTERNAL_SERVER_ERROR,
545
+
Json(json!({"error": "InternalError"})),
546
+
)
547
+
.into_response();
548
+
}
549
+
}
550
+
551
+
Response::builder()
552
+
.status(StatusCode::OK)
553
+
.header(header::CONTENT_TYPE, "application/vnd.ipld.car")
554
+
.body(Body::from(buf))
555
+
.unwrap()
556
+
}