-424
src/api/identity.rs
-424
src/api/identity.rs
···
1
-
use axum::{
2
-
extract::{State, Path},
3
-
Json,
4
-
response::{IntoResponse, Response},
5
-
http::StatusCode,
6
-
};
7
-
use serde::{Deserialize, Serialize};
8
-
use serde_json::json;
9
-
use crate::state::AppState;
10
-
use sqlx::Row;
11
-
use bcrypt::{hash, DEFAULT_COST};
12
-
use tracing::{info, error};
13
-
use jacquard_repo::{mst::Mst, commit::Commit, storage::BlockStore};
14
-
use jacquard::types::{string::Tid, did::Did, integer::LimitedU32};
15
-
use std::sync::Arc;
16
-
use k256::SecretKey;
17
-
use rand::rngs::OsRng;
18
-
use base64::Engine;
19
-
use reqwest;
20
-
21
-
#[derive(Deserialize)]
22
-
pub struct CreateAccountInput {
23
-
pub handle: String,
24
-
pub email: String,
25
-
pub password: String,
26
-
#[serde(rename = "inviteCode")]
27
-
pub invite_code: Option<String>,
28
-
pub did: Option<String>,
29
-
}
30
-
31
-
#[derive(Serialize)]
32
-
#[serde(rename_all = "camelCase")]
33
-
pub struct CreateAccountOutput {
34
-
pub access_jwt: String,
35
-
pub refresh_jwt: String,
36
-
pub handle: String,
37
-
pub did: String,
38
-
}
39
-
40
-
pub async fn create_account(
41
-
State(state): State<AppState>,
42
-
Json(input): Json<CreateAccountInput>,
43
-
) -> Response {
44
-
info!("create_account hit: {}", input.handle);
45
-
if input.handle.contains('!') || input.handle.contains('@') {
46
-
return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidHandle", "message": "Handle contains invalid characters"}))).into_response();
47
-
}
48
-
49
-
let did = if let Some(d) = &input.did {
50
-
if d.trim().is_empty() {
51
-
format!("did:plc:{}", uuid::Uuid::new_v4())
52
-
} else {
53
-
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
54
-
if let Err(e) = verify_did_web(d, &hostname, &input.handle).await {
55
-
return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidDid", "message": e}))).into_response();
56
-
}
57
-
d.clone()
58
-
}
59
-
} else {
60
-
format!("did:plc:{}", uuid::Uuid::new_v4())
61
-
};
62
-
63
-
let mut tx = match state.db.begin().await {
64
-
Ok(tx) => tx,
65
-
Err(e) => {
66
-
error!("Error starting transaction: {:?}", e);
67
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
68
-
}
69
-
};
70
-
71
-
let exists_query = sqlx::query("SELECT 1 FROM users WHERE handle = $1")
72
-
.bind(&input.handle)
73
-
.fetch_optional(&mut *tx)
74
-
.await;
75
-
76
-
match exists_query {
77
-
Ok(Some(_)) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "HandleTaken", "message": "Handle already taken"}))).into_response(),
78
-
Err(e) => {
79
-
error!("Error checking handle: {:?}", e);
80
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
81
-
}
82
-
Ok(None) => {}
83
-
}
84
-
85
-
if let Some(code) = &input.invite_code {
86
-
let invite_query = sqlx::query("SELECT available_uses FROM invite_codes WHERE code = $1 FOR UPDATE")
87
-
.bind(code)
88
-
.fetch_optional(&mut *tx)
89
-
.await;
90
-
91
-
match invite_query {
92
-
Ok(Some(row)) => {
93
-
let uses: i32 = row.get("available_uses");
94
-
if uses <= 0 {
95
-
return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidInviteCode", "message": "Invite code exhausted"}))).into_response();
96
-
}
97
-
98
-
let update_invite = sqlx::query("UPDATE invite_codes SET available_uses = available_uses - 1 WHERE code = $1")
99
-
.bind(code)
100
-
.execute(&mut *tx)
101
-
.await;
102
-
103
-
if let Err(e) = update_invite {
104
-
error!("Error updating invite code: {:?}", e);
105
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
106
-
}
107
-
},
108
-
Ok(None) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidInviteCode", "message": "Invite code not found"}))).into_response(),
109
-
Err(e) => {
110
-
error!("Error checking invite code: {:?}", e);
111
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
112
-
}
113
-
}
114
-
}
115
-
116
-
let password_hash = match hash(&input.password, DEFAULT_COST) {
117
-
Ok(h) => h,
118
-
Err(e) => {
119
-
error!("Error hashing password: {:?}", e);
120
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
121
-
}
122
-
};
123
-
124
-
let user_insert = sqlx::query("INSERT INTO users (handle, email, did, password_hash) VALUES ($1, $2, $3, $4) RETURNING id")
125
-
.bind(&input.handle)
126
-
.bind(&input.email)
127
-
.bind(&did)
128
-
.bind(&password_hash)
129
-
.fetch_one(&mut *tx)
130
-
.await;
131
-
132
-
let user_id: uuid::Uuid = match user_insert {
133
-
Ok(row) => row.get("id"),
134
-
Err(e) => {
135
-
error!("Error inserting user: {:?}", e);
136
-
// TODO: Check for unique constraint violation on email/did specifically
137
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
138
-
}
139
-
};
140
-
141
-
let secret_key = SecretKey::random(&mut OsRng);
142
-
let secret_key_bytes = secret_key.to_bytes();
143
-
144
-
let key_insert = sqlx::query("INSERT INTO user_keys (user_id, key_bytes) VALUES ($1, $2)")
145
-
.bind(user_id)
146
-
.bind(&secret_key_bytes[..])
147
-
.execute(&mut *tx)
148
-
.await;
149
-
150
-
if let Err(e) = key_insert {
151
-
error!("Error inserting user key: {:?}", e);
152
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
153
-
}
154
-
155
-
let mst = Mst::new(Arc::new(state.block_store.clone()));
156
-
let mst_root = match mst.root().await {
157
-
Ok(c) => c,
158
-
Err(e) => {
159
-
error!("Error creating MST root: {:?}", e);
160
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
161
-
}
162
-
};
163
-
164
-
let did_obj = match Did::new(&did) {
165
-
Ok(d) => d,
166
-
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Invalid DID"}))).into_response(),
167
-
};
168
-
169
-
let rev = Tid::now(LimitedU32::MIN);
170
-
171
-
let commit = Commit::new_unsigned(
172
-
did_obj,
173
-
mst_root,
174
-
rev,
175
-
None
176
-
);
177
-
178
-
let commit_bytes = match commit.to_cbor() {
179
-
Ok(b) => b,
180
-
Err(e) => {
181
-
error!("Error serializing genesis commit: {:?}", e);
182
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
183
-
}
184
-
};
185
-
186
-
let commit_cid = match state.block_store.put(&commit_bytes).await {
187
-
Ok(c) => c,
188
-
Err(e) => {
189
-
error!("Error saving genesis commit: {:?}", e);
190
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
191
-
}
192
-
};
193
-
194
-
let repo_insert = sqlx::query("INSERT INTO repos (user_id, repo_root_cid) VALUES ($1, $2)")
195
-
.bind(user_id)
196
-
.bind(commit_cid.to_string())
197
-
.execute(&mut *tx)
198
-
.await;
199
-
200
-
if let Err(e) = repo_insert {
201
-
error!("Error initializing repo: {:?}", e);
202
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
203
-
}
204
-
205
-
if let Some(code) = &input.invite_code {
206
-
let use_insert = sqlx::query("INSERT INTO invite_code_uses (code, used_by_user) VALUES ($1, $2)")
207
-
.bind(code)
208
-
.bind(user_id)
209
-
.execute(&mut *tx)
210
-
.await;
211
-
212
-
if let Err(e) = use_insert {
213
-
error!("Error recording invite usage: {:?}", e);
214
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
215
-
}
216
-
}
217
-
218
-
let access_jwt = crate::auth::create_access_token(&did, &secret_key_bytes[..]).map_err(|e| {
219
-
error!("Error creating access token: {:?}", e);
220
-
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response()
221
-
});
222
-
let access_jwt = match access_jwt {
223
-
Ok(t) => t,
224
-
Err(r) => return r,
225
-
};
226
-
227
-
let refresh_jwt = crate::auth::create_refresh_token(&did, &secret_key_bytes[..]).map_err(|e| {
228
-
error!("Error creating refresh token: {:?}", e);
229
-
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response()
230
-
});
231
-
let refresh_jwt = match refresh_jwt {
232
-
Ok(t) => t,
233
-
Err(r) => return r,
234
-
};
235
-
236
-
let session_insert = sqlx::query("INSERT INTO sessions (access_jwt, refresh_jwt, did) VALUES ($1, $2, $3)")
237
-
.bind(&access_jwt)
238
-
.bind(&refresh_jwt)
239
-
.bind(&did)
240
-
.execute(&mut *tx)
241
-
.await;
242
-
243
-
if let Err(e) = session_insert {
244
-
error!("Error inserting session: {:?}", e);
245
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
246
-
}
247
-
248
-
if let Err(e) = tx.commit().await {
249
-
error!("Error committing transaction: {:?}", e);
250
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
251
-
}
252
-
253
-
(StatusCode::OK, Json(CreateAccountOutput {
254
-
access_jwt,
255
-
refresh_jwt,
256
-
handle: input.handle,
257
-
did,
258
-
})).into_response()
259
-
}
260
-
261
-
fn get_jwk(key_bytes: &[u8]) -> serde_json::Value {
262
-
use k256::elliptic_curve::sec1::ToEncodedPoint;
263
-
264
-
let secret_key = SecretKey::from_slice(key_bytes).expect("Invalid key length");
265
-
let public_key = secret_key.public_key();
266
-
let encoded = public_key.to_encoded_point(false);
267
-
let x = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(encoded.x().unwrap());
268
-
let y = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(encoded.y().unwrap());
269
-
270
-
json!({
271
-
"kty": "EC",
272
-
"crv": "secp256k1",
273
-
"x": x,
274
-
"y": y
275
-
})
276
-
}
277
-
278
-
pub async fn well_known_did(State(_state): State<AppState>) -> impl IntoResponse {
279
-
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
280
-
// Kinda for local dev, encode hostname if it contains port
281
-
let did = if hostname.contains(':') {
282
-
format!("did:web:{}", hostname.replace(':', "%3A"))
283
-
} else {
284
-
format!("did:web:{}", hostname)
285
-
};
286
-
287
-
Json(json!({
288
-
"@context": ["https://www.w3.org/ns/did/v1"],
289
-
"id": did,
290
-
"service": [{
291
-
"id": "#atproto_pds",
292
-
"type": "AtprotoPersonalDataServer",
293
-
"serviceEndpoint": format!("https://{}", hostname)
294
-
}]
295
-
}))
296
-
}
297
-
298
-
pub async fn user_did_doc(
299
-
State(state): State<AppState>,
300
-
Path(handle): Path<String>,
301
-
) -> Response {
302
-
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
303
-
304
-
let user = sqlx::query("SELECT id, did FROM users WHERE handle = $1")
305
-
.bind(&handle)
306
-
.fetch_optional(&state.db)
307
-
.await;
308
-
309
-
let (user_id, did) = match user {
310
-
Ok(Some(row)) => {
311
-
let id: uuid::Uuid = row.get("id");
312
-
let d: String = row.get("did");
313
-
(id, d)
314
-
},
315
-
Ok(None) => return (StatusCode::NOT_FOUND, Json(json!({"error": "NotFound"}))).into_response(),
316
-
Err(e) => {
317
-
error!("DB Error: {:?}", e);
318
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response()
319
-
},
320
-
};
321
-
322
-
if !did.starts_with("did:web:") {
323
-
return (StatusCode::NOT_FOUND, Json(json!({"error": "NotFound", "message": "User is not did:web"}))).into_response();
324
-
}
325
-
326
-
let key_row = sqlx::query("SELECT key_bytes FROM user_keys WHERE user_id = $1")
327
-
.bind(user_id)
328
-
.fetch_optional(&state.db)
329
-
.await;
330
-
331
-
let key_bytes: Vec<u8> = match key_row {
332
-
Ok(Some(row)) => row.get("key_bytes"),
333
-
_ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(),
334
-
};
335
-
336
-
let jwk = get_jwk(&key_bytes);
337
-
338
-
Json(json!({
339
-
"@context": ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/jws-2020/v1"],
340
-
"id": did,
341
-
"alsoKnownAs": [format!("at://{}", handle)],
342
-
"verificationMethod": [{
343
-
"id": format!("{}#atproto", did),
344
-
"type": "JsonWebKey2020",
345
-
"controller": did,
346
-
"publicKeyJwk": jwk
347
-
}],
348
-
"service": [{
349
-
"id": "#atproto_pds",
350
-
"type": "AtprotoPersonalDataServer",
351
-
"serviceEndpoint": format!("https://{}", hostname)
352
-
}]
353
-
})).into_response()
354
-
}
355
-
356
-
async fn verify_did_web(did: &str, hostname: &str, handle: &str) -> Result<(), String> {
357
-
let expected_prefix = if hostname.contains(':') {
358
-
format!("did:web:{}", hostname.replace(':', "%3A"))
359
-
} else {
360
-
format!("did:web:{}", hostname)
361
-
};
362
-
363
-
if did.starts_with(&expected_prefix) {
364
-
let suffix = &did[expected_prefix.len()..];
365
-
let expected_suffix = format!(":u:{}", handle);
366
-
if suffix == expected_suffix {
367
-
Ok(())
368
-
} else {
369
-
Err(format!("Invalid DID path for this PDS. Expected {}", expected_suffix))
370
-
}
371
-
} else {
372
-
let parts: Vec<&str> = did.split(':').collect();
373
-
if parts.len() < 3 || parts[0] != "did" || parts[1] != "web" {
374
-
return Err("Invalid did:web format".into());
375
-
}
376
-
377
-
let domain_segment = parts[2];
378
-
let domain = domain_segment.replace("%3A", ":");
379
-
380
-
let scheme = if domain.starts_with("localhost") || domain.starts_with("127.0.0.1") {
381
-
"http"
382
-
} else {
383
-
"https"
384
-
};
385
-
386
-
let url = if parts.len() == 3 {
387
-
format!("{}://{}/.well-known/did.json", scheme, domain)
388
-
} else {
389
-
let path = parts[3..].join("/");
390
-
format!("{}://{}/{}/did.json", scheme, domain, path)
391
-
};
392
-
393
-
let client = reqwest::Client::builder()
394
-
.timeout(std::time::Duration::from_secs(5))
395
-
.build()
396
-
.map_err(|e| format!("Failed to create client: {}", e))?;
397
-
398
-
let resp = client.get(&url).send().await
399
-
.map_err(|e| format!("Failed to fetch DID doc: {}", e))?;
400
-
401
-
if !resp.status().is_success() {
402
-
return Err(format!("Failed to fetch DID doc: HTTP {}", resp.status()));
403
-
}
404
-
405
-
let doc: serde_json::Value = resp.json().await
406
-
.map_err(|e| format!("Failed to parse DID doc: {}", e))?;
407
-
408
-
let services = doc["service"].as_array()
409
-
.ok_or("No services found in DID doc")?;
410
-
411
-
let pds_endpoint = format!("https://{}", hostname);
412
-
413
-
let has_valid_service = services.iter().any(|s| {
414
-
s["type"] == "AtprotoPersonalDataServer" &&
415
-
s["serviceEndpoint"] == pds_endpoint
416
-
});
417
-
418
-
if has_valid_service {
419
-
Ok(())
420
-
} else {
421
-
Err(format!("DID document does not list this PDS ({}) as AtprotoPersonalDataServer", pds_endpoint))
422
-
}
423
-
}
424
-
}
···
+355
src/api/identity/account.rs
+355
src/api/identity/account.rs
···
···
1
+
use super::did::verify_did_web;
2
+
use crate::state::AppState;
3
+
use axum::{
4
+
Json,
5
+
extract::State,
6
+
http::StatusCode,
7
+
response::{IntoResponse, Response},
8
+
};
9
+
use bcrypt::{DEFAULT_COST, hash};
10
+
use jacquard::types::{did::Did, integer::LimitedU32, string::Tid};
11
+
use jacquard_repo::{commit::Commit, mst::Mst, storage::BlockStore};
12
+
use k256::SecretKey;
13
+
use rand::rngs::OsRng;
14
+
use serde::{Deserialize, Serialize};
15
+
use serde_json::json;
16
+
use sqlx::Row;
17
+
use std::sync::Arc;
18
+
use tracing::{error, info};
19
+
20
+
#[derive(Deserialize)]
21
+
pub struct CreateAccountInput {
22
+
pub handle: String,
23
+
pub email: String,
24
+
pub password: String,
25
+
#[serde(rename = "inviteCode")]
26
+
pub invite_code: Option<String>,
27
+
pub did: Option<String>,
28
+
}
29
+
30
+
#[derive(Serialize)]
31
+
#[serde(rename_all = "camelCase")]
32
+
pub struct CreateAccountOutput {
33
+
pub access_jwt: String,
34
+
pub refresh_jwt: String,
35
+
pub handle: String,
36
+
pub did: String,
37
+
}
38
+
39
+
pub async fn create_account(
40
+
State(state): State<AppState>,
41
+
Json(input): Json<CreateAccountInput>,
42
+
) -> Response {
43
+
info!("create_account hit: {}", input.handle);
44
+
if input.handle.contains('!') || input.handle.contains('@') {
45
+
return (
46
+
StatusCode::BAD_REQUEST,
47
+
Json(
48
+
json!({"error": "InvalidHandle", "message": "Handle contains invalid characters"}),
49
+
),
50
+
)
51
+
.into_response();
52
+
}
53
+
54
+
let did = if let Some(d) = &input.did {
55
+
if d.trim().is_empty() {
56
+
format!("did:plc:{}", uuid::Uuid::new_v4())
57
+
} else {
58
+
let hostname =
59
+
std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
60
+
if let Err(e) = verify_did_web(d, &hostname, &input.handle).await {
61
+
return (
62
+
StatusCode::BAD_REQUEST,
63
+
Json(json!({"error": "InvalidDid", "message": e})),
64
+
)
65
+
.into_response();
66
+
}
67
+
d.clone()
68
+
}
69
+
} else {
70
+
format!("did:plc:{}", uuid::Uuid::new_v4())
71
+
};
72
+
73
+
let mut tx = match state.db.begin().await {
74
+
Ok(tx) => tx,
75
+
Err(e) => {
76
+
error!("Error starting transaction: {:?}", e);
77
+
return (
78
+
StatusCode::INTERNAL_SERVER_ERROR,
79
+
Json(json!({"error": "InternalError"})),
80
+
)
81
+
.into_response();
82
+
}
83
+
};
84
+
85
+
let exists_query = sqlx::query("SELECT 1 FROM users WHERE handle = $1")
86
+
.bind(&input.handle)
87
+
.fetch_optional(&mut *tx)
88
+
.await;
89
+
90
+
match exists_query {
91
+
Ok(Some(_)) => {
92
+
return (
93
+
StatusCode::BAD_REQUEST,
94
+
Json(json!({"error": "HandleTaken", "message": "Handle already taken"})),
95
+
)
96
+
.into_response();
97
+
}
98
+
Err(e) => {
99
+
error!("Error checking handle: {:?}", e);
100
+
return (
101
+
StatusCode::INTERNAL_SERVER_ERROR,
102
+
Json(json!({"error": "InternalError"})),
103
+
)
104
+
.into_response();
105
+
}
106
+
Ok(None) => {}
107
+
}
108
+
109
+
if let Some(code) = &input.invite_code {
110
+
let invite_query =
111
+
sqlx::query("SELECT available_uses FROM invite_codes WHERE code = $1 FOR UPDATE")
112
+
.bind(code)
113
+
.fetch_optional(&mut *tx)
114
+
.await;
115
+
116
+
match invite_query {
117
+
Ok(Some(row)) => {
118
+
let uses: i32 = row.get("available_uses");
119
+
if uses <= 0 {
120
+
return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidInviteCode", "message": "Invite code exhausted"}))).into_response();
121
+
}
122
+
123
+
let update_invite = sqlx::query(
124
+
"UPDATE invite_codes SET available_uses = available_uses - 1 WHERE code = $1",
125
+
)
126
+
.bind(code)
127
+
.execute(&mut *tx)
128
+
.await;
129
+
130
+
if let Err(e) = update_invite {
131
+
error!("Error updating invite code: {:?}", e);
132
+
return (
133
+
StatusCode::INTERNAL_SERVER_ERROR,
134
+
Json(json!({"error": "InternalError"})),
135
+
)
136
+
.into_response();
137
+
}
138
+
}
139
+
Ok(None) => {
140
+
return (
141
+
StatusCode::BAD_REQUEST,
142
+
Json(json!({"error": "InvalidInviteCode", "message": "Invite code not found"})),
143
+
)
144
+
.into_response();
145
+
}
146
+
Err(e) => {
147
+
error!("Error checking invite code: {:?}", e);
148
+
return (
149
+
StatusCode::INTERNAL_SERVER_ERROR,
150
+
Json(json!({"error": "InternalError"})),
151
+
)
152
+
.into_response();
153
+
}
154
+
}
155
+
}
156
+
157
+
let password_hash = match hash(&input.password, DEFAULT_COST) {
158
+
Ok(h) => h,
159
+
Err(e) => {
160
+
error!("Error hashing password: {:?}", e);
161
+
return (
162
+
StatusCode::INTERNAL_SERVER_ERROR,
163
+
Json(json!({"error": "InternalError"})),
164
+
)
165
+
.into_response();
166
+
}
167
+
};
168
+
169
+
let user_insert = sqlx::query("INSERT INTO users (handle, email, did, password_hash) VALUES ($1, $2, $3, $4) RETURNING id")
170
+
.bind(&input.handle)
171
+
.bind(&input.email)
172
+
.bind(&did)
173
+
.bind(&password_hash)
174
+
.fetch_one(&mut *tx)
175
+
.await;
176
+
177
+
let user_id: uuid::Uuid = match user_insert {
178
+
Ok(row) => row.get("id"),
179
+
Err(e) => {
180
+
error!("Error inserting user: {:?}", e);
181
+
// TODO: Check for unique constraint violation on email/did specifically
182
+
return (
183
+
StatusCode::INTERNAL_SERVER_ERROR,
184
+
Json(json!({"error": "InternalError"})),
185
+
)
186
+
.into_response();
187
+
}
188
+
};
189
+
190
+
let secret_key = SecretKey::random(&mut OsRng);
191
+
let secret_key_bytes = secret_key.to_bytes();
192
+
193
+
let key_insert = sqlx::query("INSERT INTO user_keys (user_id, key_bytes) VALUES ($1, $2)")
194
+
.bind(user_id)
195
+
.bind(&secret_key_bytes[..])
196
+
.execute(&mut *tx)
197
+
.await;
198
+
199
+
if let Err(e) = key_insert {
200
+
error!("Error inserting user key: {:?}", e);
201
+
return (
202
+
StatusCode::INTERNAL_SERVER_ERROR,
203
+
Json(json!({"error": "InternalError"})),
204
+
)
205
+
.into_response();
206
+
}
207
+
208
+
let mst = Mst::new(Arc::new(state.block_store.clone()));
209
+
let mst_root = match mst.root().await {
210
+
Ok(c) => c,
211
+
Err(e) => {
212
+
error!("Error creating MST root: {:?}", e);
213
+
return (
214
+
StatusCode::INTERNAL_SERVER_ERROR,
215
+
Json(json!({"error": "InternalError"})),
216
+
)
217
+
.into_response();
218
+
}
219
+
};
220
+
221
+
let did_obj = match Did::new(&did) {
222
+
Ok(d) => d,
223
+
Err(_) => {
224
+
return (
225
+
StatusCode::INTERNAL_SERVER_ERROR,
226
+
Json(json!({"error": "InternalError", "message": "Invalid DID"})),
227
+
)
228
+
.into_response();
229
+
}
230
+
};
231
+
232
+
let rev = Tid::now(LimitedU32::MIN);
233
+
234
+
let commit = Commit::new_unsigned(did_obj, mst_root, rev, None);
235
+
236
+
let commit_bytes = match commit.to_cbor() {
237
+
Ok(b) => b,
238
+
Err(e) => {
239
+
error!("Error serializing genesis commit: {:?}", e);
240
+
return (
241
+
StatusCode::INTERNAL_SERVER_ERROR,
242
+
Json(json!({"error": "InternalError"})),
243
+
)
244
+
.into_response();
245
+
}
246
+
};
247
+
248
+
let commit_cid = match state.block_store.put(&commit_bytes).await {
249
+
Ok(c) => c,
250
+
Err(e) => {
251
+
error!("Error saving genesis commit: {:?}", e);
252
+
return (
253
+
StatusCode::INTERNAL_SERVER_ERROR,
254
+
Json(json!({"error": "InternalError"})),
255
+
)
256
+
.into_response();
257
+
}
258
+
};
259
+
260
+
let repo_insert = sqlx::query("INSERT INTO repos (user_id, repo_root_cid) VALUES ($1, $2)")
261
+
.bind(user_id)
262
+
.bind(commit_cid.to_string())
263
+
.execute(&mut *tx)
264
+
.await;
265
+
266
+
if let Err(e) = repo_insert {
267
+
error!("Error initializing repo: {:?}", e);
268
+
return (
269
+
StatusCode::INTERNAL_SERVER_ERROR,
270
+
Json(json!({"error": "InternalError"})),
271
+
)
272
+
.into_response();
273
+
}
274
+
275
+
if let Some(code) = &input.invite_code {
276
+
let use_insert =
277
+
sqlx::query("INSERT INTO invite_code_uses (code, used_by_user) VALUES ($1, $2)")
278
+
.bind(code)
279
+
.bind(user_id)
280
+
.execute(&mut *tx)
281
+
.await;
282
+
283
+
if let Err(e) = use_insert {
284
+
error!("Error recording invite usage: {:?}", e);
285
+
return (
286
+
StatusCode::INTERNAL_SERVER_ERROR,
287
+
Json(json!({"error": "InternalError"})),
288
+
)
289
+
.into_response();
290
+
}
291
+
}
292
+
293
+
let access_jwt = crate::auth::create_access_token(&did, &secret_key_bytes[..]).map_err(|e| {
294
+
error!("Error creating access token: {:?}", e);
295
+
(
296
+
StatusCode::INTERNAL_SERVER_ERROR,
297
+
Json(json!({"error": "InternalError"})),
298
+
)
299
+
.into_response()
300
+
});
301
+
let access_jwt = match access_jwt {
302
+
Ok(t) => t,
303
+
Err(r) => return r,
304
+
};
305
+
306
+
let refresh_jwt = crate::auth::create_refresh_token(&did, &secret_key_bytes[..]).map_err(|e| {
307
+
error!("Error creating refresh token: {:?}", e);
308
+
(
309
+
StatusCode::INTERNAL_SERVER_ERROR,
310
+
Json(json!({"error": "InternalError"})),
311
+
)
312
+
.into_response()
313
+
});
314
+
let refresh_jwt = match refresh_jwt {
315
+
Ok(t) => t,
316
+
Err(r) => return r,
317
+
};
318
+
319
+
let session_insert =
320
+
sqlx::query("INSERT INTO sessions (access_jwt, refresh_jwt, did) VALUES ($1, $2, $3)")
321
+
.bind(&access_jwt)
322
+
.bind(&refresh_jwt)
323
+
.bind(&did)
324
+
.execute(&mut *tx)
325
+
.await;
326
+
327
+
if let Err(e) = session_insert {
328
+
error!("Error inserting session: {:?}", e);
329
+
return (
330
+
StatusCode::INTERNAL_SERVER_ERROR,
331
+
Json(json!({"error": "InternalError"})),
332
+
)
333
+
.into_response();
334
+
}
335
+
336
+
if let Err(e) = tx.commit().await {
337
+
error!("Error committing transaction: {:?}", e);
338
+
return (
339
+
StatusCode::INTERNAL_SERVER_ERROR,
340
+
Json(json!({"error": "InternalError"})),
341
+
)
342
+
.into_response();
343
+
}
344
+
345
+
(
346
+
StatusCode::OK,
347
+
Json(CreateAccountOutput {
348
+
access_jwt,
349
+
refresh_jwt,
350
+
handle: input.handle,
351
+
did,
352
+
}),
353
+
)
354
+
.into_response()
355
+
}
+201
src/api/identity/did.rs
+201
src/api/identity/did.rs
···
···
1
+
use crate::state::AppState;
2
+
use axum::{
3
+
Json,
4
+
extract::{Path, State},
5
+
http::StatusCode,
6
+
response::{IntoResponse, Response},
7
+
};
8
+
use base64::Engine;
9
+
use k256::SecretKey;
10
+
use k256::elliptic_curve::sec1::ToEncodedPoint;
11
+
use reqwest;
12
+
use serde_json::json;
13
+
use sqlx::Row;
14
+
use tracing::error;
15
+
16
+
pub fn get_jwk(key_bytes: &[u8]) -> serde_json::Value {
17
+
let secret_key = SecretKey::from_slice(key_bytes).expect("Invalid key length");
18
+
let public_key = secret_key.public_key();
19
+
let encoded = public_key.to_encoded_point(false);
20
+
let x = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(encoded.x().unwrap());
21
+
let y = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(encoded.y().unwrap());
22
+
23
+
json!({
24
+
"kty": "EC",
25
+
"crv": "secp256k1",
26
+
"x": x,
27
+
"y": y
28
+
})
29
+
}
30
+
31
+
pub async fn well_known_did(State(_state): State<AppState>) -> impl IntoResponse {
32
+
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
33
+
// Kinda for local dev, encode hostname if it contains port
34
+
let did = if hostname.contains(':') {
35
+
format!("did:web:{}", hostname.replace(':', "%3A"))
36
+
} else {
37
+
format!("did:web:{}", hostname)
38
+
};
39
+
40
+
Json(json!({
41
+
"@context": ["https://www.w3.org/ns/did/v1"],
42
+
"id": did,
43
+
"service": [{
44
+
"id": "#atproto_pds",
45
+
"type": "AtprotoPersonalDataServer",
46
+
"serviceEndpoint": format!("https://{}", hostname)
47
+
}]
48
+
}))
49
+
}
50
+
51
+
pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<String>) -> Response {
52
+
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
53
+
54
+
let user = sqlx::query("SELECT id, did FROM users WHERE handle = $1")
55
+
.bind(&handle)
56
+
.fetch_optional(&state.db)
57
+
.await;
58
+
59
+
let (user_id, did) = match user {
60
+
Ok(Some(row)) => {
61
+
let id: uuid::Uuid = row.get("id");
62
+
let d: String = row.get("did");
63
+
(id, d)
64
+
}
65
+
Ok(None) => {
66
+
return (StatusCode::NOT_FOUND, Json(json!({"error": "NotFound"}))).into_response();
67
+
}
68
+
Err(e) => {
69
+
error!("DB Error: {:?}", e);
70
+
return (
71
+
StatusCode::INTERNAL_SERVER_ERROR,
72
+
Json(json!({"error": "InternalError"})),
73
+
)
74
+
.into_response();
75
+
}
76
+
};
77
+
78
+
if !did.starts_with("did:web:") {
79
+
return (
80
+
StatusCode::NOT_FOUND,
81
+
Json(json!({"error": "NotFound", "message": "User is not did:web"})),
82
+
)
83
+
.into_response();
84
+
}
85
+
86
+
let key_row = sqlx::query("SELECT key_bytes FROM user_keys WHERE user_id = $1")
87
+
.bind(user_id)
88
+
.fetch_optional(&state.db)
89
+
.await;
90
+
91
+
let key_bytes: Vec<u8> = match key_row {
92
+
Ok(Some(row)) => row.get("key_bytes"),
93
+
_ => {
94
+
return (
95
+
StatusCode::INTERNAL_SERVER_ERROR,
96
+
Json(json!({"error": "InternalError"})),
97
+
)
98
+
.into_response();
99
+
}
100
+
};
101
+
102
+
let jwk = get_jwk(&key_bytes);
103
+
104
+
Json(json!({
105
+
"@context": ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/jws-2020/v1"],
106
+
"id": did,
107
+
"alsoKnownAs": [format!("at://{}", handle)],
108
+
"verificationMethod": [{
109
+
"id": format!("{}#atproto", did),
110
+
"type": "JsonWebKey2020",
111
+
"controller": did,
112
+
"publicKeyJwk": jwk
113
+
}],
114
+
"service": [{
115
+
"id": "#atproto_pds",
116
+
"type": "AtprotoPersonalDataServer",
117
+
"serviceEndpoint": format!("https://{}", hostname)
118
+
}]
119
+
})).into_response()
120
+
}
121
+
122
+
pub async fn verify_did_web(did: &str, hostname: &str, handle: &str) -> Result<(), String> {
123
+
let expected_prefix = if hostname.contains(':') {
124
+
format!("did:web:{}", hostname.replace(':', "%3A"))
125
+
} else {
126
+
format!("did:web:{}", hostname)
127
+
};
128
+
129
+
if did.starts_with(&expected_prefix) {
130
+
let suffix = &did[expected_prefix.len()..];
131
+
let expected_suffix = format!(":u:{}", handle);
132
+
if suffix == expected_suffix {
133
+
Ok(())
134
+
} else {
135
+
Err(format!(
136
+
"Invalid DID path for this PDS. Expected {}",
137
+
expected_suffix
138
+
))
139
+
}
140
+
} else {
141
+
let parts: Vec<&str> = did.split(':').collect();
142
+
if parts.len() < 3 || parts[0] != "did" || parts[1] != "web" {
143
+
return Err("Invalid did:web format".into());
144
+
}
145
+
146
+
let domain_segment = parts[2];
147
+
let domain = domain_segment.replace("%3A", ":");
148
+
149
+
let scheme = if domain.starts_with("localhost") || domain.starts_with("127.0.0.1") {
150
+
"http"
151
+
} else {
152
+
"https"
153
+
};
154
+
155
+
let url = if parts.len() == 3 {
156
+
format!("{}://{}/.well-known/did.json", scheme, domain)
157
+
} else {
158
+
let path = parts[3..].join("/");
159
+
format!("{}://{}/{}/did.json", scheme, domain, path)
160
+
};
161
+
162
+
let client = reqwest::Client::builder()
163
+
.timeout(std::time::Duration::from_secs(5))
164
+
.build()
165
+
.map_err(|e| format!("Failed to create client: {}", e))?;
166
+
167
+
let resp = client
168
+
.get(&url)
169
+
.send()
170
+
.await
171
+
.map_err(|e| format!("Failed to fetch DID doc: {}", e))?;
172
+
173
+
if !resp.status().is_success() {
174
+
return Err(format!("Failed to fetch DID doc: HTTP {}", resp.status()));
175
+
}
176
+
177
+
let doc: serde_json::Value = resp
178
+
.json()
179
+
.await
180
+
.map_err(|e| format!("Failed to parse DID doc: {}", e))?;
181
+
182
+
let services = doc["service"]
183
+
.as_array()
184
+
.ok_or("No services found in DID doc")?;
185
+
186
+
let pds_endpoint = format!("https://{}", hostname);
187
+
188
+
let has_valid_service = services.iter().any(|s| {
189
+
s["type"] == "AtprotoPersonalDataServer" && s["serviceEndpoint"] == pds_endpoint
190
+
});
191
+
192
+
if has_valid_service {
193
+
Ok(())
194
+
} else {
195
+
Err(format!(
196
+
"DID document does not list this PDS ({}) as AtprotoPersonalDataServer",
197
+
pds_endpoint
198
+
))
199
+
}
200
+
}
201
+
}
+5
src/api/identity/mod.rs
+5
src/api/identity/mod.rs
+3
-3
src/api/mod.rs
+3
-3
src/api/mod.rs
+24
-19
src/api/proxy.rs
+24
-19
src/api/proxy.rs
···
1
use axum::{
2
extract::{Path, Query, State},
3
http::{HeaderMap, Method, StatusCode},
4
response::{IntoResponse, Response},
5
-
body::Bytes,
6
};
7
use reqwest::Client;
8
-
use tracing::{info, error};
9
use std::collections::HashMap;
10
-
use crate::state::AppState;
11
-
use sqlx::Row;
12
13
pub async fn proxy_handler(
14
State(state): State<AppState>,
···
18
Query(params): Query<HashMap<String, String>>,
19
body: Bytes,
20
) -> Response {
21
-
22
-
let proxy_header = headers.get("atproto-proxy")
23
.and_then(|h| h.to_str().ok())
24
.map(|s| s.to_string());
25
···
27
Some(url) => url.clone(),
28
None => match std::env::var("APPVIEW_URL") {
29
Ok(url) => url,
30
-
Err(_) => return (StatusCode::BAD_GATEWAY, "No upstream AppView configured").into_response(),
31
},
32
};
33
···
37
38
let client = Client::new();
39
40
-
let mut request_builder = client
41
-
.request(method_verb, &target_url)
42
-
.query(¶ms);
43
44
let mut auth_header_val = headers.get("Authorization").map(|h| h.clone());
45
···
48
if let Ok(token) = auth_val.to_str() {
49
let token = token.replace("Bearer ", "");
50
if let Ok(did) = crate::auth::get_did_from_token(&token) {
51
-
let key_row = sqlx::query("SELECT k.key_bytes FROM user_keys k JOIN users u ON k.user_id = u.id WHERE u.did = $1")
52
.bind(&did)
53
.fetch_optional(&state.db)
54
.await;
55
56
if let Ok(Some(row)) = key_row {
57
let key_bytes: Vec<u8> = row.get("key_bytes");
58
-
if let Ok(new_token) = crate::auth::create_service_token(&did, aud, &method, &key_bytes) {
59
-
if let Ok(val) = axum::http::HeaderValue::from_str(&format!("Bearer {}", new_token)) {
60
-
auth_header_val = Some(val);
61
-
}
62
}
63
}
64
}
···
86
Ok(b) => b,
87
Err(e) => {
88
error!("Error reading proxy response body: {:?}", e);
89
-
return (StatusCode::BAD_GATEWAY, "Error reading upstream response").into_response();
90
}
91
};
92
···
99
match response_builder.body(axum::body::Body::from(body)) {
100
Ok(r) => r,
101
Err(e) => {
102
-
error!("Error building proxy response: {:?}", e);
103
-
(StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error").into_response()
104
}
105
}
106
-
},
107
Err(e) => {
108
error!("Error sending proxy request: {:?}", e);
109
if e.is_timeout() {
···
1
+
use crate::state::AppState;
2
use axum::{
3
+
body::Bytes,
4
extract::{Path, Query, State},
5
http::{HeaderMap, Method, StatusCode},
6
response::{IntoResponse, Response},
7
};
8
use reqwest::Client;
9
+
use sqlx::Row;
10
use std::collections::HashMap;
11
+
use tracing::{error, info};
12
13
pub async fn proxy_handler(
14
State(state): State<AppState>,
···
18
Query(params): Query<HashMap<String, String>>,
19
body: Bytes,
20
) -> Response {
21
+
let proxy_header = headers
22
+
.get("atproto-proxy")
23
.and_then(|h| h.to_str().ok())
24
.map(|s| s.to_string());
25
···
27
Some(url) => url.clone(),
28
None => match std::env::var("APPVIEW_URL") {
29
Ok(url) => url,
30
+
Err(_) => {
31
+
return (StatusCode::BAD_GATEWAY, "No upstream AppView configured").into_response();
32
+
}
33
},
34
};
35
···
39
40
let client = Client::new();
41
42
+
let mut request_builder = client.request(method_verb, &target_url).query(¶ms);
43
44
let mut auth_header_val = headers.get("Authorization").map(|h| h.clone());
45
···
48
if let Ok(token) = auth_val.to_str() {
49
let token = token.replace("Bearer ", "");
50
if let Ok(did) = crate::auth::get_did_from_token(&token) {
51
+
let key_row = sqlx::query("SELECT k.key_bytes FROM user_keys k JOIN users u ON k.user_id = u.id WHERE u.did = $1")
52
.bind(&did)
53
.fetch_optional(&state.db)
54
.await;
55
56
if let Ok(Some(row)) = key_row {
57
let key_bytes: Vec<u8> = row.get("key_bytes");
58
+
if let Ok(new_token) =
59
+
crate::auth::create_service_token(&did, aud, &method, &key_bytes)
60
+
{
61
+
if let Ok(val) =
62
+
axum::http::HeaderValue::from_str(&format!("Bearer {}", new_token))
63
+
{
64
+
auth_header_val = Some(val);
65
+
}
66
}
67
}
68
}
···
90
Ok(b) => b,
91
Err(e) => {
92
error!("Error reading proxy response body: {:?}", e);
93
+
return (StatusCode::BAD_GATEWAY, "Error reading upstream response")
94
+
.into_response();
95
}
96
};
97
···
104
match response_builder.body(axum::body::Body::from(body)) {
105
Ok(r) => r,
106
Err(e) => {
107
+
error!("Error building proxy response: {:?}", e);
108
+
(StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error").into_response()
109
}
110
}
111
+
}
112
Err(e) => {
113
error!("Error sending proxy request: {:?}", e);
114
if e.is_timeout() {
-889
src/api/repo.rs
-889
src/api/repo.rs
···
1
-
use axum::{
2
-
extract::{State, Query},
3
-
Json,
4
-
response::{IntoResponse, Response},
5
-
http::StatusCode,
6
-
};
7
-
use serde::{Deserialize, Serialize};
8
-
use serde_json::json;
9
-
use crate::state::AppState;
10
-
use chrono::Utc;
11
-
use sqlx::Row;
12
-
use cid::Cid;
13
-
use std::str::FromStr;
14
-
use jacquard_repo::{mst::Mst, commit::Commit, storage::BlockStore};
15
-
use jacquard::types::{string::{Nsid, Tid}, did::Did, integer::LimitedU32};
16
-
use tracing::error;
17
-
use std::sync::Arc;
18
-
use sha2::{Sha256, Digest};
19
-
use multihash::Multihash;
20
-
use axum::body::Bytes;
21
-
22
-
#[derive(Deserialize)]
23
-
#[allow(dead_code)]
24
-
pub struct CreateRecordInput {
25
-
pub repo: String,
26
-
pub collection: String,
27
-
pub rkey: Option<String>,
28
-
pub validate: Option<bool>,
29
-
pub record: serde_json::Value,
30
-
#[serde(rename = "swapCommit")]
31
-
pub swap_commit: Option<String>,
32
-
}
33
-
34
-
#[derive(Serialize)]
35
-
#[serde(rename_all = "camelCase")]
36
-
pub struct CreateRecordOutput {
37
-
pub uri: String,
38
-
pub cid: String,
39
-
}
40
-
41
-
pub async fn create_record(
42
-
State(state): State<AppState>,
43
-
headers: axum::http::HeaderMap,
44
-
Json(input): Json<CreateRecordInput>,
45
-
) -> Response {
46
-
let auth_header = headers.get("Authorization");
47
-
if auth_header.is_none() {
48
-
return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationRequired"}))).into_response();
49
-
}
50
-
let token = auth_header.unwrap().to_str().unwrap_or("").replace("Bearer ", "");
51
-
52
-
let session = sqlx::query(
53
-
"SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.access_jwt = $1"
54
-
)
55
-
.bind(&token)
56
-
.fetch_optional(&state.db)
57
-
.await
58
-
.unwrap_or(None);
59
-
60
-
let (did, key_bytes) = match session {
61
-
Some(row) => (row.get::<String, _>("did"), row.get::<Vec<u8>, _>("key_bytes")),
62
-
None => return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed"}))).into_response(),
63
-
};
64
-
65
-
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
66
-
return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"}))).into_response();
67
-
}
68
-
69
-
if input.repo != did {
70
-
return (StatusCode::FORBIDDEN, Json(json!({"error": "InvalidRepo", "message": "Repo does not match authenticated user"}))).into_response();
71
-
}
72
-
73
-
let user_query = sqlx::query("SELECT id FROM users WHERE did = $1")
74
-
.bind(&did)
75
-
.fetch_optional(&state.db)
76
-
.await;
77
-
78
-
let user_id: uuid::Uuid = match user_query {
79
-
Ok(Some(row)) => row.get("id"),
80
-
_ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "User not found"}))).into_response(),
81
-
};
82
-
83
-
let repo_root_query = sqlx::query("SELECT repo_root_cid FROM repos WHERE user_id = $1")
84
-
.bind(user_id)
85
-
.fetch_optional(&state.db)
86
-
.await;
87
-
88
-
let current_root_cid = match repo_root_query {
89
-
Ok(Some(row)) => {
90
-
let cid_str: String = row.get("repo_root_cid");
91
-
Cid::from_str(&cid_str).ok()
92
-
},
93
-
_ => None,
94
-
};
95
-
96
-
if current_root_cid.is_none() {
97
-
error!("Repo root not found for user {}", did);
98
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Repo root not found"}))).into_response();
99
-
}
100
-
let current_root_cid = current_root_cid.unwrap();
101
-
102
-
let commit_bytes = match state.block_store.get(¤t_root_cid).await {
103
-
Ok(Some(b)) => b,
104
-
Ok(None) => {
105
-
error!("Commit block not found: {}", current_root_cid);
106
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
107
-
},
108
-
Err(e) => {
109
-
error!("Failed to load commit block: {:?}", e);
110
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
111
-
}
112
-
};
113
-
114
-
let commit = match Commit::from_cbor(&commit_bytes) {
115
-
Ok(c) => c,
116
-
Err(e) => {
117
-
error!("Failed to parse commit: {:?}", e);
118
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
119
-
}
120
-
};
121
-
122
-
let mst_root = commit.data;
123
-
let store = Arc::new(state.block_store.clone());
124
-
let mst = Mst::load(store.clone(), mst_root, None);
125
-
126
-
let collection_nsid = match input.collection.parse::<Nsid>() {
127
-
Ok(n) => n,
128
-
Err(_) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidCollection"}))).into_response(),
129
-
};
130
-
131
-
let rkey = input.rkey.unwrap_or_else(|| {
132
-
Utc::now().format("%Y%m%d%H%M%S%f").to_string()
133
-
});
134
-
135
-
let mut record_bytes = Vec::new();
136
-
if let Err(e) = serde_ipld_dagcbor::to_writer(&mut record_bytes, &input.record) {
137
-
error!("Error serializing record: {:?}", e);
138
-
return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidRecord", "message": "Failed to serialize record"}))).into_response();
139
-
}
140
-
141
-
let record_cid = match state.block_store.put(&record_bytes).await {
142
-
Ok(c) => c,
143
-
Err(e) => {
144
-
error!("Failed to save record block: {:?}", e);
145
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
146
-
}
147
-
};
148
-
149
-
let key = format!("{}/{}", collection_nsid, rkey);
150
-
if let Err(e) = mst.update(&key, record_cid).await {
151
-
error!("Failed to update MST: {:?}", e);
152
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
153
-
}
154
-
155
-
let new_mst_root = match mst.root().await {
156
-
Ok(c) => c,
157
-
Err(e) => {
158
-
error!("Failed to get new MST root: {:?}", e);
159
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
160
-
}
161
-
};
162
-
163
-
let did_obj = match Did::new(&did) {
164
-
Ok(d) => d,
165
-
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Invalid DID"}))).into_response(),
166
-
};
167
-
168
-
let rev = Tid::now(LimitedU32::MIN);
169
-
170
-
let new_commit = Commit::new_unsigned(
171
-
did_obj,
172
-
new_mst_root,
173
-
rev,
174
-
Some(current_root_cid)
175
-
);
176
-
177
-
let new_commit_bytes = match new_commit.to_cbor() {
178
-
Ok(b) => b,
179
-
Err(e) => {
180
-
error!("Failed to serialize new commit: {:?}", e);
181
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
182
-
}
183
-
};
184
-
185
-
let new_root_cid = match state.block_store.put(&new_commit_bytes).await {
186
-
Ok(c) => c,
187
-
Err(e) => {
188
-
error!("Failed to save new commit: {:?}", e);
189
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
190
-
}
191
-
};
192
-
193
-
let update_repo = sqlx::query("UPDATE repos SET repo_root_cid = $1 WHERE user_id = $2")
194
-
.bind(new_root_cid.to_string())
195
-
.bind(user_id)
196
-
.execute(&state.db)
197
-
.await;
198
-
199
-
if let Err(e) = update_repo {
200
-
error!("Failed to update repo root in DB: {:?}", e);
201
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
202
-
}
203
-
204
-
let record_insert = sqlx::query(
205
-
"INSERT INTO records (repo_id, collection, rkey, record_cid) VALUES ($1, $2, $3, $4)
206
-
ON CONFLICT (repo_id, collection, rkey) DO UPDATE SET record_cid = $4, created_at = NOW()"
207
-
)
208
-
.bind(user_id)
209
-
.bind(&input.collection)
210
-
.bind(&rkey)
211
-
.bind(record_cid.to_string())
212
-
.execute(&state.db)
213
-
.await;
214
-
215
-
if let Err(e) = record_insert {
216
-
error!("Error inserting record index: {:?}", e);
217
-
}
218
-
219
-
let output = CreateRecordOutput {
220
-
uri: format!("at://{}/{}/{}", input.repo, input.collection, rkey),
221
-
cid: record_cid.to_string(),
222
-
};
223
-
(StatusCode::OK, Json(output)).into_response()
224
-
}
225
-
226
-
#[derive(Deserialize)]
227
-
#[allow(dead_code)]
228
-
pub struct PutRecordInput {
229
-
pub repo: String,
230
-
pub collection: String,
231
-
pub rkey: String,
232
-
pub validate: Option<bool>,
233
-
pub record: serde_json::Value,
234
-
#[serde(rename = "swapCommit")]
235
-
pub swap_commit: Option<String>,
236
-
}
237
-
238
-
#[derive(Serialize)]
239
-
#[serde(rename_all = "camelCase")]
240
-
pub struct PutRecordOutput {
241
-
pub uri: String,
242
-
pub cid: String,
243
-
}
244
-
245
-
pub async fn put_record(
246
-
State(state): State<AppState>,
247
-
headers: axum::http::HeaderMap,
248
-
Json(input): Json<PutRecordInput>,
249
-
) -> Response {
250
-
let auth_header = headers.get("Authorization");
251
-
if auth_header.is_none() {
252
-
return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationRequired"}))).into_response();
253
-
}
254
-
let token = auth_header.unwrap().to_str().unwrap_or("").replace("Bearer ", "");
255
-
256
-
let session = sqlx::query(
257
-
"SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.access_jwt = $1"
258
-
)
259
-
.bind(&token)
260
-
.fetch_optional(&state.db)
261
-
.await
262
-
.unwrap_or(None);
263
-
264
-
let (did, key_bytes) = match session {
265
-
Some(row) => (row.get::<String, _>("did"), row.get::<Vec<u8>, _>("key_bytes")),
266
-
None => return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed"}))).into_response(),
267
-
};
268
-
269
-
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
270
-
return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"}))).into_response();
271
-
}
272
-
273
-
if input.repo != did {
274
-
return (StatusCode::FORBIDDEN, Json(json!({"error": "InvalidRepo", "message": "Repo does not match authenticated user"}))).into_response();
275
-
}
276
-
277
-
let user_query = sqlx::query("SELECT id FROM users WHERE did = $1")
278
-
.bind(&did)
279
-
.fetch_optional(&state.db)
280
-
.await;
281
-
282
-
let user_id: uuid::Uuid = match user_query {
283
-
Ok(Some(row)) => row.get("id"),
284
-
_ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "User not found"}))).into_response(),
285
-
};
286
-
287
-
let repo_root_query = sqlx::query("SELECT repo_root_cid FROM repos WHERE user_id = $1")
288
-
.bind(user_id)
289
-
.fetch_optional(&state.db)
290
-
.await;
291
-
292
-
let current_root_cid = match repo_root_query {
293
-
Ok(Some(row)) => {
294
-
let cid_str: String = row.get("repo_root_cid");
295
-
Cid::from_str(&cid_str).ok()
296
-
},
297
-
_ => None,
298
-
};
299
-
300
-
if current_root_cid.is_none() {
301
-
error!("Repo root not found for user {}", did);
302
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Repo root not found"}))).into_response();
303
-
}
304
-
let current_root_cid = current_root_cid.unwrap();
305
-
306
-
let commit_bytes = match state.block_store.get(¤t_root_cid).await {
307
-
Ok(Some(b)) => b,
308
-
Ok(None) => {
309
-
error!("Commit block not found: {}", current_root_cid);
310
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Commit block not found"}))).into_response();
311
-
},
312
-
Err(e) => {
313
-
error!("Failed to load commit block: {:?}", e);
314
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to load commit block"}))).into_response();
315
-
}
316
-
};
317
-
318
-
let commit = match Commit::from_cbor(&commit_bytes) {
319
-
Ok(c) => c,
320
-
Err(e) => {
321
-
error!("Failed to parse commit: {:?}", e);
322
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to parse commit"}))).into_response();
323
-
}
324
-
};
325
-
326
-
let mst_root = commit.data;
327
-
let store = Arc::new(state.block_store.clone());
328
-
let mst = Mst::load(store.clone(), mst_root, None);
329
-
330
-
let collection_nsid = match input.collection.parse::<Nsid>() {
331
-
Ok(n) => n,
332
-
Err(_) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidCollection"}))).into_response(),
333
-
};
334
-
335
-
let rkey = input.rkey.clone();
336
-
337
-
let mut record_bytes = Vec::new();
338
-
if let Err(e) = serde_ipld_dagcbor::to_writer(&mut record_bytes, &input.record) {
339
-
error!("Error serializing record: {:?}", e);
340
-
return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidRecord", "message": "Failed to serialize record"}))).into_response();
341
-
}
342
-
343
-
let record_cid = match state.block_store.put(&record_bytes).await {
344
-
Ok(c) => c,
345
-
Err(e) => {
346
-
error!("Failed to save record block: {:?}", e);
347
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to save record block"}))).into_response();
348
-
}
349
-
};
350
-
351
-
let key = format!("{}/{}", collection_nsid, rkey);
352
-
if let Err(e) = mst.update(&key, record_cid).await {
353
-
error!("Failed to update MST: {:?}", e);
354
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": format!("Failed to update MST: {:?}", e)}))).into_response();
355
-
}
356
-
357
-
let new_mst_root = match mst.root().await {
358
-
Ok(c) => c,
359
-
Err(e) => {
360
-
error!("Failed to get new MST root: {:?}", e);
361
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to get new MST root"}))).into_response();
362
-
}
363
-
};
364
-
365
-
let did_obj = match Did::new(&did) {
366
-
Ok(d) => d,
367
-
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Invalid DID"}))).into_response(),
368
-
};
369
-
370
-
let rev = Tid::now(LimitedU32::MIN);
371
-
372
-
let new_commit = Commit::new_unsigned(
373
-
did_obj,
374
-
new_mst_root,
375
-
rev,
376
-
Some(current_root_cid)
377
-
);
378
-
379
-
let new_commit_bytes = match new_commit.to_cbor() {
380
-
Ok(b) => b,
381
-
Err(e) => {
382
-
error!("Failed to serialize new commit: {:?}", e);
383
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to serialize new commit"}))).into_response();
384
-
}
385
-
};
386
-
387
-
let new_root_cid = match state.block_store.put(&new_commit_bytes).await {
388
-
Ok(c) => c,
389
-
Err(e) => {
390
-
error!("Failed to save new commit: {:?}", e);
391
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to save new commit"}))).into_response();
392
-
}
393
-
};
394
-
395
-
let update_repo = sqlx::query("UPDATE repos SET repo_root_cid = $1 WHERE user_id = $2")
396
-
.bind(new_root_cid.to_string())
397
-
.bind(user_id)
398
-
.execute(&state.db)
399
-
.await;
400
-
401
-
if let Err(e) = update_repo {
402
-
error!("Failed to update repo root in DB: {:?}", e);
403
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to update repo root in DB"}))).into_response();
404
-
}
405
-
406
-
let record_insert = sqlx::query(
407
-
"INSERT INTO records (repo_id, collection, rkey, record_cid) VALUES ($1, $2, $3, $4)
408
-
ON CONFLICT (repo_id, collection, rkey) DO UPDATE SET record_cid = $4, created_at = NOW()"
409
-
)
410
-
.bind(user_id)
411
-
.bind(&input.collection)
412
-
.bind(&rkey)
413
-
.bind(record_cid.to_string())
414
-
.execute(&state.db)
415
-
.await;
416
-
417
-
if let Err(e) = record_insert {
418
-
error!("Error inserting record index: {:?}", e);
419
-
}
420
-
421
-
let output = PutRecordOutput {
422
-
uri: format!("at://{}/{}/{}", input.repo, input.collection, rkey),
423
-
cid: record_cid.to_string(),
424
-
};
425
-
(StatusCode::OK, Json(output)).into_response()
426
-
}
427
-
428
-
#[derive(Deserialize)]
429
-
pub struct GetRecordInput {
430
-
pub repo: String,
431
-
pub collection: String,
432
-
pub rkey: String,
433
-
pub cid: Option<String>,
434
-
}
435
-
436
-
pub async fn get_record(
437
-
State(state): State<AppState>,
438
-
Query(input): Query<GetRecordInput>,
439
-
) -> Response {
440
-
let user_row = if input.repo.starts_with("did:") {
441
-
sqlx::query("SELECT id FROM users WHERE did = $1")
442
-
.bind(&input.repo)
443
-
.fetch_optional(&state.db)
444
-
.await
445
-
} else {
446
-
sqlx::query("SELECT id FROM users WHERE handle = $1")
447
-
.bind(&input.repo)
448
-
.fetch_optional(&state.db)
449
-
.await
450
-
};
451
-
452
-
let user_id: uuid::Uuid = match user_row {
453
-
Ok(Some(row)) => row.get("id"),
454
-
_ => return (StatusCode::NOT_FOUND, Json(json!({"error": "NotFound", "message": "Repo not found"}))).into_response(),
455
-
};
456
-
457
-
let record_row = sqlx::query("SELECT record_cid FROM records WHERE repo_id = $1 AND collection = $2 AND rkey = $3")
458
-
.bind(user_id)
459
-
.bind(&input.collection)
460
-
.bind(&input.rkey)
461
-
.fetch_optional(&state.db)
462
-
.await;
463
-
464
-
let record_cid_str: String = match record_row {
465
-
Ok(Some(row)) => row.get("record_cid"),
466
-
_ => return (StatusCode::NOT_FOUND, Json(json!({"error": "NotFound", "message": "Record not found"}))).into_response(),
467
-
};
468
-
469
-
if let Some(expected_cid) = &input.cid {
470
-
if &record_cid_str != expected_cid {
471
-
return (StatusCode::NOT_FOUND, Json(json!({"error": "NotFound", "message": "Record CID mismatch"}))).into_response();
472
-
}
473
-
}
474
-
475
-
let cid = match Cid::from_str(&record_cid_str) {
476
-
Ok(c) => c,
477
-
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Invalid CID in DB"}))).into_response(),
478
-
};
479
-
480
-
let block = match state.block_store.get(&cid).await {
481
-
Ok(Some(b)) => b,
482
-
_ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Record block not found"}))).into_response(),
483
-
};
484
-
485
-
let value: serde_json::Value = match serde_ipld_dagcbor::from_slice(&block) {
486
-
Ok(v) => v,
487
-
Err(e) => {
488
-
error!("Failed to deserialize record: {:?}", e);
489
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
490
-
}
491
-
};
492
-
493
-
Json(json!({
494
-
"uri": format!("at://{}/{}/{}", input.repo, input.collection, input.rkey),
495
-
"cid": record_cid_str,
496
-
"value": value
497
-
})).into_response()
498
-
}
499
-
500
-
#[derive(Deserialize)]
501
-
pub struct DeleteRecordInput {
502
-
pub repo: String,
503
-
pub collection: String,
504
-
pub rkey: String,
505
-
#[serde(rename = "swapRecord")]
506
-
pub swap_record: Option<String>,
507
-
#[serde(rename = "swapCommit")]
508
-
pub swap_commit: Option<String>,
509
-
}
510
-
511
-
pub async fn delete_record(
512
-
State(state): State<AppState>,
513
-
headers: axum::http::HeaderMap,
514
-
Json(input): Json<DeleteRecordInput>,
515
-
) -> Response {
516
-
let auth_header = headers.get("Authorization");
517
-
if auth_header.is_none() {
518
-
return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationRequired"}))).into_response();
519
-
}
520
-
let token = auth_header.unwrap().to_str().unwrap_or("").replace("Bearer ", "");
521
-
522
-
let session = sqlx::query(
523
-
"SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.access_jwt = $1"
524
-
)
525
-
.bind(&token)
526
-
.fetch_optional(&state.db)
527
-
.await
528
-
.unwrap_or(None);
529
-
530
-
let (did, key_bytes) = match session {
531
-
Some(row) => (row.get::<String, _>("did"), row.get::<Vec<u8>, _>("key_bytes")),
532
-
None => return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed"}))).into_response(),
533
-
};
534
-
535
-
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
536
-
return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"}))).into_response();
537
-
}
538
-
539
-
if input.repo != did {
540
-
return (StatusCode::FORBIDDEN, Json(json!({"error": "InvalidRepo", "message": "Repo does not match authenticated user"}))).into_response();
541
-
}
542
-
543
-
let user_query = sqlx::query("SELECT id FROM users WHERE did = $1")
544
-
.bind(&did)
545
-
.fetch_optional(&state.db)
546
-
.await;
547
-
548
-
let user_id: uuid::Uuid = match user_query {
549
-
Ok(Some(row)) => row.get("id"),
550
-
_ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "User not found"}))).into_response(),
551
-
};
552
-
553
-
let repo_root_query = sqlx::query("SELECT repo_root_cid FROM repos WHERE user_id = $1")
554
-
.bind(user_id)
555
-
.fetch_optional(&state.db)
556
-
.await;
557
-
558
-
let current_root_cid = match repo_root_query {
559
-
Ok(Some(row)) => {
560
-
let cid_str: String = row.get("repo_root_cid");
561
-
Cid::from_str(&cid_str).ok()
562
-
},
563
-
_ => None,
564
-
};
565
-
566
-
if current_root_cid.is_none() {
567
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Repo root not found"}))).into_response();
568
-
}
569
-
let current_root_cid = current_root_cid.unwrap();
570
-
571
-
let commit_bytes = match state.block_store.get(¤t_root_cid).await {
572
-
Ok(Some(b)) => b,
573
-
Ok(None) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Commit block not found"}))).into_response(),
574
-
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": format!("Failed to load commit block: {:?}", e)}))).into_response(),
575
-
};
576
-
577
-
let commit = match Commit::from_cbor(&commit_bytes) {
578
-
Ok(c) => c,
579
-
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": format!("Failed to parse commit: {:?}", e)}))).into_response(),
580
-
};
581
-
582
-
let mst_root = commit.data;
583
-
let store = Arc::new(state.block_store.clone());
584
-
let mst = Mst::load(store.clone(), mst_root, None);
585
-
586
-
let collection_nsid = match input.collection.parse::<Nsid>() {
587
-
Ok(n) => n,
588
-
Err(_) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidCollection"}))).into_response(),
589
-
};
590
-
591
-
let key = format!("{}/{}", collection_nsid, input.rkey);
592
-
593
-
// TODO: Check swapRecord if provided? Skipping for brevity/robustness
594
-
595
-
if let Err(e) = mst.delete(&key).await {
596
-
error!("Failed to delete from MST: {:?}", e);
597
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": format!("Failed to delete from MST: {:?}", e)}))).into_response();
598
-
}
599
-
600
-
let new_mst_root = match mst.root().await {
601
-
Ok(c) => c,
602
-
Err(_e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to get new MST root"}))).into_response(),
603
-
};
604
-
605
-
let did_obj = match Did::new(&did) {
606
-
Ok(d) => d,
607
-
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Invalid DID"}))).into_response(),
608
-
};
609
-
610
-
let rev = Tid::now(LimitedU32::MIN);
611
-
612
-
let new_commit = Commit::new_unsigned(
613
-
did_obj,
614
-
new_mst_root,
615
-
rev,
616
-
Some(current_root_cid)
617
-
);
618
-
619
-
let new_commit_bytes = match new_commit.to_cbor() {
620
-
Ok(b) => b,
621
-
Err(_e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to serialize new commit"}))).into_response(),
622
-
};
623
-
624
-
let new_root_cid = match state.block_store.put(&new_commit_bytes).await {
625
-
Ok(c) => c,
626
-
Err(_e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to save new commit"}))).into_response(),
627
-
};
628
-
629
-
let update_repo = sqlx::query("UPDATE repos SET repo_root_cid = $1 WHERE user_id = $2")
630
-
.bind(new_root_cid.to_string())
631
-
.bind(user_id)
632
-
.execute(&state.db)
633
-
.await;
634
-
635
-
if let Err(e) = update_repo {
636
-
error!("Failed to update repo root in DB: {:?}", e);
637
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to update repo root in DB"}))).into_response();
638
-
}
639
-
640
-
let record_delete = sqlx::query("DELETE FROM records WHERE repo_id = $1 AND collection = $2 AND rkey = $3")
641
-
.bind(user_id)
642
-
.bind(&input.collection)
643
-
.bind(&input.rkey)
644
-
.execute(&state.db)
645
-
.await;
646
-
647
-
if let Err(e) = record_delete {
648
-
error!("Error deleting record index: {:?}", e);
649
-
}
650
-
651
-
(StatusCode::OK, Json(json!({}))).into_response()
652
-
}
653
-
654
-
#[derive(Deserialize)]
655
-
pub struct ListRecordsInput {
656
-
pub repo: String,
657
-
pub collection: String,
658
-
pub limit: Option<i32>,
659
-
pub cursor: Option<String>,
660
-
#[serde(rename = "rkeyStart")]
661
-
pub rkey_start: Option<String>,
662
-
#[serde(rename = "rkeyEnd")]
663
-
pub rkey_end: Option<String>,
664
-
pub reverse: Option<bool>,
665
-
}
666
-
667
-
#[derive(Serialize)]
668
-
pub struct ListRecordsOutput {
669
-
pub cursor: Option<String>,
670
-
pub records: Vec<serde_json::Value>,
671
-
}
672
-
673
-
pub async fn list_records(
674
-
State(state): State<AppState>,
675
-
Query(input): Query<ListRecordsInput>,
676
-
) -> Response {
677
-
let user_row = if input.repo.starts_with("did:") {
678
-
sqlx::query("SELECT id FROM users WHERE did = $1")
679
-
.bind(&input.repo)
680
-
.fetch_optional(&state.db)
681
-
.await
682
-
} else {
683
-
sqlx::query("SELECT id FROM users WHERE handle = $1")
684
-
.bind(&input.repo)
685
-
.fetch_optional(&state.db)
686
-
.await
687
-
};
688
-
689
-
let user_id: uuid::Uuid = match user_row {
690
-
Ok(Some(row)) => row.get("id"),
691
-
_ => return (StatusCode::NOT_FOUND, Json(json!({"error": "NotFound", "message": "Repo not found"}))).into_response(),
692
-
};
693
-
694
-
let limit = input.limit.unwrap_or(50).clamp(1, 100);
695
-
let reverse = input.reverse.unwrap_or(false);
696
-
697
-
// Simplistic query construction - no sophisticated cursor handling or rkey ranges for now, just basic pagination
698
-
// TODO: Implement rkeyStart/End and correct cursor logic
699
-
700
-
let query_str = format!(
701
-
"SELECT rkey, record_cid FROM records WHERE repo_id = $1 AND collection = $2 {} ORDER BY rkey {} LIMIT {}",
702
-
if let Some(_c) = &input.cursor {
703
-
if reverse { "AND rkey < $3" } else { "AND rkey > $3" }
704
-
} else {
705
-
""
706
-
},
707
-
if reverse { "DESC" } else { "ASC" },
708
-
limit
709
-
);
710
-
711
-
let mut query = sqlx::query(&query_str)
712
-
.bind(user_id)
713
-
.bind(&input.collection);
714
-
715
-
if let Some(c) = &input.cursor {
716
-
query = query.bind(c);
717
-
}
718
-
719
-
let rows = match query.fetch_all(&state.db).await {
720
-
Ok(r) => r,
721
-
Err(e) => {
722
-
error!("Error listing records: {:?}", e);
723
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
724
-
}
725
-
};
726
-
727
-
let mut records = Vec::new();
728
-
let mut last_rkey = None;
729
-
730
-
for row in rows {
731
-
let rkey: String = row.get("rkey");
732
-
let cid_str: String = row.get("record_cid");
733
-
last_rkey = Some(rkey.clone());
734
-
735
-
if let Ok(cid) = Cid::from_str(&cid_str) {
736
-
if let Ok(Some(block)) = state.block_store.get(&cid).await {
737
-
if let Ok(value) = serde_ipld_dagcbor::from_slice::<serde_json::Value>(&block) {
738
-
records.push(json!({
739
-
"uri": format!("at://{}/{}/{}", input.repo, input.collection, rkey),
740
-
"cid": cid_str,
741
-
"value": value
742
-
}));
743
-
}
744
-
}
745
-
}
746
-
}
747
-
748
-
Json(ListRecordsOutput {
749
-
cursor: last_rkey,
750
-
records,
751
-
}).into_response()
752
-
}
753
-
754
-
#[derive(Deserialize)]
755
-
pub struct DescribeRepoInput {
756
-
pub repo: String,
757
-
}
758
-
759
-
pub async fn describe_repo(
760
-
State(state): State<AppState>,
761
-
Query(input): Query<DescribeRepoInput>,
762
-
) -> Response {
763
-
let user_row = if input.repo.starts_with("did:") {
764
-
sqlx::query("SELECT id, handle, did FROM users WHERE did = $1")
765
-
.bind(&input.repo)
766
-
.fetch_optional(&state.db)
767
-
.await
768
-
} else {
769
-
sqlx::query("SELECT id, handle, did FROM users WHERE handle = $1")
770
-
.bind(&input.repo)
771
-
.fetch_optional(&state.db)
772
-
.await
773
-
};
774
-
775
-
let (user_id, handle, did) = match user_row {
776
-
Ok(Some(row)) => (row.get::<uuid::Uuid, _>("id"), row.get::<String, _>("handle"), row.get::<String, _>("did")),
777
-
_ => return (StatusCode::NOT_FOUND, Json(json!({"error": "NotFound", "message": "Repo not found"}))).into_response(),
778
-
};
779
-
780
-
let collections_query = sqlx::query("SELECT DISTINCT collection FROM records WHERE repo_id = $1")
781
-
.bind(user_id)
782
-
.fetch_all(&state.db)
783
-
.await;
784
-
785
-
let collections: Vec<String> = match collections_query {
786
-
Ok(rows) => rows.iter().map(|r| r.get("collection")).collect(),
787
-
Err(_) => Vec::new(),
788
-
};
789
-
790
-
let did_doc = json!({
791
-
"id": did,
792
-
"alsoKnownAs": [format!("at://{}", handle)]
793
-
});
794
-
795
-
Json(json!({
796
-
"handle": handle,
797
-
"did": did,
798
-
"didDoc": did_doc,
799
-
"collections": collections,
800
-
"handleIsCorrect": true
801
-
})).into_response()
802
-
}
803
-
804
-
pub async fn upload_blob(
805
-
State(state): State<AppState>,
806
-
headers: axum::http::HeaderMap,
807
-
body: Bytes,
808
-
) -> Response {
809
-
let auth_header = headers.get("Authorization");
810
-
if auth_header.is_none() {
811
-
return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationRequired"}))).into_response();
812
-
}
813
-
let token = auth_header.unwrap().to_str().unwrap_or("").replace("Bearer ", "");
814
-
815
-
let session = sqlx::query(
816
-
"SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.access_jwt = $1"
817
-
)
818
-
.bind(&token)
819
-
.fetch_optional(&state.db)
820
-
.await
821
-
.unwrap_or(None);
822
-
823
-
let (did, key_bytes) = match session {
824
-
Some(row) => (row.get::<String, _>("did"), row.get::<Vec<u8>, _>("key_bytes")),
825
-
None => return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed"}))).into_response(),
826
-
};
827
-
828
-
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
829
-
return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"}))).into_response();
830
-
}
831
-
832
-
let mime_type = headers.get("content-type")
833
-
.and_then(|h| h.to_str().ok())
834
-
.unwrap_or("application/octet-stream")
835
-
.to_string();
836
-
837
-
let size = body.len() as i64;
838
-
let data = body.to_vec();
839
-
840
-
let mut hasher = Sha256::new();
841
-
hasher.update(&data);
842
-
let hash = hasher.finalize();
843
-
let multihash = Multihash::wrap(0x12, &hash).unwrap();
844
-
let cid = Cid::new_v1(0x55, multihash);
845
-
let cid_str = cid.to_string();
846
-
847
-
let storage_key = format!("blobs/{}", cid_str);
848
-
849
-
if let Err(e) = state.blob_store.put(&storage_key, &data).await {
850
-
error!("Failed to upload blob to storage: {:?}", e);
851
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to store blob"}))).into_response();
852
-
}
853
-
854
-
let user_query = sqlx::query("SELECT id FROM users WHERE did = $1")
855
-
.bind(&did)
856
-
.fetch_optional(&state.db)
857
-
.await;
858
-
859
-
let user_id: uuid::Uuid = match user_query {
860
-
Ok(Some(row)) => row.get("id"),
861
-
_ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(),
862
-
};
863
-
864
-
let insert = sqlx::query(
865
-
"INSERT INTO blobs (cid, mime_type, size_bytes, created_by_user, storage_key) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (cid) DO NOTHING"
866
-
)
867
-
.bind(&cid_str)
868
-
.bind(&mime_type)
869
-
.bind(size)
870
-
.bind(user_id)
871
-
.bind(&storage_key)
872
-
.execute(&state.db)
873
-
.await;
874
-
875
-
if let Err(e) = insert {
876
-
error!("Failed to insert blob record: {:?}", e);
877
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
878
-
}
879
-
880
-
Json(json!({
881
-
"blob": {
882
-
"ref": {
883
-
"$link": cid_str
884
-
},
885
-
"mimeType": mime_type,
886
-
"size": size
887
-
}
888
-
})).into_response()
889
-
}
···
+138
src/api/repo/blob.rs
+138
src/api/repo/blob.rs
···
···
1
+
use crate::state::AppState;
2
+
use axum::body::Bytes;
3
+
use axum::{
4
+
Json,
5
+
extract::State,
6
+
http::StatusCode,
7
+
response::{IntoResponse, Response},
8
+
};
9
+
use cid::Cid;
10
+
use multihash::Multihash;
11
+
use serde_json::json;
12
+
use sha2::{Digest, Sha256};
13
+
use sqlx::Row;
14
+
use tracing::error;
15
+
16
+
pub async fn upload_blob(
17
+
State(state): State<AppState>,
18
+
headers: axum::http::HeaderMap,
19
+
body: Bytes,
20
+
) -> Response {
21
+
let auth_header = headers.get("Authorization");
22
+
if auth_header.is_none() {
23
+
return (
24
+
StatusCode::UNAUTHORIZED,
25
+
Json(json!({"error": "AuthenticationRequired"})),
26
+
)
27
+
.into_response();
28
+
}
29
+
let token = auth_header
30
+
.unwrap()
31
+
.to_str()
32
+
.unwrap_or("")
33
+
.replace("Bearer ", "");
34
+
35
+
let session = sqlx::query(
36
+
"SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.access_jwt = $1"
37
+
)
38
+
.bind(&token)
39
+
.fetch_optional(&state.db)
40
+
.await
41
+
.unwrap_or(None);
42
+
43
+
let (did, key_bytes) = match session {
44
+
Some(row) => (
45
+
row.get::<String, _>("did"),
46
+
row.get::<Vec<u8>, _>("key_bytes"),
47
+
),
48
+
None => {
49
+
return (
50
+
StatusCode::UNAUTHORIZED,
51
+
Json(json!({"error": "AuthenticationFailed"})),
52
+
)
53
+
.into_response();
54
+
}
55
+
};
56
+
57
+
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
58
+
return (
59
+
StatusCode::UNAUTHORIZED,
60
+
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
61
+
)
62
+
.into_response();
63
+
}
64
+
65
+
let mime_type = headers
66
+
.get("content-type")
67
+
.and_then(|h| h.to_str().ok())
68
+
.unwrap_or("application/octet-stream")
69
+
.to_string();
70
+
71
+
let size = body.len() as i64;
72
+
let data = body.to_vec();
73
+
74
+
let mut hasher = Sha256::new();
75
+
hasher.update(&data);
76
+
let hash = hasher.finalize();
77
+
let multihash = Multihash::wrap(0x12, &hash).unwrap();
78
+
let cid = Cid::new_v1(0x55, multihash);
79
+
let cid_str = cid.to_string();
80
+
81
+
let storage_key = format!("blobs/{}", cid_str);
82
+
83
+
if let Err(e) = state.blob_store.put(&storage_key, &data).await {
84
+
error!("Failed to upload blob to storage: {:?}", e);
85
+
return (
86
+
StatusCode::INTERNAL_SERVER_ERROR,
87
+
Json(json!({"error": "InternalError", "message": "Failed to store blob"})),
88
+
)
89
+
.into_response();
90
+
}
91
+
92
+
let user_query = sqlx::query("SELECT id FROM users WHERE did = $1")
93
+
.bind(&did)
94
+
.fetch_optional(&state.db)
95
+
.await;
96
+
97
+
let user_id: uuid::Uuid = match user_query {
98
+
Ok(Some(row)) => row.get("id"),
99
+
_ => {
100
+
return (
101
+
StatusCode::INTERNAL_SERVER_ERROR,
102
+
Json(json!({"error": "InternalError"})),
103
+
)
104
+
.into_response();
105
+
}
106
+
};
107
+
108
+
let insert = sqlx::query(
109
+
"INSERT INTO blobs (cid, mime_type, size_bytes, created_by_user, storage_key) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (cid) DO NOTHING"
110
+
)
111
+
.bind(&cid_str)
112
+
.bind(&mime_type)
113
+
.bind(size)
114
+
.bind(user_id)
115
+
.bind(&storage_key)
116
+
.execute(&state.db)
117
+
.await;
118
+
119
+
if let Err(e) = insert {
120
+
error!("Failed to insert blob record: {:?}", e);
121
+
return (
122
+
StatusCode::INTERNAL_SERVER_ERROR,
123
+
Json(json!({"error": "InternalError"})),
124
+
)
125
+
.into_response();
126
+
}
127
+
128
+
Json(json!({
129
+
"blob": {
130
+
"ref": {
131
+
"$link": cid_str
132
+
},
133
+
"mimeType": mime_type,
134
+
"size": size
135
+
}
136
+
}))
137
+
.into_response()
138
+
}
+72
src/api/repo/meta.rs
+72
src/api/repo/meta.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 sqlx::Row;
11
+
12
+
#[derive(Deserialize)]
13
+
pub struct DescribeRepoInput {
14
+
pub repo: String,
15
+
}
16
+
17
+
pub async fn describe_repo(
18
+
State(state): State<AppState>,
19
+
Query(input): Query<DescribeRepoInput>,
20
+
) -> Response {
21
+
let user_row = if input.repo.starts_with("did:") {
22
+
sqlx::query("SELECT id, handle, did FROM users WHERE did = $1")
23
+
.bind(&input.repo)
24
+
.fetch_optional(&state.db)
25
+
.await
26
+
} else {
27
+
sqlx::query("SELECT id, handle, did FROM users WHERE handle = $1")
28
+
.bind(&input.repo)
29
+
.fetch_optional(&state.db)
30
+
.await
31
+
};
32
+
33
+
let (user_id, handle, did) = match user_row {
34
+
Ok(Some(row)) => (
35
+
row.get::<uuid::Uuid, _>("id"),
36
+
row.get::<String, _>("handle"),
37
+
row.get::<String, _>("did"),
38
+
),
39
+
_ => {
40
+
return (
41
+
StatusCode::NOT_FOUND,
42
+
Json(json!({"error": "NotFound", "message": "Repo not found"})),
43
+
)
44
+
.into_response();
45
+
}
46
+
};
47
+
48
+
let collections_query =
49
+
sqlx::query("SELECT DISTINCT collection FROM records WHERE repo_id = $1")
50
+
.bind(user_id)
51
+
.fetch_all(&state.db)
52
+
.await;
53
+
54
+
let collections: Vec<String> = match collections_query {
55
+
Ok(rows) => rows.iter().map(|r| r.get("collection")).collect(),
56
+
Err(_) => Vec::new(),
57
+
};
58
+
59
+
let did_doc = json!({
60
+
"id": did,
61
+
"alsoKnownAs": [format!("at://{}", handle)]
62
+
});
63
+
64
+
Json(json!({
65
+
"handle": handle,
66
+
"did": did,
67
+
"didDoc": did_doc,
68
+
"collections": collections,
69
+
"handleIsCorrect": true
70
+
}))
71
+
.into_response()
72
+
}
+7
src/api/repo/mod.rs
+7
src/api/repo/mod.rs
+236
src/api/repo/record/delete.rs
+236
src/api/repo/record/delete.rs
···
···
1
+
use crate::state::AppState;
2
+
use axum::{
3
+
Json,
4
+
extract::State,
5
+
http::StatusCode,
6
+
response::{IntoResponse, Response},
7
+
};
8
+
use cid::Cid;
9
+
use jacquard::types::{
10
+
did::Did,
11
+
integer::LimitedU32,
12
+
string::{Nsid, Tid},
13
+
};
14
+
use jacquard_repo::{commit::Commit, mst::Mst, storage::BlockStore};
15
+
use serde::Deserialize;
16
+
use serde_json::json;
17
+
use sqlx::Row;
18
+
use std::str::FromStr;
19
+
use std::sync::Arc;
20
+
use tracing::error;
21
+
22
+
#[derive(Deserialize)]
23
+
pub struct DeleteRecordInput {
24
+
pub repo: String,
25
+
pub collection: String,
26
+
pub rkey: String,
27
+
#[serde(rename = "swapRecord")]
28
+
pub swap_record: Option<String>,
29
+
#[serde(rename = "swapCommit")]
30
+
pub swap_commit: Option<String>,
31
+
}
32
+
33
+
pub async fn delete_record(
34
+
State(state): State<AppState>,
35
+
headers: axum::http::HeaderMap,
36
+
Json(input): Json<DeleteRecordInput>,
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
+
let token = auth_header
47
+
.unwrap()
48
+
.to_str()
49
+
.unwrap_or("")
50
+
.replace("Bearer ", "");
51
+
52
+
let session = sqlx::query(
53
+
"SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.access_jwt = $1"
54
+
)
55
+
.bind(&token)
56
+
.fetch_optional(&state.db)
57
+
.await
58
+
.unwrap_or(None);
59
+
60
+
let (did, key_bytes) = match session {
61
+
Some(row) => (
62
+
row.get::<String, _>("did"),
63
+
row.get::<Vec<u8>, _>("key_bytes"),
64
+
),
65
+
None => {
66
+
return (
67
+
StatusCode::UNAUTHORIZED,
68
+
Json(json!({"error": "AuthenticationFailed"})),
69
+
)
70
+
.into_response();
71
+
}
72
+
};
73
+
74
+
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
75
+
return (
76
+
StatusCode::UNAUTHORIZED,
77
+
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
78
+
)
79
+
.into_response();
80
+
}
81
+
82
+
if input.repo != did {
83
+
return (StatusCode::FORBIDDEN, Json(json!({"error": "InvalidRepo", "message": "Repo does not match authenticated user"}))).into_response();
84
+
}
85
+
86
+
let user_query = sqlx::query("SELECT id FROM users WHERE did = $1")
87
+
.bind(&did)
88
+
.fetch_optional(&state.db)
89
+
.await;
90
+
91
+
let user_id: uuid::Uuid = match user_query {
92
+
Ok(Some(row)) => row.get("id"),
93
+
_ => {
94
+
return (
95
+
StatusCode::INTERNAL_SERVER_ERROR,
96
+
Json(json!({"error": "InternalError", "message": "User not found"})),
97
+
)
98
+
.into_response();
99
+
}
100
+
};
101
+
102
+
let repo_root_query = sqlx::query("SELECT repo_root_cid FROM repos WHERE user_id = $1")
103
+
.bind(user_id)
104
+
.fetch_optional(&state.db)
105
+
.await;
106
+
107
+
let current_root_cid = match repo_root_query {
108
+
Ok(Some(row)) => {
109
+
let cid_str: String = row.get("repo_root_cid");
110
+
Cid::from_str(&cid_str).ok()
111
+
}
112
+
_ => None,
113
+
};
114
+
115
+
if current_root_cid.is_none() {
116
+
return (
117
+
StatusCode::INTERNAL_SERVER_ERROR,
118
+
Json(json!({"error": "InternalError", "message": "Repo root not found"})),
119
+
)
120
+
.into_response();
121
+
}
122
+
let current_root_cid = current_root_cid.unwrap();
123
+
124
+
let commit_bytes = match state.block_store.get(¤t_root_cid).await {
125
+
Ok(Some(b)) => b,
126
+
Ok(None) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Commit block not found"}))).into_response(),
127
+
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": format!("Failed to load commit block: {:?}", e)}))).into_response(),
128
+
};
129
+
130
+
let commit = match Commit::from_cbor(&commit_bytes) {
131
+
Ok(c) => c,
132
+
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": format!("Failed to parse commit: {:?}", e)}))).into_response(),
133
+
};
134
+
135
+
let mst_root = commit.data;
136
+
let store = Arc::new(state.block_store.clone());
137
+
let mst = Mst::load(store.clone(), mst_root, None);
138
+
139
+
let collection_nsid = match input.collection.parse::<Nsid>() {
140
+
Ok(n) => n,
141
+
Err(_) => {
142
+
return (
143
+
StatusCode::BAD_REQUEST,
144
+
Json(json!({"error": "InvalidCollection"})),
145
+
)
146
+
.into_response();
147
+
}
148
+
};
149
+
150
+
let key = format!("{}/{}", collection_nsid, input.rkey);
151
+
152
+
// TODO: Check swapRecord if provided? Skipping for brevity/robustness
153
+
154
+
if let Err(e) = mst.delete(&key).await {
155
+
error!("Failed to delete from MST: {:?}", e);
156
+
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": format!("Failed to delete from MST: {:?}", e)}))).into_response();
157
+
}
158
+
159
+
let new_mst_root = match mst.root().await {
160
+
Ok(c) => c,
161
+
Err(_e) => {
162
+
return (
163
+
StatusCode::INTERNAL_SERVER_ERROR,
164
+
Json(json!({"error": "InternalError", "message": "Failed to get new MST root"})),
165
+
)
166
+
.into_response();
167
+
}
168
+
};
169
+
170
+
let did_obj = match Did::new(&did) {
171
+
Ok(d) => d,
172
+
Err(_) => {
173
+
return (
174
+
StatusCode::INTERNAL_SERVER_ERROR,
175
+
Json(json!({"error": "InternalError", "message": "Invalid DID"})),
176
+
)
177
+
.into_response();
178
+
}
179
+
};
180
+
181
+
let rev = Tid::now(LimitedU32::MIN);
182
+
183
+
let new_commit = Commit::new_unsigned(did_obj, new_mst_root, rev, Some(current_root_cid));
184
+
185
+
let new_commit_bytes =
186
+
match new_commit.to_cbor() {
187
+
Ok(b) => b,
188
+
Err(_e) => return (
189
+
StatusCode::INTERNAL_SERVER_ERROR,
190
+
Json(
191
+
json!({"error": "InternalError", "message": "Failed to serialize new commit"}),
192
+
),
193
+
)
194
+
.into_response(),
195
+
};
196
+
197
+
let new_root_cid = match state.block_store.put(&new_commit_bytes).await {
198
+
Ok(c) => c,
199
+
Err(_e) => {
200
+
return (
201
+
StatusCode::INTERNAL_SERVER_ERROR,
202
+
Json(json!({"error": "InternalError", "message": "Failed to save new commit"})),
203
+
)
204
+
.into_response();
205
+
}
206
+
};
207
+
208
+
let update_repo = sqlx::query("UPDATE repos SET repo_root_cid = $1 WHERE user_id = $2")
209
+
.bind(new_root_cid.to_string())
210
+
.bind(user_id)
211
+
.execute(&state.db)
212
+
.await;
213
+
214
+
if let Err(e) = update_repo {
215
+
error!("Failed to update repo root in DB: {:?}", e);
216
+
return (
217
+
StatusCode::INTERNAL_SERVER_ERROR,
218
+
Json(json!({"error": "InternalError", "message": "Failed to update repo root in DB"})),
219
+
)
220
+
.into_response();
221
+
}
222
+
223
+
let record_delete =
224
+
sqlx::query("DELETE FROM records WHERE repo_id = $1 AND collection = $2 AND rkey = $3")
225
+
.bind(user_id)
226
+
.bind(&input.collection)
227
+
.bind(&input.rkey)
228
+
.execute(&state.db)
229
+
.await;
230
+
231
+
if let Err(e) = record_delete {
232
+
error!("Error deleting record index: {:?}", e);
233
+
}
234
+
235
+
(StatusCode::OK, Json(json!({}))).into_response()
236
+
}
+10
src/api/repo/record/mod.rs
+10
src/api/repo/record/mod.rs
···
···
1
+
pub mod delete;
2
+
pub mod read;
3
+
pub mod write;
4
+
5
+
pub use delete::{DeleteRecordInput, delete_record};
6
+
pub use read::{GetRecordInput, ListRecordsInput, ListRecordsOutput, get_record, list_records};
7
+
pub use write::{
8
+
CreateRecordInput, CreateRecordOutput, PutRecordInput, PutRecordOutput, create_record,
9
+
put_record,
10
+
};
+236
src/api/repo/record/read.rs
+236
src/api/repo/record/read.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 cid::Cid;
9
+
use jacquard_repo::storage::BlockStore;
10
+
use serde::{Deserialize, Serialize};
11
+
use serde_json::json;
12
+
use sqlx::Row;
13
+
use std::str::FromStr;
14
+
use tracing::error;
15
+
16
+
#[derive(Deserialize)]
17
+
pub struct GetRecordInput {
18
+
pub repo: String,
19
+
pub collection: String,
20
+
pub rkey: String,
21
+
pub cid: Option<String>,
22
+
}
23
+
24
+
pub async fn get_record(
25
+
State(state): State<AppState>,
26
+
Query(input): Query<GetRecordInput>,
27
+
) -> Response {
28
+
let user_row = if input.repo.starts_with("did:") {
29
+
sqlx::query("SELECT id FROM users WHERE did = $1")
30
+
.bind(&input.repo)
31
+
.fetch_optional(&state.db)
32
+
.await
33
+
} else {
34
+
sqlx::query("SELECT id FROM users WHERE handle = $1")
35
+
.bind(&input.repo)
36
+
.fetch_optional(&state.db)
37
+
.await
38
+
};
39
+
40
+
let user_id: uuid::Uuid = match user_row {
41
+
Ok(Some(row)) => row.get("id"),
42
+
_ => {
43
+
return (
44
+
StatusCode::NOT_FOUND,
45
+
Json(json!({"error": "NotFound", "message": "Repo not found"})),
46
+
)
47
+
.into_response();
48
+
}
49
+
};
50
+
51
+
let record_row = sqlx::query(
52
+
"SELECT record_cid FROM records WHERE repo_id = $1 AND collection = $2 AND rkey = $3",
53
+
)
54
+
.bind(user_id)
55
+
.bind(&input.collection)
56
+
.bind(&input.rkey)
57
+
.fetch_optional(&state.db)
58
+
.await;
59
+
60
+
let record_cid_str: String = match record_row {
61
+
Ok(Some(row)) => row.get("record_cid"),
62
+
_ => {
63
+
return (
64
+
StatusCode::NOT_FOUND,
65
+
Json(json!({"error": "NotFound", "message": "Record not found"})),
66
+
)
67
+
.into_response();
68
+
}
69
+
};
70
+
71
+
if let Some(expected_cid) = &input.cid {
72
+
if &record_cid_str != expected_cid {
73
+
return (
74
+
StatusCode::NOT_FOUND,
75
+
Json(json!({"error": "NotFound", "message": "Record CID mismatch"})),
76
+
)
77
+
.into_response();
78
+
}
79
+
}
80
+
81
+
let cid = match Cid::from_str(&record_cid_str) {
82
+
Ok(c) => c,
83
+
Err(_) => {
84
+
return (
85
+
StatusCode::INTERNAL_SERVER_ERROR,
86
+
Json(json!({"error": "InternalError", "message": "Invalid CID in DB"})),
87
+
)
88
+
.into_response();
89
+
}
90
+
};
91
+
92
+
let block = match state.block_store.get(&cid).await {
93
+
Ok(Some(b)) => b,
94
+
_ => {
95
+
return (
96
+
StatusCode::INTERNAL_SERVER_ERROR,
97
+
Json(json!({"error": "InternalError", "message": "Record block not found"})),
98
+
)
99
+
.into_response();
100
+
}
101
+
};
102
+
103
+
let value: serde_json::Value = match serde_ipld_dagcbor::from_slice(&block) {
104
+
Ok(v) => v,
105
+
Err(e) => {
106
+
error!("Failed to deserialize record: {:?}", e);
107
+
return (
108
+
StatusCode::INTERNAL_SERVER_ERROR,
109
+
Json(json!({"error": "InternalError"})),
110
+
)
111
+
.into_response();
112
+
}
113
+
};
114
+
115
+
Json(json!({
116
+
"uri": format!("at://{}/{}/{}", input.repo, input.collection, input.rkey),
117
+
"cid": record_cid_str,
118
+
"value": value
119
+
}))
120
+
.into_response()
121
+
}
122
+
123
+
#[derive(Deserialize)]
124
+
pub struct ListRecordsInput {
125
+
pub repo: String,
126
+
pub collection: String,
127
+
pub limit: Option<i32>,
128
+
pub cursor: Option<String>,
129
+
#[serde(rename = "rkeyStart")]
130
+
pub rkey_start: Option<String>,
131
+
#[serde(rename = "rkeyEnd")]
132
+
pub rkey_end: Option<String>,
133
+
pub reverse: Option<bool>,
134
+
}
135
+
136
+
#[derive(Serialize)]
137
+
pub struct ListRecordsOutput {
138
+
pub cursor: Option<String>,
139
+
pub records: Vec<serde_json::Value>,
140
+
}
141
+
142
+
pub async fn list_records(
143
+
State(state): State<AppState>,
144
+
Query(input): Query<ListRecordsInput>,
145
+
) -> Response {
146
+
let user_row = if input.repo.starts_with("did:") {
147
+
sqlx::query("SELECT id FROM users WHERE did = $1")
148
+
.bind(&input.repo)
149
+
.fetch_optional(&state.db)
150
+
.await
151
+
} else {
152
+
sqlx::query("SELECT id FROM users WHERE handle = $1")
153
+
.bind(&input.repo)
154
+
.fetch_optional(&state.db)
155
+
.await
156
+
};
157
+
158
+
let user_id: uuid::Uuid = match user_row {
159
+
Ok(Some(row)) => row.get("id"),
160
+
_ => {
161
+
return (
162
+
StatusCode::NOT_FOUND,
163
+
Json(json!({"error": "NotFound", "message": "Repo not found"})),
164
+
)
165
+
.into_response();
166
+
}
167
+
};
168
+
169
+
let limit = input.limit.unwrap_or(50).clamp(1, 100);
170
+
let reverse = input.reverse.unwrap_or(false);
171
+
172
+
// Simplistic query construction - no sophisticated cursor handling or rkey ranges for now, just basic pagination
173
+
// TODO: Implement rkeyStart/End and correct cursor logic
174
+
175
+
let query_str = format!(
176
+
"SELECT rkey, record_cid FROM records WHERE repo_id = $1 AND collection = $2 {} ORDER BY rkey {} LIMIT {}",
177
+
if let Some(_c) = &input.cursor {
178
+
if reverse {
179
+
"AND rkey < $3"
180
+
} else {
181
+
"AND rkey > $3"
182
+
}
183
+
} else {
184
+
""
185
+
},
186
+
if reverse { "DESC" } else { "ASC" },
187
+
limit
188
+
);
189
+
190
+
let mut query = sqlx::query(&query_str)
191
+
.bind(user_id)
192
+
.bind(&input.collection);
193
+
194
+
if let Some(c) = &input.cursor {
195
+
query = query.bind(c);
196
+
}
197
+
198
+
let rows = match query.fetch_all(&state.db).await {
199
+
Ok(r) => r,
200
+
Err(e) => {
201
+
error!("Error listing records: {:?}", e);
202
+
return (
203
+
StatusCode::INTERNAL_SERVER_ERROR,
204
+
Json(json!({"error": "InternalError"})),
205
+
)
206
+
.into_response();
207
+
}
208
+
};
209
+
210
+
let mut records = Vec::new();
211
+
let mut last_rkey = None;
212
+
213
+
for row in rows {
214
+
let rkey: String = row.get("rkey");
215
+
let cid_str: String = row.get("record_cid");
216
+
last_rkey = Some(rkey.clone());
217
+
218
+
if let Ok(cid) = Cid::from_str(&cid_str) {
219
+
if let Ok(Some(block)) = state.block_store.get(&cid).await {
220
+
if let Ok(value) = serde_ipld_dagcbor::from_slice::<serde_json::Value>(&block) {
221
+
records.push(json!({
222
+
"uri": format!("at://{}/{}/{}", input.repo, input.collection, rkey),
223
+
"cid": cid_str,
224
+
"value": value
225
+
}));
226
+
}
227
+
}
228
+
}
229
+
}
230
+
231
+
Json(ListRecordsOutput {
232
+
cursor: last_rkey,
233
+
records,
234
+
})
235
+
.into_response()
236
+
}
+591
src/api/repo/record/write.rs
+591
src/api/repo/record/write.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::Utc;
9
+
use cid::Cid;
10
+
use jacquard::types::{
11
+
did::Did,
12
+
integer::LimitedU32,
13
+
string::{Nsid, Tid},
14
+
};
15
+
use jacquard_repo::{commit::Commit, mst::Mst, storage::BlockStore};
16
+
use serde::{Deserialize, Serialize};
17
+
use serde_json::json;
18
+
use sqlx::Row;
19
+
use std::str::FromStr;
20
+
use std::sync::Arc;
21
+
use tracing::error;
22
+
23
+
#[derive(Deserialize)]
24
+
#[allow(dead_code)]
25
+
pub struct CreateRecordInput {
26
+
pub repo: String,
27
+
pub collection: String,
28
+
pub rkey: Option<String>,
29
+
pub validate: Option<bool>,
30
+
pub record: serde_json::Value,
31
+
#[serde(rename = "swapCommit")]
32
+
pub swap_commit: Option<String>,
33
+
}
34
+
35
+
#[derive(Serialize)]
36
+
#[serde(rename_all = "camelCase")]
37
+
pub struct CreateRecordOutput {
38
+
pub uri: String,
39
+
pub cid: String,
40
+
}
41
+
42
+
pub async fn create_record(
43
+
State(state): State<AppState>,
44
+
headers: axum::http::HeaderMap,
45
+
Json(input): Json<CreateRecordInput>,
46
+
) -> Response {
47
+
let auth_header = headers.get("Authorization");
48
+
if auth_header.is_none() {
49
+
return (
50
+
StatusCode::UNAUTHORIZED,
51
+
Json(json!({"error": "AuthenticationRequired"})),
52
+
)
53
+
.into_response();
54
+
}
55
+
let token = auth_header
56
+
.unwrap()
57
+
.to_str()
58
+
.unwrap_or("")
59
+
.replace("Bearer ", "");
60
+
61
+
let session = sqlx::query(
62
+
"SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.access_jwt = $1"
63
+
)
64
+
.bind(&token)
65
+
.fetch_optional(&state.db)
66
+
.await
67
+
.unwrap_or(None);
68
+
69
+
let (did, key_bytes) = match session {
70
+
Some(row) => (
71
+
row.get::<String, _>("did"),
72
+
row.get::<Vec<u8>, _>("key_bytes"),
73
+
),
74
+
None => {
75
+
return (
76
+
StatusCode::UNAUTHORIZED,
77
+
Json(json!({"error": "AuthenticationFailed"})),
78
+
)
79
+
.into_response();
80
+
}
81
+
};
82
+
83
+
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
84
+
return (
85
+
StatusCode::UNAUTHORIZED,
86
+
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
87
+
)
88
+
.into_response();
89
+
}
90
+
91
+
if input.repo != did {
92
+
return (StatusCode::FORBIDDEN, Json(json!({"error": "InvalidRepo", "message": "Repo does not match authenticated user"}))).into_response();
93
+
}
94
+
95
+
let user_query = sqlx::query("SELECT id FROM users WHERE did = $1")
96
+
.bind(&did)
97
+
.fetch_optional(&state.db)
98
+
.await;
99
+
100
+
let user_id: uuid::Uuid = match user_query {
101
+
Ok(Some(row)) => row.get("id"),
102
+
_ => {
103
+
return (
104
+
StatusCode::INTERNAL_SERVER_ERROR,
105
+
Json(json!({"error": "InternalError", "message": "User not found"})),
106
+
)
107
+
.into_response();
108
+
}
109
+
};
110
+
111
+
let repo_root_query = sqlx::query("SELECT repo_root_cid FROM repos WHERE user_id = $1")
112
+
.bind(user_id)
113
+
.fetch_optional(&state.db)
114
+
.await;
115
+
116
+
let current_root_cid = match repo_root_query {
117
+
Ok(Some(row)) => {
118
+
let cid_str: String = row.get("repo_root_cid");
119
+
Cid::from_str(&cid_str).ok()
120
+
}
121
+
_ => None,
122
+
};
123
+
124
+
if current_root_cid.is_none() {
125
+
error!("Repo root not found for user {}", did);
126
+
return (
127
+
StatusCode::INTERNAL_SERVER_ERROR,
128
+
Json(json!({"error": "InternalError", "message": "Repo root not found"})),
129
+
)
130
+
.into_response();
131
+
}
132
+
let current_root_cid = current_root_cid.unwrap();
133
+
134
+
let commit_bytes = match state.block_store.get(¤t_root_cid).await {
135
+
Ok(Some(b)) => b,
136
+
Ok(None) => {
137
+
error!("Commit block not found: {}", current_root_cid);
138
+
return (
139
+
StatusCode::INTERNAL_SERVER_ERROR,
140
+
Json(json!({"error": "InternalError"})),
141
+
)
142
+
.into_response();
143
+
}
144
+
Err(e) => {
145
+
error!("Failed to load commit block: {:?}", e);
146
+
return (
147
+
StatusCode::INTERNAL_SERVER_ERROR,
148
+
Json(json!({"error": "InternalError"})),
149
+
)
150
+
.into_response();
151
+
}
152
+
};
153
+
154
+
let commit = match Commit::from_cbor(&commit_bytes) {
155
+
Ok(c) => c,
156
+
Err(e) => {
157
+
error!("Failed to parse commit: {:?}", e);
158
+
return (
159
+
StatusCode::INTERNAL_SERVER_ERROR,
160
+
Json(json!({"error": "InternalError"})),
161
+
)
162
+
.into_response();
163
+
}
164
+
};
165
+
166
+
let mst_root = commit.data;
167
+
let store = Arc::new(state.block_store.clone());
168
+
let mst = Mst::load(store.clone(), mst_root, None);
169
+
170
+
let collection_nsid = match input.collection.parse::<Nsid>() {
171
+
Ok(n) => n,
172
+
Err(_) => {
173
+
return (
174
+
StatusCode::BAD_REQUEST,
175
+
Json(json!({"error": "InvalidCollection"})),
176
+
)
177
+
.into_response();
178
+
}
179
+
};
180
+
181
+
let rkey = input
182
+
.rkey
183
+
.unwrap_or_else(|| Utc::now().format("%Y%m%d%H%M%S%f").to_string());
184
+
185
+
let mut record_bytes = Vec::new();
186
+
if let Err(e) = serde_ipld_dagcbor::to_writer(&mut record_bytes, &input.record) {
187
+
error!("Error serializing record: {:?}", e);
188
+
return (
189
+
StatusCode::BAD_REQUEST,
190
+
Json(json!({"error": "InvalidRecord", "message": "Failed to serialize record"})),
191
+
)
192
+
.into_response();
193
+
}
194
+
195
+
let record_cid = match state.block_store.put(&record_bytes).await {
196
+
Ok(c) => c,
197
+
Err(e) => {
198
+
error!("Failed to save record block: {:?}", e);
199
+
return (
200
+
StatusCode::INTERNAL_SERVER_ERROR,
201
+
Json(json!({"error": "InternalError"})),
202
+
)
203
+
.into_response();
204
+
}
205
+
};
206
+
207
+
let key = format!("{}/{}", collection_nsid, rkey);
208
+
if let Err(e) = mst.update(&key, record_cid).await {
209
+
error!("Failed to update MST: {:?}", e);
210
+
return (
211
+
StatusCode::INTERNAL_SERVER_ERROR,
212
+
Json(json!({"error": "InternalError"})),
213
+
)
214
+
.into_response();
215
+
}
216
+
217
+
let new_mst_root = match mst.root().await {
218
+
Ok(c) => c,
219
+
Err(e) => {
220
+
error!("Failed to get new MST root: {:?}", e);
221
+
return (
222
+
StatusCode::INTERNAL_SERVER_ERROR,
223
+
Json(json!({"error": "InternalError"})),
224
+
)
225
+
.into_response();
226
+
}
227
+
};
228
+
229
+
let did_obj = match Did::new(&did) {
230
+
Ok(d) => d,
231
+
Err(_) => {
232
+
return (
233
+
StatusCode::INTERNAL_SERVER_ERROR,
234
+
Json(json!({"error": "InternalError", "message": "Invalid DID"})),
235
+
)
236
+
.into_response();
237
+
}
238
+
};
239
+
240
+
let rev = Tid::now(LimitedU32::MIN);
241
+
242
+
let new_commit = Commit::new_unsigned(did_obj, new_mst_root, rev, Some(current_root_cid));
243
+
244
+
let new_commit_bytes = match new_commit.to_cbor() {
245
+
Ok(b) => b,
246
+
Err(e) => {
247
+
error!("Failed to serialize new commit: {:?}", e);
248
+
return (
249
+
StatusCode::INTERNAL_SERVER_ERROR,
250
+
Json(json!({"error": "InternalError"})),
251
+
)
252
+
.into_response();
253
+
}
254
+
};
255
+
256
+
let new_root_cid = match state.block_store.put(&new_commit_bytes).await {
257
+
Ok(c) => c,
258
+
Err(e) => {
259
+
error!("Failed to save new commit: {:?}", e);
260
+
return (
261
+
StatusCode::INTERNAL_SERVER_ERROR,
262
+
Json(json!({"error": "InternalError"})),
263
+
)
264
+
.into_response();
265
+
}
266
+
};
267
+
268
+
let update_repo = sqlx::query("UPDATE repos SET repo_root_cid = $1 WHERE user_id = $2")
269
+
.bind(new_root_cid.to_string())
270
+
.bind(user_id)
271
+
.execute(&state.db)
272
+
.await;
273
+
274
+
if let Err(e) = update_repo {
275
+
error!("Failed to update repo root in DB: {:?}", e);
276
+
return (
277
+
StatusCode::INTERNAL_SERVER_ERROR,
278
+
Json(json!({"error": "InternalError"})),
279
+
)
280
+
.into_response();
281
+
}
282
+
283
+
let record_insert = sqlx::query(
284
+
"INSERT INTO records (repo_id, collection, rkey, record_cid) VALUES ($1, $2, $3, $4)
285
+
ON CONFLICT (repo_id, collection, rkey) DO UPDATE SET record_cid = $4, created_at = NOW()",
286
+
)
287
+
.bind(user_id)
288
+
.bind(&input.collection)
289
+
.bind(&rkey)
290
+
.bind(record_cid.to_string())
291
+
.execute(&state.db)
292
+
.await;
293
+
294
+
if let Err(e) = record_insert {
295
+
error!("Error inserting record index: {:?}", e);
296
+
return (
297
+
StatusCode::INTERNAL_SERVER_ERROR,
298
+
Json(json!({"error": "InternalError", "message": "Failed to index record"})),
299
+
)
300
+
.into_response();
301
+
}
302
+
303
+
let output = CreateRecordOutput {
304
+
uri: format!("at://{}/{}/{}", input.repo, input.collection, rkey),
305
+
cid: record_cid.to_string(),
306
+
};
307
+
(StatusCode::OK, Json(output)).into_response()
308
+
}
309
+
310
+
#[derive(Deserialize)]
311
+
#[allow(dead_code)]
312
+
pub struct PutRecordInput {
313
+
pub repo: String,
314
+
pub collection: String,
315
+
pub rkey: String,
316
+
pub validate: Option<bool>,
317
+
pub record: serde_json::Value,
318
+
#[serde(rename = "swapCommit")]
319
+
pub swap_commit: Option<String>,
320
+
}
321
+
322
+
#[derive(Serialize)]
323
+
#[serde(rename_all = "camelCase")]
324
+
pub struct PutRecordOutput {
325
+
pub uri: String,
326
+
pub cid: String,
327
+
}
328
+
329
+
pub async fn put_record(
330
+
State(state): State<AppState>,
331
+
headers: axum::http::HeaderMap,
332
+
Json(input): Json<PutRecordInput>,
333
+
) -> Response {
334
+
let auth_header = headers.get("Authorization");
335
+
if auth_header.is_none() {
336
+
return (
337
+
StatusCode::UNAUTHORIZED,
338
+
Json(json!({"error": "AuthenticationRequired"})),
339
+
)
340
+
.into_response();
341
+
}
342
+
let token = auth_header
343
+
.unwrap()
344
+
.to_str()
345
+
.unwrap_or("")
346
+
.replace("Bearer ", "");
347
+
348
+
let session = sqlx::query(
349
+
"SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.access_jwt = $1"
350
+
)
351
+
.bind(&token)
352
+
.fetch_optional(&state.db)
353
+
.await
354
+
.unwrap_or(None);
355
+
356
+
let (did, key_bytes) = match session {
357
+
Some(row) => (
358
+
row.get::<String, _>("did"),
359
+
row.get::<Vec<u8>, _>("key_bytes"),
360
+
),
361
+
None => {
362
+
return (
363
+
StatusCode::UNAUTHORIZED,
364
+
Json(json!({"error": "AuthenticationFailed"})),
365
+
)
366
+
.into_response();
367
+
}
368
+
};
369
+
370
+
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
371
+
return (
372
+
StatusCode::UNAUTHORIZED,
373
+
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
374
+
)
375
+
.into_response();
376
+
}
377
+
378
+
if input.repo != did {
379
+
return (StatusCode::FORBIDDEN, Json(json!({"error": "InvalidRepo", "message": "Repo does not match authenticated user"}))).into_response();
380
+
}
381
+
382
+
let user_query = sqlx::query("SELECT id FROM users WHERE did = $1")
383
+
.bind(&did)
384
+
.fetch_optional(&state.db)
385
+
.await;
386
+
387
+
let user_id: uuid::Uuid = match user_query {
388
+
Ok(Some(row)) => row.get("id"),
389
+
_ => {
390
+
return (
391
+
StatusCode::INTERNAL_SERVER_ERROR,
392
+
Json(json!({"error": "InternalError", "message": "User not found"})),
393
+
)
394
+
.into_response();
395
+
}
396
+
};
397
+
398
+
let repo_root_query = sqlx::query("SELECT repo_root_cid FROM repos WHERE user_id = $1")
399
+
.bind(user_id)
400
+
.fetch_optional(&state.db)
401
+
.await;
402
+
403
+
let current_root_cid = match repo_root_query {
404
+
Ok(Some(row)) => {
405
+
let cid_str: String = row.get("repo_root_cid");
406
+
Cid::from_str(&cid_str).ok()
407
+
}
408
+
_ => None,
409
+
};
410
+
411
+
if current_root_cid.is_none() {
412
+
error!("Repo root not found for user {}", did);
413
+
return (
414
+
StatusCode::INTERNAL_SERVER_ERROR,
415
+
Json(json!({"error": "InternalError", "message": "Repo root not found"})),
416
+
)
417
+
.into_response();
418
+
}
419
+
let current_root_cid = current_root_cid.unwrap();
420
+
421
+
let commit_bytes = match state.block_store.get(¤t_root_cid).await {
422
+
Ok(Some(b)) => b,
423
+
Ok(None) => {
424
+
error!("Commit block not found: {}", current_root_cid);
425
+
return (
426
+
StatusCode::INTERNAL_SERVER_ERROR,
427
+
Json(json!({"error": "InternalError", "message": "Commit block not found"})),
428
+
)
429
+
.into_response();
430
+
}
431
+
Err(e) => {
432
+
error!("Failed to load commit block: {:?}", e);
433
+
return (
434
+
StatusCode::INTERNAL_SERVER_ERROR,
435
+
Json(json!({"error": "InternalError", "message": "Failed to load commit block"})),
436
+
)
437
+
.into_response();
438
+
}
439
+
};
440
+
441
+
let commit = match Commit::from_cbor(&commit_bytes) {
442
+
Ok(c) => c,
443
+
Err(e) => {
444
+
error!("Failed to parse commit: {:?}", e);
445
+
return (
446
+
StatusCode::INTERNAL_SERVER_ERROR,
447
+
Json(json!({"error": "InternalError", "message": "Failed to parse commit"})),
448
+
)
449
+
.into_response();
450
+
}
451
+
};
452
+
453
+
let mst_root = commit.data;
454
+
let store = Arc::new(state.block_store.clone());
455
+
let mst = Mst::load(store.clone(), mst_root, None);
456
+
457
+
let collection_nsid = match input.collection.parse::<Nsid>() {
458
+
Ok(n) => n,
459
+
Err(_) => {
460
+
return (
461
+
StatusCode::BAD_REQUEST,
462
+
Json(json!({"error": "InvalidCollection"})),
463
+
)
464
+
.into_response();
465
+
}
466
+
};
467
+
468
+
let rkey = input.rkey.clone();
469
+
470
+
let mut record_bytes = Vec::new();
471
+
if let Err(e) = serde_ipld_dagcbor::to_writer(&mut record_bytes, &input.record) {
472
+
error!("Error serializing record: {:?}", e);
473
+
return (
474
+
StatusCode::BAD_REQUEST,
475
+
Json(json!({"error": "InvalidRecord", "message": "Failed to serialize record"})),
476
+
)
477
+
.into_response();
478
+
}
479
+
480
+
let record_cid = match state.block_store.put(&record_bytes).await {
481
+
Ok(c) => c,
482
+
Err(e) => {
483
+
error!("Failed to save record block: {:?}", e);
484
+
return (
485
+
StatusCode::INTERNAL_SERVER_ERROR,
486
+
Json(json!({"error": "InternalError", "message": "Failed to save record block"})),
487
+
)
488
+
.into_response();
489
+
}
490
+
};
491
+
492
+
let key = format!("{}/{}", collection_nsid, rkey);
493
+
if let Err(e) = mst.update(&key, record_cid).await {
494
+
error!("Failed to update MST: {:?}", e);
495
+
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": format!("Failed to update MST: {:?}", e)}))).into_response();
496
+
}
497
+
498
+
let new_mst_root = match mst.root().await {
499
+
Ok(c) => c,
500
+
Err(e) => {
501
+
error!("Failed to get new MST root: {:?}", e);
502
+
return (
503
+
StatusCode::INTERNAL_SERVER_ERROR,
504
+
Json(json!({"error": "InternalError", "message": "Failed to get new MST root"})),
505
+
)
506
+
.into_response();
507
+
}
508
+
};
509
+
510
+
let did_obj = match Did::new(&did) {
511
+
Ok(d) => d,
512
+
Err(_) => {
513
+
return (
514
+
StatusCode::INTERNAL_SERVER_ERROR,
515
+
Json(json!({"error": "InternalError", "message": "Invalid DID"})),
516
+
)
517
+
.into_response();
518
+
}
519
+
};
520
+
521
+
let rev = Tid::now(LimitedU32::MIN);
522
+
523
+
let new_commit = Commit::new_unsigned(did_obj, new_mst_root, rev, Some(current_root_cid));
524
+
525
+
let new_commit_bytes = match new_commit.to_cbor() {
526
+
Ok(b) => b,
527
+
Err(e) => {
528
+
error!("Failed to serialize new commit: {:?}", e);
529
+
return (
530
+
StatusCode::INTERNAL_SERVER_ERROR,
531
+
Json(
532
+
json!({"error": "InternalError", "message": "Failed to serialize new commit"}),
533
+
),
534
+
)
535
+
.into_response();
536
+
}
537
+
};
538
+
539
+
let new_root_cid = match state.block_store.put(&new_commit_bytes).await {
540
+
Ok(c) => c,
541
+
Err(e) => {
542
+
error!("Failed to save new commit: {:?}", e);
543
+
return (
544
+
StatusCode::INTERNAL_SERVER_ERROR,
545
+
Json(json!({"error": "InternalError", "message": "Failed to save new commit"})),
546
+
)
547
+
.into_response();
548
+
}
549
+
};
550
+
551
+
let update_repo = sqlx::query("UPDATE repos SET repo_root_cid = $1 WHERE user_id = $2")
552
+
.bind(new_root_cid.to_string())
553
+
.bind(user_id)
554
+
.execute(&state.db)
555
+
.await;
556
+
557
+
if let Err(e) = update_repo {
558
+
error!("Failed to update repo root in DB: {:?}", e);
559
+
return (
560
+
StatusCode::INTERNAL_SERVER_ERROR,
561
+
Json(json!({"error": "InternalError", "message": "Failed to update repo root in DB"})),
562
+
)
563
+
.into_response();
564
+
}
565
+
566
+
let record_insert = sqlx::query(
567
+
"INSERT INTO records (repo_id, collection, rkey, record_cid) VALUES ($1, $2, $3, $4)
568
+
ON CONFLICT (repo_id, collection, rkey) DO UPDATE SET record_cid = $4, created_at = NOW()",
569
+
)
570
+
.bind(user_id)
571
+
.bind(&input.collection)
572
+
.bind(&rkey)
573
+
.bind(record_cid.to_string())
574
+
.execute(&state.db)
575
+
.await;
576
+
577
+
if let Err(e) = record_insert {
578
+
error!("Error inserting record index: {:?}", e);
579
+
return (
580
+
StatusCode::INTERNAL_SERVER_ERROR,
581
+
Json(json!({"error": "InternalError", "message": "Failed to index record"})),
582
+
)
583
+
.into_response();
584
+
}
585
+
586
+
let output = PutRecordOutput {
587
+
uri: format!("at://{}/{}/{}", input.repo, input.collection, rkey),
588
+
cid: record_cid.to_string(),
589
+
};
590
+
(StatusCode::OK, Json(output)).into_response()
591
+
}
-294
src/api/server.rs
-294
src/api/server.rs
···
1
-
use axum::{
2
-
extract::State,
3
-
Json,
4
-
response::{IntoResponse, Response},
5
-
http::StatusCode,
6
-
};
7
-
use serde::{Deserialize, Serialize};
8
-
use serde_json::json;
9
-
use crate::state::AppState;
10
-
use sqlx::Row;
11
-
use bcrypt::verify;
12
-
use tracing::{info, error, warn};
13
-
14
-
pub async fn describe_server() -> impl IntoResponse {
15
-
let domains_str = std::env::var("AVAILABLE_USER_DOMAINS").unwrap_or_else(|_| "example.com".to_string());
16
-
let domains: Vec<&str> = domains_str.split(',').map(|s| s.trim()).collect();
17
-
18
-
Json(json!({
19
-
"availableUserDomains": domains
20
-
}))
21
-
}
22
-
23
-
pub async fn health(State(state): State<AppState>) -> impl IntoResponse {
24
-
match sqlx::query("SELECT 1").execute(&state.db).await {
25
-
Ok(_) => (StatusCode::OK, "OK"),
26
-
Err(e) => {
27
-
error!("Health check failed: {:?}", e);
28
-
(StatusCode::SERVICE_UNAVAILABLE, "Service Unavailable")
29
-
}
30
-
}
31
-
}
32
-
33
-
#[derive(Deserialize)]
34
-
pub struct CreateSessionInput {
35
-
pub identifier: String,
36
-
pub password: String,
37
-
}
38
-
39
-
#[derive(Serialize)]
40
-
#[serde(rename_all = "camelCase")]
41
-
pub struct CreateSessionOutput {
42
-
pub access_jwt: String,
43
-
pub refresh_jwt: String,
44
-
pub handle: String,
45
-
pub did: String,
46
-
}
47
-
48
-
pub async fn create_session(
49
-
State(state): State<AppState>,
50
-
Json(input): Json<CreateSessionInput>,
51
-
) -> Response {
52
-
info!("create_session: identifier='{}'", input.identifier);
53
-
54
-
let user_row = sqlx::query("SELECT u.did, u.handle, u.password_hash, k.key_bytes FROM users u JOIN user_keys k ON u.id = k.user_id WHERE u.handle = $1 OR u.email = $1")
55
-
.bind(&input.identifier)
56
-
.fetch_optional(&state.db)
57
-
.await;
58
-
59
-
match user_row {
60
-
Ok(Some(row)) => {
61
-
let stored_hash: String = row.get("password_hash");
62
-
63
-
if verify(&input.password, &stored_hash).unwrap_or(false) {
64
-
let did: String = row.get("did");
65
-
let handle: String = row.get("handle");
66
-
let key_bytes: Vec<u8> = row.get("key_bytes");
67
-
68
-
let access_jwt = match crate::auth::create_access_token(&did, &key_bytes) {
69
-
Ok(t) => t,
70
-
Err(e) => {
71
-
error!("Failed to create access token: {:?}", e);
72
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
73
-
}
74
-
};
75
-
76
-
let refresh_jwt = match crate::auth::create_refresh_token(&did, &key_bytes) {
77
-
Ok(t) => t,
78
-
Err(e) => {
79
-
error!("Failed to create refresh token: {:?}", e);
80
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
81
-
}
82
-
};
83
-
84
-
let session_insert = sqlx::query("INSERT INTO sessions (access_jwt, refresh_jwt, did) VALUES ($1, $2, $3)")
85
-
.bind(&access_jwt)
86
-
.bind(&refresh_jwt)
87
-
.bind(&did)
88
-
.execute(&state.db)
89
-
.await;
90
-
91
-
match session_insert {
92
-
Ok(_) => {
93
-
return (StatusCode::OK, Json(CreateSessionOutput {
94
-
access_jwt,
95
-
refresh_jwt,
96
-
handle,
97
-
did,
98
-
})).into_response();
99
-
},
100
-
Err(e) => {
101
-
error!("Failed to insert session: {:?}", e);
102
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
103
-
}
104
-
}
105
-
} else {
106
-
warn!("Password verification failed for identifier: {}", input.identifier);
107
-
}
108
-
},
109
-
Ok(None) => {
110
-
warn!("User not found for identifier: {}", input.identifier);
111
-
},
112
-
Err(e) => {
113
-
error!("Database error fetching user: {:?}", e);
114
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
115
-
}
116
-
}
117
-
118
-
(StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid identifier or password"}))).into_response()
119
-
}
120
-
121
-
pub async fn get_session(
122
-
State(state): State<AppState>,
123
-
headers: axum::http::HeaderMap,
124
-
) -> Response {
125
-
let auth_header = headers.get("Authorization");
126
-
if auth_header.is_none() {
127
-
return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationRequired"}))).into_response();
128
-
}
129
-
130
-
let token = auth_header.unwrap().to_str().unwrap_or("").replace("Bearer ", "");
131
-
132
-
let result = sqlx::query(
133
-
r#"
134
-
SELECT u.handle, u.did, u.email, k.key_bytes
135
-
FROM sessions s
136
-
JOIN users u ON s.did = u.did
137
-
JOIN user_keys k ON u.id = k.user_id
138
-
WHERE s.access_jwt = $1
139
-
"#
140
-
)
141
-
.bind(&token)
142
-
.fetch_optional(&state.db)
143
-
.await;
144
-
145
-
match result {
146
-
Ok(Some(row)) => {
147
-
let handle: String = row.get("handle");
148
-
let did: String = row.get("did");
149
-
let email: String = row.get("email");
150
-
let key_bytes: Vec<u8> = row.get("key_bytes");
151
-
152
-
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
153
-
return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"}))).into_response();
154
-
}
155
-
156
-
return (StatusCode::OK, Json(json!({
157
-
"handle": handle,
158
-
"did": did,
159
-
"email": email,
160
-
"didDoc": {}
161
-
}))).into_response();
162
-
},
163
-
Ok(None) => {
164
-
return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed"}))).into_response();
165
-
},
166
-
Err(e) => {
167
-
error!("Database error in get_session: {:?}", e);
168
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
169
-
}
170
-
}
171
-
}
172
-
173
-
pub async fn delete_session(
174
-
State(state): State<AppState>,
175
-
headers: axum::http::HeaderMap,
176
-
) -> Response {
177
-
let auth_header = headers.get("Authorization");
178
-
if auth_header.is_none() {
179
-
return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationRequired"}))).into_response();
180
-
}
181
-
182
-
let token = auth_header.unwrap().to_str().unwrap_or("").replace("Bearer ", "");
183
-
184
-
let result = sqlx::query("DELETE FROM sessions WHERE access_jwt = $1")
185
-
.bind(token)
186
-
.execute(&state.db)
187
-
.await;
188
-
189
-
match result {
190
-
Ok(res) => {
191
-
if res.rows_affected() > 0 {
192
-
return (StatusCode::OK, Json(json!({}))).into_response();
193
-
}
194
-
},
195
-
Err(e) => {
196
-
error!("Database error in delete_session: {:?}", e);
197
-
}
198
-
}
199
-
200
-
(StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed"}))).into_response()
201
-
}
202
-
203
-
pub async fn refresh_session(
204
-
State(state): State<AppState>,
205
-
headers: axum::http::HeaderMap,
206
-
) -> Response {
207
-
let auth_header = headers.get("Authorization");
208
-
if auth_header.is_none() {
209
-
return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationRequired"}))).into_response();
210
-
}
211
-
212
-
let refresh_token = auth_header.unwrap().to_str().unwrap_or("").replace("Bearer ", "");
213
-
214
-
let session = sqlx::query(
215
-
"SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.refresh_jwt = $1"
216
-
)
217
-
.bind(&refresh_token)
218
-
.fetch_optional(&state.db)
219
-
.await;
220
-
221
-
match session {
222
-
Ok(Some(session_row)) => {
223
-
let did: String = session_row.get("did");
224
-
let key_bytes: Vec<u8> = session_row.get("key_bytes");
225
-
226
-
if let Err(_) = crate::auth::verify_token(&refresh_token, &key_bytes) {
227
-
return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid refresh token signature"}))).into_response();
228
-
}
229
-
230
-
let new_access_jwt = match crate::auth::create_access_token(&did, &key_bytes) {
231
-
Ok(t) => t,
232
-
Err(e) => {
233
-
error!("Failed to create access token: {:?}", e);
234
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
235
-
}
236
-
};
237
-
let new_refresh_jwt = match crate::auth::create_refresh_token(&did, &key_bytes) {
238
-
Ok(t) => t,
239
-
Err(e) => {
240
-
error!("Failed to create refresh token: {:?}", e);
241
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
242
-
}
243
-
};
244
-
245
-
let update = sqlx::query("UPDATE sessions SET access_jwt = $1, refresh_jwt = $2 WHERE refresh_jwt = $3")
246
-
.bind(&new_access_jwt)
247
-
.bind(&new_refresh_jwt)
248
-
.bind(&refresh_token)
249
-
.execute(&state.db)
250
-
.await;
251
-
252
-
match update {
253
-
Ok(_) => {
254
-
let user = sqlx::query("SELECT handle FROM users WHERE did = $1")
255
-
.bind(&did)
256
-
.fetch_optional(&state.db)
257
-
.await;
258
-
259
-
match user {
260
-
Ok(Some(u)) => {
261
-
let handle: String = u.get("handle");
262
-
return (StatusCode::OK, Json(json!({
263
-
"accessJwt": new_access_jwt,
264
-
"refreshJwt": new_refresh_jwt,
265
-
"handle": handle,
266
-
"did": did
267
-
}))).into_response();
268
-
},
269
-
Ok(None) => {
270
-
error!("User not found for existing session: {}", did);
271
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
272
-
},
273
-
Err(e) => {
274
-
error!("Database error fetching user: {:?}", e);
275
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
276
-
}
277
-
}
278
-
},
279
-
Err(e) => {
280
-
error!("Database error updating session: {:?}", e);
281
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
282
-
}
283
-
}
284
-
},
285
-
Ok(None) => {
286
-
return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid refresh token"}))).into_response();
287
-
},
288
-
Err(e) => {
289
-
error!("Database error fetching session: {:?}", e);
290
-
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
291
-
}
292
-
}
293
-
}
294
-
···
+25
src/api/server/meta.rs
+25
src/api/server/meta.rs
···
···
1
+
use crate::state::AppState;
2
+
use axum::{Json, extract::State, http::StatusCode, response::IntoResponse};
3
+
use serde_json::json;
4
+
5
+
use tracing::error;
6
+
7
+
pub async fn describe_server() -> impl IntoResponse {
8
+
let domains_str =
9
+
std::env::var("AVAILABLE_USER_DOMAINS").unwrap_or_else(|_| "example.com".to_string());
10
+
let domains: Vec<&str> = domains_str.split(',').map(|s| s.trim()).collect();
11
+
12
+
Json(json!({
13
+
"availableUserDomains": domains
14
+
}))
15
+
}
16
+
17
+
pub async fn health(State(state): State<AppState>) -> impl IntoResponse {
18
+
match sqlx::query("SELECT 1").execute(&state.db).await {
19
+
Ok(_) => (StatusCode::OK, "OK"),
20
+
Err(e) => {
21
+
error!("Health check failed: {:?}", e);
22
+
(StatusCode::SERVICE_UNAVAILABLE, "Service Unavailable")
23
+
}
24
+
}
25
+
}
+5
src/api/server/mod.rs
+5
src/api/server/mod.rs
+377
src/api/server/session.rs
+377
src/api/server/session.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::verify;
9
+
use serde::{Deserialize, Serialize};
10
+
use serde_json::json;
11
+
use sqlx::Row;
12
+
use tracing::{error, info, warn};
13
+
14
+
#[derive(Deserialize)]
15
+
pub struct CreateSessionInput {
16
+
pub identifier: String,
17
+
pub password: String,
18
+
}
19
+
20
+
#[derive(Serialize)]
21
+
#[serde(rename_all = "camelCase")]
22
+
pub struct CreateSessionOutput {
23
+
pub access_jwt: String,
24
+
pub refresh_jwt: String,
25
+
pub handle: String,
26
+
pub did: String,
27
+
}
28
+
29
+
pub async fn create_session(
30
+
State(state): State<AppState>,
31
+
Json(input): Json<CreateSessionInput>,
32
+
) -> Response {
33
+
info!("create_session: identifier='{}'", input.identifier);
34
+
35
+
let user_row = sqlx::query("SELECT u.did, u.handle, u.password_hash, k.key_bytes FROM users u JOIN user_keys k ON u.id = k.user_id WHERE u.handle = $1 OR u.email = $1")
36
+
.bind(&input.identifier)
37
+
.fetch_optional(&state.db)
38
+
.await;
39
+
40
+
match user_row {
41
+
Ok(Some(row)) => {
42
+
let stored_hash: String = row.get("password_hash");
43
+
44
+
if verify(&input.password, &stored_hash).unwrap_or(false) {
45
+
let did: String = row.get("did");
46
+
let handle: String = row.get("handle");
47
+
let key_bytes: Vec<u8> = row.get("key_bytes");
48
+
49
+
let access_jwt = match crate::auth::create_access_token(&did, &key_bytes) {
50
+
Ok(t) => t,
51
+
Err(e) => {
52
+
error!("Failed to create access token: {:?}", e);
53
+
return (
54
+
StatusCode::INTERNAL_SERVER_ERROR,
55
+
Json(json!({"error": "InternalError"})),
56
+
)
57
+
.into_response();
58
+
}
59
+
};
60
+
61
+
let refresh_jwt = match crate::auth::create_refresh_token(&did, &key_bytes) {
62
+
Ok(t) => t,
63
+
Err(e) => {
64
+
error!("Failed to create refresh token: {:?}", e);
65
+
return (
66
+
StatusCode::INTERNAL_SERVER_ERROR,
67
+
Json(json!({"error": "InternalError"})),
68
+
)
69
+
.into_response();
70
+
}
71
+
};
72
+
73
+
let session_insert = sqlx::query(
74
+
"INSERT INTO sessions (access_jwt, refresh_jwt, did) VALUES ($1, $2, $3)",
75
+
)
76
+
.bind(&access_jwt)
77
+
.bind(&refresh_jwt)
78
+
.bind(&did)
79
+
.execute(&state.db)
80
+
.await;
81
+
82
+
match session_insert {
83
+
Ok(_) => {
84
+
return (
85
+
StatusCode::OK,
86
+
Json(CreateSessionOutput {
87
+
access_jwt,
88
+
refresh_jwt,
89
+
handle,
90
+
did,
91
+
}),
92
+
)
93
+
.into_response();
94
+
}
95
+
Err(e) => {
96
+
error!("Failed to insert session: {:?}", e);
97
+
return (
98
+
StatusCode::INTERNAL_SERVER_ERROR,
99
+
Json(json!({"error": "InternalError"})),
100
+
)
101
+
.into_response();
102
+
}
103
+
}
104
+
} else {
105
+
warn!(
106
+
"Password verification failed for identifier: {}",
107
+
input.identifier
108
+
);
109
+
}
110
+
}
111
+
Ok(None) => {
112
+
warn!("User not found for identifier: {}", input.identifier);
113
+
}
114
+
Err(e) => {
115
+
error!("Database error fetching user: {:?}", e);
116
+
return (
117
+
StatusCode::INTERNAL_SERVER_ERROR,
118
+
Json(json!({"error": "InternalError"})),
119
+
)
120
+
.into_response();
121
+
}
122
+
}
123
+
124
+
(
125
+
StatusCode::UNAUTHORIZED,
126
+
Json(json!({"error": "AuthenticationFailed", "message": "Invalid identifier or password"})),
127
+
)
128
+
.into_response()
129
+
}
130
+
131
+
pub async fn get_session(
132
+
State(state): State<AppState>,
133
+
headers: axum::http::HeaderMap,
134
+
) -> Response {
135
+
let auth_header = headers.get("Authorization");
136
+
if auth_header.is_none() {
137
+
return (
138
+
StatusCode::UNAUTHORIZED,
139
+
Json(json!({"error": "AuthenticationRequired"})),
140
+
)
141
+
.into_response();
142
+
}
143
+
144
+
let token = auth_header
145
+
.unwrap()
146
+
.to_str()
147
+
.unwrap_or("")
148
+
.replace("Bearer ", "");
149
+
150
+
let result = sqlx::query(
151
+
r#"
152
+
SELECT u.handle, u.did, u.email, k.key_bytes
153
+
FROM sessions s
154
+
JOIN users u ON s.did = u.did
155
+
JOIN user_keys k ON u.id = k.user_id
156
+
WHERE s.access_jwt = $1
157
+
"#,
158
+
)
159
+
.bind(&token)
160
+
.fetch_optional(&state.db)
161
+
.await;
162
+
163
+
match result {
164
+
Ok(Some(row)) => {
165
+
let handle: String = row.get("handle");
166
+
let did: String = row.get("did");
167
+
let email: String = row.get("email");
168
+
let key_bytes: Vec<u8> = row.get("key_bytes");
169
+
170
+
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
171
+
return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"}))).into_response();
172
+
}
173
+
174
+
return (
175
+
StatusCode::OK,
176
+
Json(json!({
177
+
"handle": handle,
178
+
"did": did,
179
+
"email": email,
180
+
"didDoc": {}
181
+
})),
182
+
)
183
+
.into_response();
184
+
}
185
+
Ok(None) => {
186
+
return (
187
+
StatusCode::UNAUTHORIZED,
188
+
Json(json!({"error": "AuthenticationFailed"})),
189
+
)
190
+
.into_response();
191
+
}
192
+
Err(e) => {
193
+
error!("Database error in get_session: {:?}", e);
194
+
return (
195
+
StatusCode::INTERNAL_SERVER_ERROR,
196
+
Json(json!({"error": "InternalError"})),
197
+
)
198
+
.into_response();
199
+
}
200
+
}
201
+
}
202
+
203
+
pub async fn delete_session(
204
+
State(state): State<AppState>,
205
+
headers: axum::http::HeaderMap,
206
+
) -> Response {
207
+
let auth_header = headers.get("Authorization");
208
+
if auth_header.is_none() {
209
+
return (
210
+
StatusCode::UNAUTHORIZED,
211
+
Json(json!({"error": "AuthenticationRequired"})),
212
+
)
213
+
.into_response();
214
+
}
215
+
216
+
let token = auth_header
217
+
.unwrap()
218
+
.to_str()
219
+
.unwrap_or("")
220
+
.replace("Bearer ", "");
221
+
222
+
let result = sqlx::query("DELETE FROM sessions WHERE access_jwt = $1")
223
+
.bind(token)
224
+
.execute(&state.db)
225
+
.await;
226
+
227
+
match result {
228
+
Ok(res) => {
229
+
if res.rows_affected() > 0 {
230
+
return (StatusCode::OK, Json(json!({}))).into_response();
231
+
}
232
+
}
233
+
Err(e) => {
234
+
error!("Database error in delete_session: {:?}", e);
235
+
}
236
+
}
237
+
238
+
(
239
+
StatusCode::UNAUTHORIZED,
240
+
Json(json!({"error": "AuthenticationFailed"})),
241
+
)
242
+
.into_response()
243
+
}
244
+
245
+
pub async fn refresh_session(
246
+
State(state): State<AppState>,
247
+
headers: axum::http::HeaderMap,
248
+
) -> Response {
249
+
let auth_header = headers.get("Authorization");
250
+
if auth_header.is_none() {
251
+
return (
252
+
StatusCode::UNAUTHORIZED,
253
+
Json(json!({"error": "AuthenticationRequired"})),
254
+
)
255
+
.into_response();
256
+
}
257
+
258
+
let refresh_token = auth_header
259
+
.unwrap()
260
+
.to_str()
261
+
.unwrap_or("")
262
+
.replace("Bearer ", "");
263
+
264
+
let session = sqlx::query(
265
+
"SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.refresh_jwt = $1"
266
+
)
267
+
.bind(&refresh_token)
268
+
.fetch_optional(&state.db)
269
+
.await;
270
+
271
+
match session {
272
+
Ok(Some(session_row)) => {
273
+
let did: String = session_row.get("did");
274
+
let key_bytes: Vec<u8> = session_row.get("key_bytes");
275
+
276
+
if let Err(_) = crate::auth::verify_token(&refresh_token, &key_bytes) {
277
+
return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid refresh token signature"}))).into_response();
278
+
}
279
+
280
+
let new_access_jwt = match crate::auth::create_access_token(&did, &key_bytes) {
281
+
Ok(t) => t,
282
+
Err(e) => {
283
+
error!("Failed to create access token: {:?}", e);
284
+
return (
285
+
StatusCode::INTERNAL_SERVER_ERROR,
286
+
Json(json!({"error": "InternalError"})),
287
+
)
288
+
.into_response();
289
+
}
290
+
};
291
+
let new_refresh_jwt = match crate::auth::create_refresh_token(&did, &key_bytes) {
292
+
Ok(t) => t,
293
+
Err(e) => {
294
+
error!("Failed to create refresh token: {:?}", e);
295
+
return (
296
+
StatusCode::INTERNAL_SERVER_ERROR,
297
+
Json(json!({"error": "InternalError"})),
298
+
)
299
+
.into_response();
300
+
}
301
+
};
302
+
303
+
let update = sqlx::query(
304
+
"UPDATE sessions SET access_jwt = $1, refresh_jwt = $2 WHERE refresh_jwt = $3",
305
+
)
306
+
.bind(&new_access_jwt)
307
+
.bind(&new_refresh_jwt)
308
+
.bind(&refresh_token)
309
+
.execute(&state.db)
310
+
.await;
311
+
312
+
match update {
313
+
Ok(_) => {
314
+
let user = sqlx::query("SELECT handle FROM users WHERE did = $1")
315
+
.bind(&did)
316
+
.fetch_optional(&state.db)
317
+
.await;
318
+
319
+
match user {
320
+
Ok(Some(u)) => {
321
+
let handle: String = u.get("handle");
322
+
return (
323
+
StatusCode::OK,
324
+
Json(json!({
325
+
"accessJwt": new_access_jwt,
326
+
"refreshJwt": new_refresh_jwt,
327
+
"handle": handle,
328
+
"did": did
329
+
})),
330
+
)
331
+
.into_response();
332
+
}
333
+
Ok(None) => {
334
+
error!("User not found for existing session: {}", did);
335
+
return (
336
+
StatusCode::INTERNAL_SERVER_ERROR,
337
+
Json(json!({"error": "InternalError"})),
338
+
)
339
+
.into_response();
340
+
}
341
+
Err(e) => {
342
+
error!("Database error fetching user: {:?}", e);
343
+
return (
344
+
StatusCode::INTERNAL_SERVER_ERROR,
345
+
Json(json!({"error": "InternalError"})),
346
+
)
347
+
.into_response();
348
+
}
349
+
}
350
+
}
351
+
Err(e) => {
352
+
error!("Database error updating session: {:?}", e);
353
+
return (
354
+
StatusCode::INTERNAL_SERVER_ERROR,
355
+
Json(json!({"error": "InternalError"})),
356
+
)
357
+
.into_response();
358
+
}
359
+
}
360
+
}
361
+
Ok(None) => {
362
+
return (
363
+
StatusCode::UNAUTHORIZED,
364
+
Json(json!({"error": "AuthenticationFailed", "message": "Invalid refresh token"})),
365
+
)
366
+
.into_response();
367
+
}
368
+
Err(e) => {
369
+
error!("Database error fetching session: {:?}", e);
370
+
return (
371
+
StatusCode::INTERNAL_SERVER_ERROR,
372
+
Json(json!({"error": "InternalError"})),
373
+
)
374
+
.into_response();
375
+
}
376
+
}
377
+
}
-157
src/auth.rs
-157
src/auth.rs
···
1
-
use serde::{Deserialize, Serialize};
2
-
use chrono::{Utc, Duration};
3
-
use k256::ecdsa::{SigningKey, VerifyingKey, signature::Signer, signature::Verifier, Signature};
4
-
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
5
-
use anyhow::{Context, Result, anyhow};
6
-
7
-
#[derive(Debug, Serialize, Deserialize)]
8
-
pub struct Claims {
9
-
pub iss: String,
10
-
pub sub: String,
11
-
pub aud: String,
12
-
pub exp: usize,
13
-
pub iat: usize,
14
-
#[serde(skip_serializing_if = "Option::is_none")]
15
-
pub scope: Option<String>,
16
-
#[serde(skip_serializing_if = "Option::is_none")]
17
-
pub lxm: Option<String>,
18
-
pub jti: String,
19
-
}
20
-
21
-
#[derive(Debug, Serialize, Deserialize)]
22
-
struct Header {
23
-
alg: String,
24
-
typ: String,
25
-
}
26
-
27
-
#[derive(Debug, Serialize, Deserialize)]
28
-
struct UnsafeClaims {
29
-
iss: String,
30
-
sub: Option<String>,
31
-
}
32
-
33
-
// fancy boy TokenData equivalent for compatibility/structure
34
-
pub struct TokenData<T> {
35
-
pub claims: T,
36
-
}
37
-
38
-
pub fn get_did_from_token(token: &str) -> Result<String, String> {
39
-
let parts: Vec<&str> = token.split('.').collect();
40
-
if parts.len() != 3 {
41
-
return Err("Invalid token format".to_string());
42
-
}
43
-
44
-
let payload_bytes = URL_SAFE_NO_PAD.decode(parts[1])
45
-
.map_err(|e| format!("Base64 decode failed: {}", e))?;
46
-
47
-
let claims: UnsafeClaims = serde_json::from_slice(&payload_bytes)
48
-
.map_err(|e| format!("JSON decode failed: {}", e))?;
49
-
50
-
Ok(claims.sub.unwrap_or(claims.iss))
51
-
}
52
-
53
-
pub fn create_access_token(did: &str, key_bytes: &[u8]) -> Result<String, anyhow::Error> {
54
-
create_signed_token(did, "access", key_bytes, Duration::minutes(15))
55
-
}
56
-
57
-
pub fn create_refresh_token(did: &str, key_bytes: &[u8]) -> Result<String, anyhow::Error> {
58
-
create_signed_token(did, "refresh", key_bytes, Duration::days(7))
59
-
}
60
-
61
-
pub fn create_service_token(did: &str, aud: &str, lxm: &str, key_bytes: &[u8]) -> Result<String, anyhow::Error> {
62
-
let signing_key = SigningKey::from_slice(key_bytes)?;
63
-
64
-
let expiration = Utc::now()
65
-
.checked_add_signed(Duration::seconds(60))
66
-
.expect("valid timestamp")
67
-
.timestamp();
68
-
69
-
let claims = Claims {
70
-
iss: did.to_owned(),
71
-
sub: did.to_owned(),
72
-
aud: aud.to_owned(),
73
-
exp: expiration as usize,
74
-
iat: Utc::now().timestamp() as usize,
75
-
scope: None,
76
-
lxm: Some(lxm.to_string()),
77
-
jti: uuid::Uuid::new_v4().to_string(),
78
-
};
79
-
80
-
sign_claims(claims, &signing_key)
81
-
}
82
-
83
-
fn create_signed_token(did: &str, scope: &str, key_bytes: &[u8], duration: Duration) -> Result<String, anyhow::Error> {
84
-
let signing_key = SigningKey::from_slice(key_bytes)?;
85
-
86
-
let expiration = Utc::now()
87
-
.checked_add_signed(duration)
88
-
.expect("valid timestamp")
89
-
.timestamp();
90
-
91
-
let claims = Claims {
92
-
iss: did.to_owned(),
93
-
sub: did.to_owned(),
94
-
aud: format!("did:web:{}", std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string())),
95
-
exp: expiration as usize,
96
-
iat: Utc::now().timestamp() as usize,
97
-
scope: Some(scope.to_string()),
98
-
lxm: None,
99
-
jti: uuid::Uuid::new_v4().to_string(),
100
-
};
101
-
102
-
sign_claims(claims, &signing_key)
103
-
}
104
-
105
-
fn sign_claims(claims: Claims, key: &SigningKey) -> Result<String, anyhow::Error> {
106
-
let header = Header {
107
-
alg: "ES256K".to_string(),
108
-
typ: "JWT".to_string(),
109
-
};
110
-
111
-
let header_json = serde_json::to_string(&header)?;
112
-
let claims_json = serde_json::to_string(&claims)?;
113
-
114
-
let header_b64 = URL_SAFE_NO_PAD.encode(header_json);
115
-
let claims_b64 = URL_SAFE_NO_PAD.encode(claims_json);
116
-
117
-
let message = format!("{}.{}", header_b64, claims_b64);
118
-
let signature: Signature = key.sign(message.as_bytes());
119
-
let signature_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes());
120
-
121
-
Ok(format!("{}.{}", message, signature_b64))
122
-
}
123
-
124
-
pub fn verify_token(token: &str, key_bytes: &[u8]) -> Result<TokenData<Claims>, anyhow::Error> {
125
-
let parts: Vec<&str> = token.split('.').collect();
126
-
if parts.len() != 3 {
127
-
return Err(anyhow!("Invalid token format"));
128
-
}
129
-
130
-
let header_b64 = parts[0];
131
-
let claims_b64 = parts[1];
132
-
let signature_b64 = parts[2];
133
-
134
-
let signature_bytes = URL_SAFE_NO_PAD.decode(signature_b64)
135
-
.context("Base64 decode of signature failed")?;
136
-
let signature = Signature::from_slice(&signature_bytes)
137
-
.map_err(|e| anyhow!("Invalid signature format: {}", e))?;
138
-
139
-
let signing_key = SigningKey::from_slice(key_bytes)?;
140
-
let verifying_key = VerifyingKey::from(&signing_key);
141
-
142
-
let message = format!("{}.{}", header_b64, claims_b64);
143
-
verifying_key.verify(message.as_bytes(), &signature)
144
-
.map_err(|e| anyhow!("Signature verification failed: {}", e))?;
145
-
146
-
let claims_bytes = URL_SAFE_NO_PAD.decode(claims_b64)
147
-
.context("Base64 decode of claims failed")?;
148
-
let claims: Claims = serde_json::from_slice(&claims_bytes)
149
-
.context("JSON decode of claims failed")?;
150
-
151
-
let now = Utc::now().timestamp() as usize;
152
-
if claims.exp < now {
153
-
return Err(anyhow!("Token expired"));
154
-
}
155
-
156
-
Ok(TokenData { claims })
157
-
}
···
+38
src/auth/mod.rs
+38
src/auth/mod.rs
···
···
1
+
use serde::{Deserialize, Serialize};
2
+
3
+
pub mod token;
4
+
pub mod verify;
5
+
6
+
pub use token::{create_access_token, create_refresh_token, create_service_token};
7
+
pub use verify::{get_did_from_token, verify_token};
8
+
9
+
#[derive(Debug, Serialize, Deserialize)]
10
+
pub struct Claims {
11
+
pub iss: String,
12
+
pub sub: String,
13
+
pub aud: String,
14
+
pub exp: usize,
15
+
pub iat: usize,
16
+
#[serde(skip_serializing_if = "Option::is_none")]
17
+
pub scope: Option<String>,
18
+
#[serde(skip_serializing_if = "Option::is_none")]
19
+
pub lxm: Option<String>,
20
+
pub jti: String,
21
+
}
22
+
23
+
#[derive(Debug, Serialize, Deserialize)]
24
+
pub struct Header {
25
+
pub alg: String,
26
+
pub typ: String,
27
+
}
28
+
29
+
#[derive(Debug, Serialize, Deserialize)]
30
+
pub struct UnsafeClaims {
31
+
pub iss: String,
32
+
pub sub: Option<String>,
33
+
}
34
+
35
+
// fancy boy TokenData equivalent for compatibility/structure
36
+
pub struct TokenData<T> {
37
+
pub claims: T,
38
+
}
+86
src/auth/token.rs
+86
src/auth/token.rs
···
···
1
+
use super::{Claims, Header};
2
+
use anyhow::Result;
3
+
use base64::Engine as _;
4
+
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
5
+
use chrono::{Duration, Utc};
6
+
use k256::ecdsa::{Signature, SigningKey, signature::Signer};
7
+
use uuid;
8
+
9
+
pub fn create_access_token(did: &str, key_bytes: &[u8]) -> Result<String> {
10
+
create_signed_token(did, "access", key_bytes, Duration::minutes(15))
11
+
}
12
+
13
+
pub fn create_refresh_token(did: &str, key_bytes: &[u8]) -> Result<String> {
14
+
create_signed_token(did, "refresh", key_bytes, Duration::days(7))
15
+
}
16
+
17
+
pub fn create_service_token(did: &str, aud: &str, lxm: &str, key_bytes: &[u8]) -> Result<String> {
18
+
let signing_key = SigningKey::from_slice(key_bytes)?;
19
+
20
+
let expiration = Utc::now()
21
+
.checked_add_signed(Duration::seconds(60))
22
+
.expect("valid timestamp")
23
+
.timestamp();
24
+
25
+
let claims = Claims {
26
+
iss: did.to_owned(),
27
+
sub: did.to_owned(),
28
+
aud: aud.to_owned(),
29
+
exp: expiration as usize,
30
+
iat: Utc::now().timestamp() as usize,
31
+
scope: None,
32
+
lxm: Some(lxm.to_string()),
33
+
jti: uuid::Uuid::new_v4().to_string(),
34
+
};
35
+
36
+
sign_claims(claims, &signing_key)
37
+
}
38
+
39
+
fn create_signed_token(
40
+
did: &str,
41
+
scope: &str,
42
+
key_bytes: &[u8],
43
+
duration: Duration,
44
+
) -> Result<String> {
45
+
let signing_key = SigningKey::from_slice(key_bytes)?;
46
+
47
+
let expiration = Utc::now()
48
+
.checked_add_signed(duration)
49
+
.expect("valid timestamp")
50
+
.timestamp();
51
+
52
+
let claims = Claims {
53
+
iss: did.to_owned(),
54
+
sub: did.to_owned(),
55
+
aud: format!(
56
+
"did:web:{}",
57
+
std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string())
58
+
),
59
+
exp: expiration as usize,
60
+
iat: Utc::now().timestamp() as usize,
61
+
scope: Some(scope.to_string()),
62
+
lxm: None,
63
+
jti: uuid::Uuid::new_v4().to_string(),
64
+
};
65
+
66
+
sign_claims(claims, &signing_key)
67
+
}
68
+
69
+
fn sign_claims(claims: Claims, key: &SigningKey) -> Result<String> {
70
+
let header = Header {
71
+
alg: "ES256K".to_string(),
72
+
typ: "JWT".to_string(),
73
+
};
74
+
75
+
let header_json = serde_json::to_string(&header)?;
76
+
let claims_json = serde_json::to_string(&claims)?;
77
+
78
+
let header_b64 = URL_SAFE_NO_PAD.encode(header_json);
79
+
let claims_b64 = URL_SAFE_NO_PAD.encode(claims_json);
80
+
81
+
let message = format!("{}.{}", header_b64, claims_b64);
82
+
let signature: Signature = key.sign(message.as_bytes());
83
+
let signature_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes());
84
+
85
+
Ok(format!("{}.{}", message, signature_b64))
86
+
}
+60
src/auth/verify.rs
+60
src/auth/verify.rs
···
···
1
+
use super::{Claims, TokenData, UnsafeClaims};
2
+
use anyhow::{Context, Result, anyhow};
3
+
use base64::Engine as _;
4
+
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
5
+
use chrono::Utc;
6
+
use k256::ecdsa::{Signature, SigningKey, VerifyingKey, signature::Verifier};
7
+
8
+
pub fn get_did_from_token(token: &str) -> Result<String, String> {
9
+
let parts: Vec<&str> = token.split('.').collect();
10
+
if parts.len() != 3 {
11
+
return Err("Invalid token format".to_string());
12
+
}
13
+
14
+
let payload_bytes = URL_SAFE_NO_PAD
15
+
.decode(parts[1])
16
+
.map_err(|e| format!("Base64 decode failed: {}", e))?;
17
+
18
+
let claims: UnsafeClaims =
19
+
serde_json::from_slice(&payload_bytes).map_err(|e| format!("JSON decode failed: {}", e))?;
20
+
21
+
Ok(claims.sub.unwrap_or(claims.iss))
22
+
}
23
+
24
+
pub fn verify_token(token: &str, key_bytes: &[u8]) -> Result<TokenData<Claims>> {
25
+
let parts: Vec<&str> = token.split('.').collect();
26
+
if parts.len() != 3 {
27
+
return Err(anyhow!("Invalid token format"));
28
+
}
29
+
30
+
let header_b64 = parts[0];
31
+
let claims_b64 = parts[1];
32
+
let signature_b64 = parts[2];
33
+
34
+
let signature_bytes = URL_SAFE_NO_PAD
35
+
.decode(signature_b64)
36
+
.context("Base64 decode of signature failed")?;
37
+
let signature = Signature::from_slice(&signature_bytes)
38
+
.map_err(|e| anyhow!("Invalid signature format: {}", e))?;
39
+
40
+
let signing_key = SigningKey::from_slice(key_bytes)?;
41
+
let verifying_key = VerifyingKey::from(&signing_key);
42
+
43
+
let message = format!("{}.{}", header_b64, claims_b64);
44
+
verifying_key
45
+
.verify(message.as_bytes(), &signature)
46
+
.map_err(|e| anyhow!("Signature verification failed: {}", e))?;
47
+
48
+
let claims_bytes = URL_SAFE_NO_PAD
49
+
.decode(claims_b64)
50
+
.context("Base64 decode of claims failed")?;
51
+
let claims: Claims =
52
+
serde_json::from_slice(&claims_bytes).context("JSON decode of claims failed")?;
53
+
54
+
let now = Utc::now().timestamp() as usize;
55
+
if claims.exp < now {
56
+
return Err(anyhow!("Token expired"));
57
+
}
58
+
59
+
Ok(TokenData { claims })
60
+
}
+54
-15
src/lib.rs
+54
-15
src/lib.rs
···
1
pub mod api;
2
-
pub mod state;
3
pub mod auth;
4
pub mod repo;
5
pub mod storage;
6
7
use axum::{
8
-
routing::{get, post, any},
9
Router,
10
};
11
use state::AppState;
12
13
pub fn app(state: AppState) -> Router {
14
Router::new()
15
.route("/health", get(api::server::health))
16
-
.route("/xrpc/com.atproto.server.describeServer", get(api::server::describe_server))
17
-
.route("/xrpc/com.atproto.server.createAccount", post(api::identity::create_account))
18
-
.route("/xrpc/com.atproto.server.createSession", post(api::server::create_session))
19
-
.route("/xrpc/com.atproto.server.getSession", get(api::server::get_session))
20
-
.route("/xrpc/com.atproto.server.deleteSession", post(api::server::delete_session))
21
-
.route("/xrpc/com.atproto.server.refreshSession", post(api::server::refresh_session))
22
-
.route("/xrpc/com.atproto.repo.createRecord", post(api::repo::create_record))
23
-
.route("/xrpc/com.atproto.repo.putRecord", post(api::repo::put_record))
24
-
.route("/xrpc/com.atproto.repo.getRecord", get(api::repo::get_record))
25
-
.route("/xrpc/com.atproto.repo.deleteRecord", post(api::repo::delete_record))
26
-
.route("/xrpc/com.atproto.repo.listRecords", get(api::repo::list_records))
27
-
.route("/xrpc/com.atproto.repo.describeRepo", get(api::repo::describe_repo))
28
-
.route("/xrpc/com.atproto.repo.uploadBlob", post(api::repo::upload_blob))
29
.route("/.well-known/did.json", get(api::identity::well_known_did))
30
.route("/u/{handle}/did.json", get(api::identity::user_did_doc))
31
.route("/xrpc/{*method}", any(api::proxy::proxy_handler))
···
1
pub mod api;
2
pub mod auth;
3
pub mod repo;
4
+
pub mod state;
5
pub mod storage;
6
7
use axum::{
8
Router,
9
+
routing::{any, get, post},
10
};
11
use state::AppState;
12
13
pub fn app(state: AppState) -> Router {
14
Router::new()
15
.route("/health", get(api::server::health))
16
+
.route(
17
+
"/xrpc/com.atproto.server.describeServer",
18
+
get(api::server::describe_server),
19
+
)
20
+
.route(
21
+
"/xrpc/com.atproto.server.createAccount",
22
+
post(api::identity::create_account),
23
+
)
24
+
.route(
25
+
"/xrpc/com.atproto.server.createSession",
26
+
post(api::server::create_session),
27
+
)
28
+
.route(
29
+
"/xrpc/com.atproto.server.getSession",
30
+
get(api::server::get_session),
31
+
)
32
+
.route(
33
+
"/xrpc/com.atproto.server.deleteSession",
34
+
post(api::server::delete_session),
35
+
)
36
+
.route(
37
+
"/xrpc/com.atproto.server.refreshSession",
38
+
post(api::server::refresh_session),
39
+
)
40
+
.route(
41
+
"/xrpc/com.atproto.repo.createRecord",
42
+
post(api::repo::create_record),
43
+
)
44
+
.route(
45
+
"/xrpc/com.atproto.repo.putRecord",
46
+
post(api::repo::put_record),
47
+
)
48
+
.route(
49
+
"/xrpc/com.atproto.repo.getRecord",
50
+
get(api::repo::get_record),
51
+
)
52
+
.route(
53
+
"/xrpc/com.atproto.repo.deleteRecord",
54
+
post(api::repo::delete_record),
55
+
)
56
+
.route(
57
+
"/xrpc/com.atproto.repo.listRecords",
58
+
get(api::repo::list_records),
59
+
)
60
+
.route(
61
+
"/xrpc/com.atproto.repo.describeRepo",
62
+
get(api::repo::describe_repo),
63
+
)
64
+
.route(
65
+
"/xrpc/com.atproto.repo.uploadBlob",
66
+
post(api::repo::upload_blob),
67
+
)
68
.route("/.well-known/did.json", get(api::identity::well_known_did))
69
.route("/u/{handle}/did.json", get(api::identity::user_did_doc))
70
.route("/xrpc/{*method}", any(api::proxy::proxy_handler))
+1
-1
src/main.rs
+1
-1
src/main.rs
+18
-13
src/repo/mod.rs
+18
-13
src/repo/mod.rs
···
1
-
use jacquard_repo::storage::BlockStore;
2
use jacquard_repo::error::RepoError;
3
use jacquard_repo::repo::CommitData;
4
-
use cid::Cid;
5
-
use sqlx::{PgPool, Row};
6
-
use bytes::Bytes;
7
-
use sha2::{Sha256, Digest};
8
use multihash::Multihash;
9
10
#[derive(Clone)]
11
pub struct PostgresBlockStore {
···
31
Some(row) => {
32
let data: Vec<u8> = row.get("data");
33
Ok(Some(Bytes::from(data)))
34
-
},
35
None => Ok(None),
36
}
37
}
···
65
Ok(row.is_some())
66
}
67
68
-
async fn put_many(&self, blocks: impl IntoIterator<Item = (Cid, Bytes)> + Send) -> Result<(), RepoError> {
69
let blocks: Vec<_> = blocks.into_iter().collect();
70
for (cid, data) in blocks {
71
let cid_bytes = cid.to_bytes();
72
-
sqlx::query("INSERT INTO blocks (cid, data) VALUES ($1, $2) ON CONFLICT (cid) DO NOTHING")
73
-
.bind(cid_bytes)
74
-
.bind(data.as_ref())
75
-
.execute(&self.pool)
76
-
.await
77
-
.map_err(|e| RepoError::storage(e))?;
78
}
79
Ok(())
80
}
···
1
+
use bytes::Bytes;
2
+
use cid::Cid;
3
use jacquard_repo::error::RepoError;
4
use jacquard_repo::repo::CommitData;
5
+
use jacquard_repo::storage::BlockStore;
6
use multihash::Multihash;
7
+
use sha2::{Digest, Sha256};
8
+
use sqlx::{PgPool, Row};
9
10
#[derive(Clone)]
11
pub struct PostgresBlockStore {
···
31
Some(row) => {
32
let data: Vec<u8> = row.get("data");
33
Ok(Some(Bytes::from(data)))
34
+
}
35
None => Ok(None),
36
}
37
}
···
65
Ok(row.is_some())
66
}
67
68
+
async fn put_many(
69
+
&self,
70
+
blocks: impl IntoIterator<Item = (Cid, Bytes)> + Send,
71
+
) -> Result<(), RepoError> {
72
let blocks: Vec<_> = blocks.into_iter().collect();
73
for (cid, data) in blocks {
74
let cid_bytes = cid.to_bytes();
75
+
sqlx::query(
76
+
"INSERT INTO blocks (cid, data) VALUES ($1, $2) ON CONFLICT (cid) DO NOTHING",
77
+
)
78
+
.bind(cid_bytes)
79
+
.bind(data.as_ref())
80
+
.execute(&self.pool)
81
+
.await
82
+
.map_err(|e| RepoError::storage(e))?;
83
}
84
Ok(())
85
}
+6
-2
src/state.rs
+6
-2
src/state.rs
···
1
-
use sqlx::PgPool;
2
use crate::repo::PostgresBlockStore;
3
use crate::storage::{BlobStorage, S3BlobStorage};
4
use std::sync::Arc;
5
6
#[derive(Clone)]
···
14
pub async fn new(db: PgPool) -> Self {
15
let block_store = PostgresBlockStore::new(db.clone());
16
let blob_store = S3BlobStorage::new().await;
17
-
Self { db, block_store, blob_store: Arc::new(blob_store) }
18
}
19
}
···
1
use crate::repo::PostgresBlockStore;
2
use crate::storage::{BlobStorage, S3BlobStorage};
3
+
use sqlx::PgPool;
4
use std::sync::Arc;
5
6
#[derive(Clone)]
···
14
pub async fn new(db: PgPool) -> Self {
15
let block_store = PostgresBlockStore::new(db.clone());
16
let blob_store = S3BlobStorage::new().await;
17
+
Self {
18
+
db,
19
+
block_store,
20
+
blob_store: Arc::new(blob_store),
21
+
}
22
}
23
}
+14
-7
src/storage/mod.rs
+14
-7
src/storage/mod.rs
···
1
use async_trait::async_trait;
2
-
use thiserror::Error;
3
use aws_sdk_s3::Client;
4
use aws_sdk_s3::primitives::ByteStream;
5
-
use aws_config::meta::region::RegionProviderChain;
6
-
use aws_config::BehaviorVersion;
7
8
#[derive(Error, Debug)]
9
pub enum StorageError {
···
55
#[async_trait]
56
impl BlobStorage for S3BlobStorage {
57
async fn put(&self, key: &str, data: &[u8]) -> Result<(), StorageError> {
58
-
self.client.put_object()
59
.bucket(&self.bucket)
60
.key(key)
61
.body(ByteStream::from(data.to_vec()))
···
66
}
67
68
async fn get(&self, key: &str) -> Result<Vec<u8>, StorageError> {
69
-
let resp = self.client.get_object()
70
.bucket(&self.bucket)
71
.key(key)
72
.send()
73
.await
74
.map_err(|e| StorageError::S3(e.to_string()))?;
75
76
-
let data = resp.body.collect().await
77
.map_err(|e| StorageError::S3(e.to_string()))?
78
.into_bytes();
79
···
81
}
82
83
async fn delete(&self, key: &str) -> Result<(), StorageError> {
84
-
self.client.delete_object()
85
.bucket(&self.bucket)
86
.key(key)
87
.send()
···
1
use async_trait::async_trait;
2
+
use aws_config::BehaviorVersion;
3
+
use aws_config::meta::region::RegionProviderChain;
4
use aws_sdk_s3::Client;
5
use aws_sdk_s3::primitives::ByteStream;
6
+
use thiserror::Error;
7
8
#[derive(Error, Debug)]
9
pub enum StorageError {
···
55
#[async_trait]
56
impl BlobStorage for S3BlobStorage {
57
async fn put(&self, key: &str, data: &[u8]) -> Result<(), StorageError> {
58
+
self.client
59
+
.put_object()
60
.bucket(&self.bucket)
61
.key(key)
62
.body(ByteStream::from(data.to_vec()))
···
67
}
68
69
async fn get(&self, key: &str) -> Result<Vec<u8>, StorageError> {
70
+
let resp = self
71
+
.client
72
+
.get_object()
73
.bucket(&self.bucket)
74
.key(key)
75
.send()
76
.await
77
.map_err(|e| StorageError::S3(e.to_string()))?;
78
79
+
let data = resp
80
+
.body
81
+
.collect()
82
+
.await
83
.map_err(|e| StorageError::S3(e.to_string()))?
84
.into_bytes();
85
···
87
}
88
89
async fn delete(&self, key: &str) -> Result<(), StorageError> {
90
+
self.client
91
+
.delete_object()
92
.bucket(&self.bucket)
93
.key(key)
94
.send()
+5
-4
tests/auth.rs
+5
-4
tests/auth.rs
···
1
use bspds::auth;
2
use k256::SecretKey;
3
use rand::rngs::OsRng;
4
-
use chrono::{Utc, Duration};
5
-
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
6
use serde_json::json;
7
-
use k256::ecdsa::{SigningKey, signature::Signer};
8
9
#[test]
10
fn test_jwt_flow() {
···
24
25
let aud = "did:web:service";
26
let lxm = "com.example.test";
27
-
let s_token = auth::create_service_token(did, aud, lxm, &key_bytes).expect("create service token");
28
let s_data = auth::verify_token(&s_token, &key_bytes).expect("verify service token");
29
assert_eq!(s_data.claims.aud, aud);
30
assert_eq!(s_data.claims.lxm, Some(lxm.to_string()));
···
1
+
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
2
use bspds::auth;
3
+
use chrono::{Duration, Utc};
4
use k256::SecretKey;
5
+
use k256::ecdsa::{SigningKey, signature::Signer};
6
use rand::rngs::OsRng;
7
use serde_json::json;
8
9
#[test]
10
fn test_jwt_flow() {
···
24
25
let aud = "did:web:service";
26
let lxm = "com.example.test";
27
+
let s_token =
28
+
auth::create_service_token(did, aud, lxm, &key_bytes).expect("create service token");
29
let s_data = auth::verify_token(&s_token, &key_bytes).expect("verify service token");
30
assert_eq!(s_data.claims.aud, aud);
31
assert_eq!(s_data.claims.lxm, Some(lxm.to_string()));
+77
-27
tests/common/mod.rs
+77
-27
tests/common/mod.rs
···
1
-
use reqwest::{header, Client, StatusCode};
2
-
use serde_json::{json, Value};
3
use chrono::Utc;
4
#[allow(unused_imports)]
5
use std::collections::HashMap;
6
#[allow(unused_imports)]
7
use std::time::Duration;
8
-
use std::sync::OnceLock;
9
-
use bspds::state::AppState;
10
-
use sqlx::postgres::PgPoolOptions;
11
-
use tokio::net::TcpListener;
12
-
use testcontainers::{runners::AsyncRunner, ContainerAsync, ImageExt, GenericImage};
13
use testcontainers::core::ContainerPort;
14
use testcontainers_modules::postgres::Postgres;
15
-
use aws_sdk_s3::Client as S3Client;
16
-
use aws_config::BehaviorVersion;
17
-
use aws_sdk_s3::config::Credentials;
18
-
use wiremock::{MockServer, Mock, ResponseTemplate};
19
use wiremock::matchers::{method, path};
20
21
static SERVER_URL: OnceLock<String> = OnceLock::new();
22
static DB_CONTAINER: OnceLock<ContainerAsync<Postgres>> = OnceLock::new();
···
46
if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
47
let podman_sock = std::path::Path::new(&runtime_dir).join("podman/podman.sock");
48
if podman_sock.exists() {
49
-
unsafe { std::env::set_var("DOCKER_HOST", format!("unix://{}", podman_sock.display())); }
50
}
51
}
52
}
···
62
.await
63
.expect("Failed to start MinIO");
64
65
-
let s3_port = s3_container.get_host_port_ipv4(9000).await.expect("Failed to get S3 port");
66
let s3_endpoint = format!("http://127.0.0.1:{}", s3_port);
67
68
unsafe {
···
76
let sdk_config = aws_config::defaults(BehaviorVersion::latest())
77
.region("us-east-1")
78
.endpoint_url(&s3_endpoint)
79
-
.credentials_provider(Credentials::new("minioadmin", "minioadmin", None, None, "test"))
80
.load()
81
.await;
82
···
108
.mount(&mock_server)
109
.await;
110
111
-
unsafe { std::env::set_var("APPVIEW_URL", mock_server.uri()); }
112
MOCK_APPVIEW.set(mock_server).ok();
113
114
S3_CONTAINER.set(s3_container).ok();
115
116
-
let container = Postgres::default().with_tag("18-alpine").start().await.expect("Failed to start Postgres");
117
let connection_string = format!(
118
"postgres://postgres:postgres@127.0.0.1:{}/postgres",
119
-
container.get_host_port_ipv4(5432).await.expect("Failed to get port")
120
);
121
122
DB_CONTAINER.set(container).ok();
···
157
158
#[allow(dead_code)]
159
pub async fn upload_test_blob(client: &Client, data: &'static str, mime: &'static str) -> Value {
160
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await))
161
.header(header::CONTENT_TYPE, mime)
162
.bearer_auth(AUTH_TOKEN)
163
.body(data)
···
170
body["blob"].clone()
171
}
172
173
-
174
#[allow(dead_code)]
175
pub async fn create_test_post(
176
client: &Client,
177
text: &str,
178
-
reply_to: Option<Value>
179
) -> (String, String, String) {
180
let collection = "app.bsky.feed.post";
181
let mut record = json!({
···
194
"record": record
195
});
196
197
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await))
198
.bearer_auth(AUTH_TOKEN)
199
.json(&payload)
200
.send()
···
202
.expect("Failed to send createRecord");
203
204
assert_eq!(res.status(), StatusCode::OK, "Failed to create post record");
205
-
let body: Value = res.json().await.expect("createRecord response was not JSON");
206
207
-
let uri = body["uri"].as_str().expect("Response had no URI").to_string();
208
-
let cid = body["cid"].as_str().expect("Response had no CID").to_string();
209
-
let rkey = uri.split('/').last().expect("URI was malformed").to_string();
210
211
(uri, cid, rkey)
212
}
···
220
"password": "password"
221
});
222
223
-
let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
224
.json(&payload)
225
.send()
226
.await
···
231
}
232
233
let body: Value = res.json().await.expect("Invalid JSON");
234
-
let access_jwt = body["accessJwt"].as_str().expect("No accessJwt").to_string();
235
let did = body["did"].as_str().expect("No did").to_string();
236
(access_jwt, did)
237
}
···
1
+
use aws_config::BehaviorVersion;
2
+
use aws_sdk_s3::Client as S3Client;
3
+
use aws_sdk_s3::config::Credentials;
4
+
use bspds::state::AppState;
5
use chrono::Utc;
6
+
use reqwest::{Client, StatusCode, header};
7
+
use serde_json::{Value, json};
8
+
use sqlx::postgres::PgPoolOptions;
9
#[allow(unused_imports)]
10
use std::collections::HashMap;
11
+
use std::sync::OnceLock;
12
#[allow(unused_imports)]
13
use std::time::Duration;
14
use testcontainers::core::ContainerPort;
15
+
use testcontainers::{ContainerAsync, GenericImage, ImageExt, runners::AsyncRunner};
16
use testcontainers_modules::postgres::Postgres;
17
+
use tokio::net::TcpListener;
18
use wiremock::matchers::{method, path};
19
+
use wiremock::{Mock, MockServer, ResponseTemplate};
20
21
static SERVER_URL: OnceLock<String> = OnceLock::new();
22
static DB_CONTAINER: OnceLock<ContainerAsync<Postgres>> = OnceLock::new();
···
46
if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
47
let podman_sock = std::path::Path::new(&runtime_dir).join("podman/podman.sock");
48
if podman_sock.exists() {
49
+
unsafe {
50
+
std::env::set_var(
51
+
"DOCKER_HOST",
52
+
format!("unix://{}", podman_sock.display()),
53
+
);
54
+
}
55
}
56
}
57
}
···
67
.await
68
.expect("Failed to start MinIO");
69
70
+
let s3_port = s3_container
71
+
.get_host_port_ipv4(9000)
72
+
.await
73
+
.expect("Failed to get S3 port");
74
let s3_endpoint = format!("http://127.0.0.1:{}", s3_port);
75
76
unsafe {
···
84
let sdk_config = aws_config::defaults(BehaviorVersion::latest())
85
.region("us-east-1")
86
.endpoint_url(&s3_endpoint)
87
+
.credentials_provider(Credentials::new(
88
+
"minioadmin",
89
+
"minioadmin",
90
+
None,
91
+
None,
92
+
"test",
93
+
))
94
.load()
95
.await;
96
···
122
.mount(&mock_server)
123
.await;
124
125
+
unsafe {
126
+
std::env::set_var("APPVIEW_URL", mock_server.uri());
127
+
}
128
MOCK_APPVIEW.set(mock_server).ok();
129
130
S3_CONTAINER.set(s3_container).ok();
131
132
+
let container = Postgres::default()
133
+
.with_tag("18-alpine")
134
+
.start()
135
+
.await
136
+
.expect("Failed to start Postgres");
137
let connection_string = format!(
138
"postgres://postgres:postgres@127.0.0.1:{}/postgres",
139
+
container
140
+
.get_host_port_ipv4(5432)
141
+
.await
142
+
.expect("Failed to get port")
143
);
144
145
DB_CONTAINER.set(container).ok();
···
180
181
#[allow(dead_code)]
182
pub async fn upload_test_blob(client: &Client, data: &'static str, mime: &'static str) -> Value {
183
+
let res = client
184
+
.post(format!(
185
+
"{}/xrpc/com.atproto.repo.uploadBlob",
186
+
base_url().await
187
+
))
188
.header(header::CONTENT_TYPE, mime)
189
.bearer_auth(AUTH_TOKEN)
190
.body(data)
···
197
body["blob"].clone()
198
}
199
200
#[allow(dead_code)]
201
pub async fn create_test_post(
202
client: &Client,
203
text: &str,
204
+
reply_to: Option<Value>,
205
) -> (String, String, String) {
206
let collection = "app.bsky.feed.post";
207
let mut record = json!({
···
220
"record": record
221
});
222
223
+
let res = client
224
+
.post(format!(
225
+
"{}/xrpc/com.atproto.repo.createRecord",
226
+
base_url().await
227
+
))
228
.bearer_auth(AUTH_TOKEN)
229
.json(&payload)
230
.send()
···
232
.expect("Failed to send createRecord");
233
234
assert_eq!(res.status(), StatusCode::OK, "Failed to create post record");
235
+
let body: Value = res
236
+
.json()
237
+
.await
238
+
.expect("createRecord response was not JSON");
239
240
+
let uri = body["uri"]
241
+
.as_str()
242
+
.expect("Response had no URI")
243
+
.to_string();
244
+
let cid = body["cid"]
245
+
.as_str()
246
+
.expect("Response had no CID")
247
+
.to_string();
248
+
let rkey = uri
249
+
.split('/')
250
+
.last()
251
+
.expect("URI was malformed")
252
+
.to_string();
253
254
(uri, cid, rkey)
255
}
···
263
"password": "password"
264
});
265
266
+
let res = client
267
+
.post(format!(
268
+
"{}/xrpc/com.atproto.server.createAccount",
269
+
base_url().await
270
+
))
271
.json(&payload)
272
.send()
273
.await
···
278
}
279
280
let body: Value = res.json().await.expect("Invalid JSON");
281
+
let access_jwt = body["accessJwt"]
282
+
.as_str()
283
+
.expect("No accessJwt")
284
+
.to_string();
285
let did = body["did"].as_str().expect("No did").to_string();
286
(access_jwt, did)
287
}
+39
-11
tests/identity.rs
+39
-11
tests/identity.rs
···
1
mod common;
2
use common::*;
3
use reqwest::StatusCode;
4
-
use serde_json::{json, Value};
5
-
use wiremock::{MockServer, Mock, ResponseTemplate};
6
use wiremock::matchers::{method, path};
7
8
// #[tokio::test]
9
// async fn test_resolve_handle() {
···
23
#[tokio::test]
24
async fn test_well_known_did() {
25
let client = client();
26
-
let res = client.get(format!("{}/.well-known/did.json", base_url().await))
27
.send()
28
.await
29
.expect("Failed to send request");
···
71
"did": did
72
});
73
74
-
let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
75
.json(&payload)
76
.send()
77
.await
···
79
80
if res.status() != StatusCode::OK {
81
let status = res.status();
82
-
let body: Value = res.json().await.unwrap_or(json!({"error": "could not parse body"}));
83
panic!("createAccount failed with status {}: {:?}", status, body);
84
}
85
-
let body: Value = res.json().await.expect("createAccount response was not JSON");
86
assert_eq!(body["did"], did);
87
88
-
let res = client.get(format!("{}/u/{}/did.json", base_url().await, handle))
89
.send()
90
.await
91
.expect("Failed to fetch DID doc");
···
111
"password": "password"
112
});
113
114
-
let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
115
.json(&payload)
116
.send()
117
.await
118
.expect("Failed to send request");
119
assert_eq!(res.status(), StatusCode::OK);
120
121
-
let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
122
.json(&payload)
123
.send()
124
.await
···
143
"did": did
144
});
145
146
-
let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
147
.json(&create_payload)
148
.send()
149
.await
···
162
"identifier": handle,
163
"password": "password"
164
});
165
-
let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await))
166
.json(&login_payload)
167
.send()
168
.await
···
1
mod common;
2
use common::*;
3
use reqwest::StatusCode;
4
+
use serde_json::{Value, json};
5
use wiremock::matchers::{method, path};
6
+
use wiremock::{Mock, MockServer, ResponseTemplate};
7
8
// #[tokio::test]
9
// async fn test_resolve_handle() {
···
23
#[tokio::test]
24
async fn test_well_known_did() {
25
let client = client();
26
+
let res = client
27
+
.get(format!("{}/.well-known/did.json", base_url().await))
28
.send()
29
.await
30
.expect("Failed to send request");
···
72
"did": did
73
});
74
75
+
let res = client
76
+
.post(format!(
77
+
"{}/xrpc/com.atproto.server.createAccount",
78
+
base_url().await
79
+
))
80
.json(&payload)
81
.send()
82
.await
···
84
85
if res.status() != StatusCode::OK {
86
let status = res.status();
87
+
let body: Value = res
88
+
.json()
89
+
.await
90
+
.unwrap_or(json!({"error": "could not parse body"}));
91
panic!("createAccount failed with status {}: {:?}", status, body);
92
}
93
+
let body: Value = res
94
+
.json()
95
+
.await
96
+
.expect("createAccount response was not JSON");
97
assert_eq!(body["did"], did);
98
99
+
let res = client
100
+
.get(format!("{}/u/{}/did.json", base_url().await, handle))
101
.send()
102
.await
103
.expect("Failed to fetch DID doc");
···
123
"password": "password"
124
});
125
126
+
let res = client
127
+
.post(format!(
128
+
"{}/xrpc/com.atproto.server.createAccount",
129
+
base_url().await
130
+
))
131
.json(&payload)
132
.send()
133
.await
134
.expect("Failed to send request");
135
assert_eq!(res.status(), StatusCode::OK);
136
137
+
let res = client
138
+
.post(format!(
139
+
"{}/xrpc/com.atproto.server.createAccount",
140
+
base_url().await
141
+
))
142
.json(&payload)
143
.send()
144
.await
···
163
"did": did
164
});
165
166
+
let res = client
167
+
.post(format!(
168
+
"{}/xrpc/com.atproto.server.createAccount",
169
+
base_url().await
170
+
))
171
.json(&create_payload)
172
.send()
173
.await
···
186
"identifier": handle,
187
"password": "password"
188
});
189
+
let res = client
190
+
.post(format!(
191
+
"{}/xrpc/com.atproto.server.createSession",
192
+
base_url().await
193
+
))
194
.json(&login_payload)
195
.send()
196
.await
+376
-45
tests/lifecycle.rs
+376
-45
tests/lifecycle.rs
···
1
mod common;
2
use common::*;
3
4
-
use reqwest::{Client, StatusCode};
5
-
use serde_json::{json, Value};
6
use chrono::Utc;
7
-
#[allow(unused_imports)]
8
use std::time::Duration;
9
10
async fn setup_new_user(handle_prefix: &str) -> (String, String) {
···
19
"email": email,
20
"password": password
21
});
22
-
let create_res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
23
.json(&create_account_payload)
24
.send()
25
.await
26
.expect("setup_new_user: Failed to send createAccount");
27
28
-
if create_res.status() != StatusCode::OK {
29
-
panic!("setup_new_user: Failed to create account: {:?}", create_res.text().await);
30
}
31
32
-
let create_body: Value = create_res.json().await.expect("setup_new_user: createAccount response was not JSON");
33
34
-
let new_did = create_body["did"].as_str().expect("setup_new_user: Response had no DID").to_string();
35
-
let new_jwt = create_body["accessJwt"].as_str().expect("setup_new_user: Response had no accessJwt").to_string();
36
37
(new_did, new_jwt)
38
}
···
59
}
60
});
61
62
-
let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
63
.bearer_auth(&jwt)
64
.json(&create_payload)
65
.send()
66
.await
67
.expect("Failed to send create request");
68
69
-
assert_eq!(create_res.status(), StatusCode::OK, "Failed to create record");
70
-
let create_body: Value = create_res.json().await.expect("create response was not JSON");
71
-
let uri = create_body["uri"].as_str().unwrap();
72
73
74
let params = [
75
("repo", did.as_str()),
76
("collection", collection),
77
("rkey", &rkey),
78
];
79
-
let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
80
.query(¶ms)
81
.send()
82
.await
83
.expect("Failed to send get request");
84
85
-
assert_eq!(get_res.status(), StatusCode::OK, "Failed to get record after create");
86
let get_body: Value = get_res.json().await.expect("get response was not JSON");
87
assert_eq!(get_body["uri"], uri);
88
assert_eq!(get_body["value"]["text"], original_text);
89
-
90
91
let updated_text = "This post has been updated.";
92
let update_payload = json!({
···
100
}
101
});
102
103
-
let update_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
104
.bearer_auth(&jwt)
105
.json(&update_payload)
106
.send()
107
.await
108
.expect("Failed to send update request");
109
110
-
assert_eq!(update_res.status(), StatusCode::OK, "Failed to update record");
111
112
-
113
-
let get_updated_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
114
.query(¶ms)
115
.send()
116
.await
117
.expect("Failed to send get-after-update request");
118
119
-
assert_eq!(get_updated_res.status(), StatusCode::OK, "Failed to get record after update");
120
-
let get_updated_body: Value = get_updated_res.json().await.expect("get-updated response was not JSON");
121
-
assert_eq!(get_updated_body["value"]["text"], updated_text, "Text was not updated");
122
-
123
124
let delete_payload = json!({
125
"repo": did,
···
127
"rkey": rkey
128
});
129
130
-
let delete_res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await))
131
.bearer_auth(&jwt)
132
.json(&delete_payload)
133
.send()
134
.await
135
.expect("Failed to send delete request");
136
137
-
assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete record");
138
139
-
140
-
let get_deleted_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
141
.query(¶ms)
142
.send()
143
.await
144
.expect("Failed to send get-after-delete request");
145
146
-
assert_eq!(get_deleted_res.status(), StatusCode::NOT_FOUND, "Record was found, but it should be deleted");
147
}
148
149
#[tokio::test]
···
161
"displayName": "Original Name"
162
}
163
});
164
-
let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
165
.bearer_auth(&user_jwt)
166
.json(&profile_payload)
167
-
.send().await.expect("create profile failed");
168
169
-
if create_res.status() != StatusCode::OK {
170
return;
171
}
172
173
-
let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
174
.query(&[
175
("repo", &user_did),
176
("collection", &"app.bsky.actor.profile".to_string()),
177
("rkey", &"self".to_string()),
178
])
179
-
.send().await.expect("getRecord failed");
180
let get_body: Value = get_res.json().await.expect("getRecord not json");
181
-
let cid_v1 = get_body["cid"].as_str().expect("Profile v1 had no CID").to_string();
182
183
let update_payload_v2 = json!({
184
"repo": user_did,
···
190
},
191
"swapCommit": cid_v1 // <-- Correctly point to v1
192
});
193
-
let update_res_v2 = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
194
.bearer_auth(&user_jwt)
195
.json(&update_payload_v2)
196
-
.send().await.expect("putRecord v2 failed");
197
-
assert_eq!(update_res_v2.status(), StatusCode::OK, "v2 update failed");
198
let update_body_v2: Value = update_res_v2.json().await.expect("v2 body not json");
199
-
let cid_v2 = update_body_v2["cid"].as_str().expect("v2 response had no CID").to_string();
200
201
let update_payload_v3_stale = json!({
202
"repo": user_did,
···
208
},
209
"swapCommit": cid_v1
210
});
211
-
let update_res_v3_stale = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
212
.bearer_auth(&user_jwt)
213
.json(&update_payload_v3_stale)
214
-
.send().await.expect("putRecord v3 (stale) failed");
215
216
assert_eq!(
217
update_res_v3_stale.status(),
218
-
StatusCode::CONFLICT,
219
"Stale update did not cause a 409 Conflict"
220
);
221
···
229
},
230
"swapCommit": cid_v2 // <-- Correct
231
});
232
-
let update_res_v3_good = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
233
.bearer_auth(&user_jwt)
234
.json(&update_payload_v3_good)
235
-
.send().await.expect("putRecord v3 (good) failed");
236
237
-
assert_eq!(update_res_v3_good.status(), StatusCode::OK, "v3 (good) update failed");
238
}
···
1
mod common;
2
use common::*;
3
4
use chrono::Utc;
5
+
use reqwest;
6
+
use serde_json::{Value, json};
7
use std::time::Duration;
8
9
async fn setup_new_user(handle_prefix: &str) -> (String, String) {
···
18
"email": email,
19
"password": password
20
});
21
+
let create_res = client
22
+
.post(format!(
23
+
"{}/xrpc/com.atproto.server.createAccount",
24
+
base_url().await
25
+
))
26
.json(&create_account_payload)
27
.send()
28
.await
29
.expect("setup_new_user: Failed to send createAccount");
30
31
+
if create_res.status() != reqwest::StatusCode::OK {
32
+
panic!(
33
+
"setup_new_user: Failed to create account: {:?}",
34
+
create_res.text().await
35
+
);
36
}
37
38
+
let create_body: Value = create_res
39
+
.json()
40
+
.await
41
+
.expect("setup_new_user: createAccount response was not JSON");
42
43
+
let new_did = create_body["did"]
44
+
.as_str()
45
+
.expect("setup_new_user: Response had no DID")
46
+
.to_string();
47
+
let new_jwt = create_body["accessJwt"]
48
+
.as_str()
49
+
.expect("setup_new_user: Response had no accessJwt")
50
+
.to_string();
51
52
(new_did, new_jwt)
53
}
···
74
}
75
});
76
77
+
let create_res = client
78
+
.post(format!(
79
+
"{}/xrpc/com.atproto.repo.putRecord",
80
+
base_url().await
81
+
))
82
.bearer_auth(&jwt)
83
.json(&create_payload)
84
.send()
85
.await
86
.expect("Failed to send create request");
87
88
+
if create_res.status() != reqwest::StatusCode::OK {
89
+
let status = create_res.status();
90
+
let body = create_res
91
+
.text()
92
+
.await
93
+
.unwrap_or_else(|_| "Could not get body".to_string());
94
+
panic!(
95
+
"Failed to create record. Status: {}, Body: {}",
96
+
status, body
97
+
);
98
+
}
99
100
+
let create_body: Value = create_res
101
+
.json()
102
+
.await
103
+
.expect("create response was not JSON");
104
+
let uri = create_body["uri"].as_str().unwrap();
105
106
let params = [
107
("repo", did.as_str()),
108
("collection", collection),
109
("rkey", &rkey),
110
];
111
+
let get_res = client
112
+
.get(format!(
113
+
"{}/xrpc/com.atproto.repo.getRecord",
114
+
base_url().await
115
+
))
116
.query(¶ms)
117
.send()
118
.await
119
.expect("Failed to send get request");
120
121
+
assert_eq!(
122
+
get_res.status(),
123
+
reqwest::StatusCode::OK,
124
+
"Failed to get record after create"
125
+
);
126
let get_body: Value = get_res.json().await.expect("get response was not JSON");
127
assert_eq!(get_body["uri"], uri);
128
assert_eq!(get_body["value"]["text"], original_text);
129
130
let updated_text = "This post has been updated.";
131
let update_payload = json!({
···
139
}
140
});
141
142
+
let update_res = client
143
+
.post(format!(
144
+
"{}/xrpc/com.atproto.repo.putRecord",
145
+
base_url().await
146
+
))
147
.bearer_auth(&jwt)
148
.json(&update_payload)
149
.send()
150
.await
151
.expect("Failed to send update request");
152
153
+
assert_eq!(
154
+
update_res.status(),
155
+
reqwest::StatusCode::OK,
156
+
"Failed to update record"
157
+
);
158
159
+
let get_updated_res = client
160
+
.get(format!(
161
+
"{}/xrpc/com.atproto.repo.getRecord",
162
+
base_url().await
163
+
))
164
.query(¶ms)
165
.send()
166
.await
167
.expect("Failed to send get-after-update request");
168
169
+
assert_eq!(
170
+
get_updated_res.status(),
171
+
reqwest::StatusCode::OK,
172
+
"Failed to get record after update"
173
+
);
174
+
let get_updated_body: Value = get_updated_res
175
+
.json()
176
+
.await
177
+
.expect("get-updated response was not JSON");
178
+
assert_eq!(
179
+
get_updated_body["value"]["text"], updated_text,
180
+
"Text was not updated"
181
+
);
182
183
let delete_payload = json!({
184
"repo": did,
···
186
"rkey": rkey
187
});
188
189
+
let delete_res = client
190
+
.post(format!(
191
+
"{}/xrpc/com.atproto.repo.deleteRecord",
192
+
base_url().await
193
+
))
194
.bearer_auth(&jwt)
195
.json(&delete_payload)
196
.send()
197
.await
198
.expect("Failed to send delete request");
199
200
+
assert_eq!(
201
+
delete_res.status(),
202
+
reqwest::StatusCode::OK,
203
+
"Failed to delete record"
204
+
);
205
206
+
let get_deleted_res = client
207
+
.get(format!(
208
+
"{}/xrpc/com.atproto.repo.getRecord",
209
+
base_url().await
210
+
))
211
.query(¶ms)
212
.send()
213
.await
214
.expect("Failed to send get-after-delete request");
215
216
+
assert_eq!(
217
+
get_deleted_res.status(),
218
+
reqwest::StatusCode::NOT_FOUND,
219
+
"Record was found, but it should be deleted"
220
+
);
221
}
222
223
#[tokio::test]
···
235
"displayName": "Original Name"
236
}
237
});
238
+
let create_res = client
239
+
.post(format!(
240
+
"{}/xrpc/com.atproto.repo.putRecord",
241
+
base_url().await
242
+
))
243
.bearer_auth(&user_jwt)
244
.json(&profile_payload)
245
+
.send()
246
+
.await
247
+
.expect("create profile failed");
248
249
+
if create_res.status() != reqwest::StatusCode::OK {
250
return;
251
}
252
253
+
let get_res = client
254
+
.get(format!(
255
+
"{}/xrpc/com.atproto.repo.getRecord",
256
+
base_url().await
257
+
))
258
.query(&[
259
("repo", &user_did),
260
("collection", &"app.bsky.actor.profile".to_string()),
261
("rkey", &"self".to_string()),
262
])
263
+
.send()
264
+
.await
265
+
.expect("getRecord failed");
266
let get_body: Value = get_res.json().await.expect("getRecord not json");
267
+
let cid_v1 = get_body["cid"]
268
+
.as_str()
269
+
.expect("Profile v1 had no CID")
270
+
.to_string();
271
272
let update_payload_v2 = json!({
273
"repo": user_did,
···
279
},
280
"swapCommit": cid_v1 // <-- Correctly point to v1
281
});
282
+
let update_res_v2 = client
283
+
.post(format!(
284
+
"{}/xrpc/com.atproto.repo.putRecord",
285
+
base_url().await
286
+
))
287
.bearer_auth(&user_jwt)
288
.json(&update_payload_v2)
289
+
.send()
290
+
.await
291
+
.expect("putRecord v2 failed");
292
+
assert_eq!(
293
+
update_res_v2.status(),
294
+
reqwest::StatusCode::OK,
295
+
"v2 update failed"
296
+
);
297
let update_body_v2: Value = update_res_v2.json().await.expect("v2 body not json");
298
+
let cid_v2 = update_body_v2["cid"]
299
+
.as_str()
300
+
.expect("v2 response had no CID")
301
+
.to_string();
302
303
let update_payload_v3_stale = json!({
304
"repo": user_did,
···
310
},
311
"swapCommit": cid_v1
312
});
313
+
let update_res_v3_stale = client
314
+
.post(format!(
315
+
"{}/xrpc/com.atproto.repo.putRecord",
316
+
base_url().await
317
+
))
318
.bearer_auth(&user_jwt)
319
.json(&update_payload_v3_stale)
320
+
.send()
321
+
.await
322
+
.expect("putRecord v3 (stale) failed");
323
324
assert_eq!(
325
update_res_v3_stale.status(),
326
+
reqwest::StatusCode::CONFLICT,
327
"Stale update did not cause a 409 Conflict"
328
);
329
···
337
},
338
"swapCommit": cid_v2 // <-- Correct
339
});
340
+
let update_res_v3_good = client
341
+
.post(format!(
342
+
"{}/xrpc/com.atproto.repo.putRecord",
343
+
base_url().await
344
+
))
345
.bearer_auth(&user_jwt)
346
.json(&update_payload_v3_good)
347
+
.send()
348
+
.await
349
+
.expect("putRecord v3 (good) failed");
350
+
351
+
assert_eq!(
352
+
update_res_v3_good.status(),
353
+
reqwest::StatusCode::OK,
354
+
"v3 (good) update failed"
355
+
);
356
+
}
357
+
358
+
async fn create_post(
359
+
client: &reqwest::Client,
360
+
did: &str,
361
+
jwt: &str,
362
+
text: &str,
363
+
) -> (String, String) {
364
+
let collection = "app.bsky.feed.post";
365
+
let rkey = format!("e2e_social_{}", Utc::now().timestamp_millis());
366
+
let now = Utc::now().to_rfc3339();
367
+
368
+
let create_payload = json!({
369
+
"repo": did,
370
+
"collection": collection,
371
+
"rkey": rkey,
372
+
"record": {
373
+
"$type": collection,
374
+
"text": text,
375
+
"createdAt": now
376
+
}
377
+
});
378
+
379
+
let create_res = client
380
+
.post(format!(
381
+
"{}/xrpc/com.atproto.repo.putRecord",
382
+
base_url().await
383
+
))
384
+
.bearer_auth(jwt)
385
+
.json(&create_payload)
386
+
.send()
387
+
.await
388
+
.expect("Failed to send create post request");
389
+
390
+
assert_eq!(
391
+
create_res.status(),
392
+
reqwest::StatusCode::OK,
393
+
"Failed to create post record"
394
+
);
395
+
let create_body: Value = create_res
396
+
.json()
397
+
.await
398
+
.expect("create post response was not JSON");
399
+
let uri = create_body["uri"].as_str().unwrap().to_string();
400
+
let cid = create_body["cid"].as_str().unwrap().to_string();
401
+
(uri, cid)
402
+
}
403
+
404
+
async fn create_follow(
405
+
client: &reqwest::Client,
406
+
follower_did: &str,
407
+
follower_jwt: &str,
408
+
followee_did: &str,
409
+
) -> (String, String) {
410
+
let collection = "app.bsky.graph.follow";
411
+
let rkey = format!("e2e_follow_{}", Utc::now().timestamp_millis());
412
+
let now = Utc::now().to_rfc3339();
413
+
414
+
let create_payload = json!({
415
+
"repo": follower_did,
416
+
"collection": collection,
417
+
"rkey": rkey,
418
+
"record": {
419
+
"$type": collection,
420
+
"subject": followee_did,
421
+
"createdAt": now
422
+
}
423
+
});
424
425
+
let create_res = client
426
+
.post(format!(
427
+
"{}/xrpc/com.atproto.repo.putRecord",
428
+
base_url().await
429
+
))
430
+
.bearer_auth(follower_jwt)
431
+
.json(&create_payload)
432
+
.send()
433
+
.await
434
+
.expect("Failed to send create follow request");
435
+
436
+
assert_eq!(
437
+
create_res.status(),
438
+
reqwest::StatusCode::OK,
439
+
"Failed to create follow record"
440
+
);
441
+
let create_body: Value = create_res
442
+
.json()
443
+
.await
444
+
.expect("create follow response was not JSON");
445
+
let uri = create_body["uri"].as_str().unwrap().to_string();
446
+
let cid = create_body["cid"].as_str().unwrap().to_string();
447
+
(uri, cid)
448
+
}
449
+
450
+
#[tokio::test]
451
+
#[ignore]
452
+
async fn test_social_flow_lifecycle() {
453
+
let client = client();
454
+
455
+
let (alice_did, alice_jwt) = setup_new_user("alice-social").await;
456
+
let (bob_did, bob_jwt) = setup_new_user("bob-social").await;
457
+
458
+
let (post1_uri, _) = create_post(&client, &alice_did, &alice_jwt, "Alice's first post!").await;
459
+
460
+
create_follow(&client, &bob_did, &bob_jwt, &alice_did).await;
461
+
462
+
tokio::time::sleep(Duration::from_secs(1)).await;
463
+
464
+
let timeline_res_1 = client
465
+
.get(format!(
466
+
"{}/xrpc/app.bsky.feed.getTimeline",
467
+
base_url().await
468
+
))
469
+
.bearer_auth(&bob_jwt)
470
+
.send()
471
+
.await
472
+
.expect("Failed to get timeline (1)");
473
+
474
+
assert_eq!(
475
+
timeline_res_1.status(),
476
+
reqwest::StatusCode::OK,
477
+
"Failed to get timeline (1)"
478
+
);
479
+
let timeline_body_1: Value = timeline_res_1.json().await.expect("Timeline (1) not JSON");
480
+
let feed_1 = timeline_body_1["feed"].as_array().unwrap();
481
+
assert_eq!(feed_1.len(), 1, "Timeline should have 1 post");
482
+
assert_eq!(
483
+
feed_1[0]["post"]["uri"], post1_uri,
484
+
"Post URI mismatch in timeline (1)"
485
+
);
486
+
487
+
let (post2_uri, _) = create_post(
488
+
&client,
489
+
&alice_did,
490
+
&alice_jwt,
491
+
"Alice's second post, so exciting!",
492
+
)
493
+
.await;
494
+
495
+
tokio::time::sleep(Duration::from_secs(1)).await;
496
+
497
+
let timeline_res_2 = client
498
+
.get(format!(
499
+
"{}/xrpc/app.bsky.feed.getTimeline",
500
+
base_url().await
501
+
))
502
+
.bearer_auth(&bob_jwt)
503
+
.send()
504
+
.await
505
+
.expect("Failed to get timeline (2)");
506
+
507
+
assert_eq!(
508
+
timeline_res_2.status(),
509
+
reqwest::StatusCode::OK,
510
+
"Failed to get timeline (2)"
511
+
);
512
+
let timeline_body_2: Value = timeline_res_2.json().await.expect("Timeline (2) not JSON");
513
+
let feed_2 = timeline_body_2["feed"].as_array().unwrap();
514
+
assert_eq!(feed_2.len(), 2, "Timeline should have 2 posts");
515
+
assert_eq!(
516
+
feed_2[0]["post"]["uri"], post2_uri,
517
+
"Post 2 should be first"
518
+
);
519
+
assert_eq!(
520
+
feed_2[1]["post"]["uri"], post1_uri,
521
+
"Post 1 should be second"
522
+
);
523
+
524
+
let delete_payload = json!({
525
+
"repo": alice_did,
526
+
"collection": "app.bsky.feed.post",
527
+
"rkey": post1_uri.split('/').last().unwrap()
528
+
});
529
+
let delete_res = client
530
+
.post(format!(
531
+
"{}/xrpc/com.atproto.repo.deleteRecord",
532
+
base_url().await
533
+
))
534
+
.bearer_auth(&alice_jwt)
535
+
.json(&delete_payload)
536
+
.send()
537
+
.await
538
+
.expect("Failed to send delete request");
539
+
assert_eq!(
540
+
delete_res.status(),
541
+
reqwest::StatusCode::OK,
542
+
"Failed to delete record"
543
+
);
544
+
545
+
tokio::time::sleep(Duration::from_secs(1)).await;
546
+
547
+
let timeline_res_3 = client
548
+
.get(format!(
549
+
"{}/xrpc/app.bsky.feed.getTimeline",
550
+
base_url().await
551
+
))
552
+
.bearer_auth(&bob_jwt)
553
+
.send()
554
+
.await
555
+
.expect("Failed to get timeline (3)");
556
+
557
+
assert_eq!(
558
+
timeline_res_3.status(),
559
+
reqwest::StatusCode::OK,
560
+
"Failed to get timeline (3)"
561
+
);
562
+
let timeline_body_3: Value = timeline_res_3.json().await.expect("Timeline (3) not JSON");
563
+
let feed_3 = timeline_body_3["feed"].as_array().unwrap();
564
+
assert_eq!(feed_3.len(), 1, "Timeline should have 1 post after delete");
565
+
assert_eq!(
566
+
feed_3[0]["post"]["uri"], post2_uri,
567
+
"Only post 2 should remain"
568
+
);
569
}
+24
-16
tests/proxy.rs
+24
-16
tests/proxy.rs
···
1
mod common;
2
3
-
use axum::{
4
-
routing::any,
5
-
Router,
6
-
extract::Request,
7
-
http::StatusCode,
8
-
};
9
-
use tokio::net::TcpListener;
10
use reqwest::Client;
11
use std::sync::Arc;
12
-
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
13
14
-
async fn spawn_mock_upstream() -> (String, tokio::sync::mpsc::Receiver<(String, String, Option<String>)>) {
15
let (tx, rx) = tokio::sync::mpsc::channel(10);
16
let tx = Arc::new(tx);
17
···
20
async move {
21
let method = req.method().to_string();
22
let uri = req.uri().to_string();
23
-
let auth = req.headers().get("Authorization")
24
.and_then(|h| h.to_str().ok())
25
.map(|s| s.to_string());
26
···
45
let (upstream_url, mut rx) = spawn_mock_upstream().await;
46
let client = Client::new();
47
48
-
let res = client.get(format!("{}/xrpc/com.example.test", app_url))
49
.header("atproto-proxy", &upstream_url)
50
.header("Authorization", "Bearer test-token")
51
.send()
···
65
async fn test_proxy_via_env_var() {
66
let (upstream_url, mut rx) = spawn_mock_upstream().await;
67
68
-
unsafe { std::env::set_var("APPVIEW_URL", &upstream_url); }
69
70
let app_url = common::base_url().await;
71
let client = Client::new();
72
73
-
let res = client.get(format!("{}/xrpc/com.example.envtest", app_url))
74
.send()
75
.await
76
.unwrap();
···
85
#[tokio::test]
86
#[ignore]
87
async fn test_proxy_missing_config() {
88
-
unsafe { std::env::remove_var("APPVIEW_URL"); }
89
90
let app_url = common::base_url().await;
91
let client = Client::new();
92
93
-
let res = client.get(format!("{}/xrpc/com.example.fail", app_url))
94
.send()
95
.await
96
.unwrap();
···
106
107
let (access_jwt, did) = common::create_account_and_login(&client).await;
108
109
-
let res = client.get(format!("{}/xrpc/com.example.signed", app_url))
110
.header("atproto-proxy", &upstream_url)
111
.header("Authorization", format!("Bearer {}", access_jwt))
112
.send()
···
1
mod common;
2
3
+
use axum::{Router, extract::Request, http::StatusCode, routing::any};
4
+
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
5
use reqwest::Client;
6
use std::sync::Arc;
7
+
use tokio::net::TcpListener;
8
9
+
async fn spawn_mock_upstream() -> (
10
+
String,
11
+
tokio::sync::mpsc::Receiver<(String, String, Option<String>)>,
12
+
) {
13
let (tx, rx) = tokio::sync::mpsc::channel(10);
14
let tx = Arc::new(tx);
15
···
18
async move {
19
let method = req.method().to_string();
20
let uri = req.uri().to_string();
21
+
let auth = req
22
+
.headers()
23
+
.get("Authorization")
24
.and_then(|h| h.to_str().ok())
25
.map(|s| s.to_string());
26
···
45
let (upstream_url, mut rx) = spawn_mock_upstream().await;
46
let client = Client::new();
47
48
+
let res = client
49
+
.get(format!("{}/xrpc/com.example.test", app_url))
50
.header("atproto-proxy", &upstream_url)
51
.header("Authorization", "Bearer test-token")
52
.send()
···
66
async fn test_proxy_via_env_var() {
67
let (upstream_url, mut rx) = spawn_mock_upstream().await;
68
69
+
unsafe {
70
+
std::env::set_var("APPVIEW_URL", &upstream_url);
71
+
}
72
73
let app_url = common::base_url().await;
74
let client = Client::new();
75
76
+
let res = client
77
+
.get(format!("{}/xrpc/com.example.envtest", app_url))
78
.send()
79
.await
80
.unwrap();
···
89
#[tokio::test]
90
#[ignore]
91
async fn test_proxy_missing_config() {
92
+
unsafe {
93
+
std::env::remove_var("APPVIEW_URL");
94
+
}
95
96
let app_url = common::base_url().await;
97
let client = Client::new();
98
99
+
let res = client
100
+
.get(format!("{}/xrpc/com.example.fail", app_url))
101
.send()
102
.await
103
.unwrap();
···
113
114
let (access_jwt, did) = common::create_account_and_login(&client).await;
115
116
+
let res = client
117
+
.get(format!("{}/xrpc/com.example.signed", app_url))
118
.header("atproto-proxy", &upstream_url)
119
.header("Authorization", format!("Bearer {}", access_jwt))
120
.send()
+103
-28
tests/repo.rs
+103
-28
tests/repo.rs
···
1
mod common;
2
use common::*;
3
4
-
use reqwest::{header, StatusCode};
5
-
use serde_json::{json, Value};
6
use chrono::Utc;
7
8
#[tokio::test]
9
#[ignore]
···
15
("rkey", "self"),
16
];
17
18
-
let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
19
.query(¶ms)
20
.send()
21
.await
···
36
("rkey", "nonexistent"),
37
];
38
39
-
let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
40
.query(¶ms)
41
.send()
42
.await
···
50
#[tokio::test]
51
async fn test_upload_blob_no_auth() {
52
let client = client();
53
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await))
54
.header(header::CONTENT_TYPE, "text/plain")
55
.body("no auth")
56
.send()
···
66
async fn test_upload_blob_success() {
67
let client = client();
68
let (token, _) = create_account_and_login(&client).await;
69
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await))
70
.header(header::CONTENT_TYPE, "text/plain")
71
.bearer_auth(token)
72
.body("This is our blob data")
···
90
"record": {}
91
});
92
93
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
94
.json(&payload)
95
.send()
96
.await
···
118
}
119
});
120
121
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
122
.bearer_auth(token)
123
.json(&payload)
124
.send()
···
135
#[ignore]
136
async fn test_get_record_missing_params() {
137
let client = client();
138
-
let params = [
139
-
("repo", "did:plc:12345"),
140
-
];
141
142
-
let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
143
.query(¶ms)
144
.send()
145
.await
146
.expect("Failed to send request");
147
148
-
assert_eq!(res.status(), StatusCode::BAD_REQUEST, "Expected 400 for missing params");
149
}
150
151
#[tokio::test]
152
async fn test_upload_blob_bad_token() {
153
let client = client();
154
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await))
155
.header(header::CONTENT_TYPE, "text/plain")
156
.bearer_auth(BAD_AUTH_TOKEN)
157
.body("This is our blob data")
···
181
}
182
});
183
184
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
185
.bearer_auth(token)
186
.json(&payload)
187
.send()
188
.await
189
.expect("Failed to send request");
190
191
-
assert_eq!(res.status(), StatusCode::FORBIDDEN, "Expected 403 for mismatched repo and auth");
192
}
193
194
#[tokio::test]
···
207
}
208
});
209
210
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
211
.bearer_auth(token)
212
.json(&payload)
213
.send()
214
.await
215
.expect("Failed to send request");
216
217
-
assert_eq!(res.status(), StatusCode::BAD_REQUEST, "Expected 400 for invalid record schema");
218
}
219
220
#[tokio::test]
221
async fn test_upload_blob_unsupported_mime_type() {
222
let client = client();
223
let (token, _) = create_account_and_login(&client).await;
224
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await))
225
.header(header::CONTENT_TYPE, "application/xml")
226
.bearer_auth(token)
227
.body("<xml>not an image</xml>")
···
242
("collection", "app.bsky.feed.post"),
243
("limit", "10"),
244
];
245
-
let res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await))
246
.query(¶ms)
247
.send()
248
.await
···
255
async fn test_describe_repo() {
256
let client = client();
257
let (_, did) = create_account_and_login(&client).await;
258
-
let params = [
259
-
("repo", did.as_str()),
260
-
];
261
-
let res = client.get(format!("{}/xrpc/com.atproto.repo.describeRepo", base_url().await))
262
.query(¶ms)
263
.send()
264
.await
···
282
}
283
});
284
285
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await))
286
.json(&payload)
287
.bearer_auth(token)
288
.send()
···
313
}
314
});
315
316
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await))
317
.json(&payload)
318
.bearer_auth(token)
319
.send()
···
322
323
assert_eq!(res.status(), StatusCode::OK);
324
let body: Value = res.json().await.expect("Response was not valid JSON");
325
-
assert_eq!(body["uri"], format!("at://{}/app.bsky.feed.post/{}", did, rkey));
326
// assert_eq!(body["cid"], "bafyreihy");
327
}
328
···
336
"collection": "app.bsky.feed.post",
337
"rkey": "some_post_to_delete"
338
});
339
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await))
340
.bearer_auth(token)
341
.json(&payload)
342
.send()
···
1
mod common;
2
use common::*;
3
4
use chrono::Utc;
5
+
use reqwest::{StatusCode, header};
6
+
use serde_json::{Value, json};
7
8
#[tokio::test]
9
#[ignore]
···
15
("rkey", "self"),
16
];
17
18
+
let res = client
19
+
.get(format!(
20
+
"{}/xrpc/com.atproto.repo.getRecord",
21
+
base_url().await
22
+
))
23
.query(¶ms)
24
.send()
25
.await
···
40
("rkey", "nonexistent"),
41
];
42
43
+
let res = client
44
+
.get(format!(
45
+
"{}/xrpc/com.atproto.repo.getRecord",
46
+
base_url().await
47
+
))
48
.query(¶ms)
49
.send()
50
.await
···
58
#[tokio::test]
59
async fn test_upload_blob_no_auth() {
60
let client = client();
61
+
let res = client
62
+
.post(format!(
63
+
"{}/xrpc/com.atproto.repo.uploadBlob",
64
+
base_url().await
65
+
))
66
.header(header::CONTENT_TYPE, "text/plain")
67
.body("no auth")
68
.send()
···
78
async fn test_upload_blob_success() {
79
let client = client();
80
let (token, _) = create_account_and_login(&client).await;
81
+
let res = client
82
+
.post(format!(
83
+
"{}/xrpc/com.atproto.repo.uploadBlob",
84
+
base_url().await
85
+
))
86
.header(header::CONTENT_TYPE, "text/plain")
87
.bearer_auth(token)
88
.body("This is our blob data")
···
106
"record": {}
107
});
108
109
+
let res = client
110
+
.post(format!(
111
+
"{}/xrpc/com.atproto.repo.putRecord",
112
+
base_url().await
113
+
))
114
.json(&payload)
115
.send()
116
.await
···
138
}
139
});
140
141
+
let res = client
142
+
.post(format!(
143
+
"{}/xrpc/com.atproto.repo.putRecord",
144
+
base_url().await
145
+
))
146
.bearer_auth(token)
147
.json(&payload)
148
.send()
···
159
#[ignore]
160
async fn test_get_record_missing_params() {
161
let client = client();
162
+
let params = [("repo", "did:plc:12345")];
163
164
+
let res = client
165
+
.get(format!(
166
+
"{}/xrpc/com.atproto.repo.getRecord",
167
+
base_url().await
168
+
))
169
.query(¶ms)
170
.send()
171
.await
172
.expect("Failed to send request");
173
174
+
assert_eq!(
175
+
res.status(),
176
+
StatusCode::BAD_REQUEST,
177
+
"Expected 400 for missing params"
178
+
);
179
}
180
181
#[tokio::test]
182
async fn test_upload_blob_bad_token() {
183
let client = client();
184
+
let res = client
185
+
.post(format!(
186
+
"{}/xrpc/com.atproto.repo.uploadBlob",
187
+
base_url().await
188
+
))
189
.header(header::CONTENT_TYPE, "text/plain")
190
.bearer_auth(BAD_AUTH_TOKEN)
191
.body("This is our blob data")
···
215
}
216
});
217
218
+
let res = client
219
+
.post(format!(
220
+
"{}/xrpc/com.atproto.repo.putRecord",
221
+
base_url().await
222
+
))
223
.bearer_auth(token)
224
.json(&payload)
225
.send()
226
.await
227
.expect("Failed to send request");
228
229
+
assert_eq!(
230
+
res.status(),
231
+
StatusCode::FORBIDDEN,
232
+
"Expected 403 for mismatched repo and auth"
233
+
);
234
}
235
236
#[tokio::test]
···
249
}
250
});
251
252
+
let res = client
253
+
.post(format!(
254
+
"{}/xrpc/com.atproto.repo.putRecord",
255
+
base_url().await
256
+
))
257
.bearer_auth(token)
258
.json(&payload)
259
.send()
260
.await
261
.expect("Failed to send request");
262
263
+
assert_eq!(
264
+
res.status(),
265
+
StatusCode::BAD_REQUEST,
266
+
"Expected 400 for invalid record schema"
267
+
);
268
}
269
270
#[tokio::test]
271
async fn test_upload_blob_unsupported_mime_type() {
272
let client = client();
273
let (token, _) = create_account_and_login(&client).await;
274
+
let res = client
275
+
.post(format!(
276
+
"{}/xrpc/com.atproto.repo.uploadBlob",
277
+
base_url().await
278
+
))
279
.header(header::CONTENT_TYPE, "application/xml")
280
.bearer_auth(token)
281
.body("<xml>not an image</xml>")
···
296
("collection", "app.bsky.feed.post"),
297
("limit", "10"),
298
];
299
+
let res = client
300
+
.get(format!(
301
+
"{}/xrpc/com.atproto.repo.listRecords",
302
+
base_url().await
303
+
))
304
.query(¶ms)
305
.send()
306
.await
···
313
async fn test_describe_repo() {
314
let client = client();
315
let (_, did) = create_account_and_login(&client).await;
316
+
let params = [("repo", did.as_str())];
317
+
let res = client
318
+
.get(format!(
319
+
"{}/xrpc/com.atproto.repo.describeRepo",
320
+
base_url().await
321
+
))
322
.query(¶ms)
323
.send()
324
.await
···
342
}
343
});
344
345
+
let res = client
346
+
.post(format!(
347
+
"{}/xrpc/com.atproto.repo.createRecord",
348
+
base_url().await
349
+
))
350
.json(&payload)
351
.bearer_auth(token)
352
.send()
···
377
}
378
});
379
380
+
let res = client
381
+
.post(format!(
382
+
"{}/xrpc/com.atproto.repo.createRecord",
383
+
base_url().await
384
+
))
385
.json(&payload)
386
.bearer_auth(token)
387
.send()
···
390
391
assert_eq!(res.status(), StatusCode::OK);
392
let body: Value = res.json().await.expect("Response was not valid JSON");
393
+
assert_eq!(
394
+
body["uri"],
395
+
format!("at://{}/app.bsky.feed.post/{}", did, rkey)
396
+
);
397
// assert_eq!(body["cid"], "bafyreihy");
398
}
399
···
407
"collection": "app.bsky.feed.post",
408
"rkey": "some_post_to_delete"
409
});
410
+
let res = client
411
+
.post(format!(
412
+
"{}/xrpc/com.atproto.repo.deleteRecord",
413
+
base_url().await
414
+
))
415
.bearer_auth(token)
416
.json(&payload)
417
.send()
+71
-17
tests/server.rs
+71
-17
tests/server.rs
···
2
use common::*;
3
4
use reqwest::StatusCode;
5
-
use serde_json::{json, Value};
6
7
#[tokio::test]
8
async fn test_health() {
9
let client = client();
10
-
let res = client.get(format!("{}/health", base_url().await))
11
.send()
12
.await
13
.expect("Failed to send request");
···
19
#[tokio::test]
20
async fn test_describe_server() {
21
let client = client();
22
-
let res = client.get(format!("{}/xrpc/com.atproto.server.describeServer", base_url().await))
23
.send()
24
.await
25
.expect("Failed to send request");
···
39
"email": format!("{}@example.com", handle),
40
"password": "password"
41
});
42
-
let _ = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
43
.json(&payload)
44
.send()
45
.await;
···
49
"password": "password"
50
});
51
52
-
let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await))
53
.json(&payload)
54
.send()
55
.await
···
67
"password": "password"
68
});
69
70
-
let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await))
71
.json(&payload)
72
.send()
73
.await
74
.expect("Failed to send request");
75
76
-
assert!(res.status() == StatusCode::BAD_REQUEST || res.status() == StatusCode::UNPROCESSABLE_ENTITY,
77
-
"Expected 400 or 422 for missing identifier, got {}", res.status());
78
}
79
80
#[tokio::test]
···
86
"password": "password"
87
});
88
89
-
let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
90
.json(&payload)
91
.send()
92
.await
93
.expect("Failed to send request");
94
95
-
assert_eq!(res.status(), StatusCode::BAD_REQUEST, "Expected 400 for invalid handle chars");
96
}
97
98
#[tokio::test]
99
async fn test_get_session() {
100
let client = client();
101
-
let res = client.get(format!("{}/xrpc/com.atproto.server.getSession", base_url().await))
102
.bearer_auth(AUTH_TOKEN)
103
.send()
104
.await
···
117
"email": format!("{}@example.com", handle),
118
"password": "password"
119
});
120
-
let _ = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
121
.json(&payload)
122
.send()
123
.await;
···
126
"identifier": handle,
127
"password": "password"
128
});
129
-
let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await))
130
.json(&login_payload)
131
.send()
132
.await
···
134
135
assert_eq!(res.status(), StatusCode::OK);
136
let body: Value = res.json().await.expect("Invalid JSON");
137
-
let refresh_jwt = body["refreshJwt"].as_str().expect("No refreshJwt").to_string();
138
-
let access_jwt = body["accessJwt"].as_str().expect("No accessJwt").to_string();
139
140
-
let res = client.post(format!("{}/xrpc/com.atproto.server.refreshSession", base_url().await))
141
.bearer_auth(&refresh_jwt)
142
.send()
143
.await
···
154
#[tokio::test]
155
async fn test_delete_session() {
156
let client = client();
157
-
let res = client.post(format!("{}/xrpc/com.atproto.server.deleteSession", base_url().await))
158
.bearer_auth(AUTH_TOKEN)
159
.send()
160
.await
···
2
use common::*;
3
4
use reqwest::StatusCode;
5
+
use serde_json::{Value, json};
6
7
#[tokio::test]
8
async fn test_health() {
9
let client = client();
10
+
let res = client
11
+
.get(format!("{}/health", base_url().await))
12
.send()
13
.await
14
.expect("Failed to send request");
···
20
#[tokio::test]
21
async fn test_describe_server() {
22
let client = client();
23
+
let res = client
24
+
.get(format!(
25
+
"{}/xrpc/com.atproto.server.describeServer",
26
+
base_url().await
27
+
))
28
.send()
29
.await
30
.expect("Failed to send request");
···
44
"email": format!("{}@example.com", handle),
45
"password": "password"
46
});
47
+
let _ = client
48
+
.post(format!(
49
+
"{}/xrpc/com.atproto.server.createAccount",
50
+
base_url().await
51
+
))
52
.json(&payload)
53
.send()
54
.await;
···
58
"password": "password"
59
});
60
61
+
let res = client
62
+
.post(format!(
63
+
"{}/xrpc/com.atproto.server.createSession",
64
+
base_url().await
65
+
))
66
.json(&payload)
67
.send()
68
.await
···
80
"password": "password"
81
});
82
83
+
let res = client
84
+
.post(format!(
85
+
"{}/xrpc/com.atproto.server.createSession",
86
+
base_url().await
87
+
))
88
.json(&payload)
89
.send()
90
.await
91
.expect("Failed to send request");
92
93
+
assert!(
94
+
res.status() == StatusCode::BAD_REQUEST || res.status() == StatusCode::UNPROCESSABLE_ENTITY,
95
+
"Expected 400 or 422 for missing identifier, got {}",
96
+
res.status()
97
+
);
98
}
99
100
#[tokio::test]
···
106
"password": "password"
107
});
108
109
+
let res = client
110
+
.post(format!(
111
+
"{}/xrpc/com.atproto.server.createAccount",
112
+
base_url().await
113
+
))
114
.json(&payload)
115
.send()
116
.await
117
.expect("Failed to send request");
118
119
+
assert_eq!(
120
+
res.status(),
121
+
StatusCode::BAD_REQUEST,
122
+
"Expected 400 for invalid handle chars"
123
+
);
124
}
125
126
#[tokio::test]
127
async fn test_get_session() {
128
let client = client();
129
+
let res = client
130
+
.get(format!(
131
+
"{}/xrpc/com.atproto.server.getSession",
132
+
base_url().await
133
+
))
134
.bearer_auth(AUTH_TOKEN)
135
.send()
136
.await
···
149
"email": format!("{}@example.com", handle),
150
"password": "password"
151
});
152
+
let _ = client
153
+
.post(format!(
154
+
"{}/xrpc/com.atproto.server.createAccount",
155
+
base_url().await
156
+
))
157
.json(&payload)
158
.send()
159
.await;
···
162
"identifier": handle,
163
"password": "password"
164
});
165
+
let res = client
166
+
.post(format!(
167
+
"{}/xrpc/com.atproto.server.createSession",
168
+
base_url().await
169
+
))
170
.json(&login_payload)
171
.send()
172
.await
···
174
175
assert_eq!(res.status(), StatusCode::OK);
176
let body: Value = res.json().await.expect("Invalid JSON");
177
+
let refresh_jwt = body["refreshJwt"]
178
+
.as_str()
179
+
.expect("No refreshJwt")
180
+
.to_string();
181
+
let access_jwt = body["accessJwt"]
182
+
.as_str()
183
+
.expect("No accessJwt")
184
+
.to_string();
185
186
+
let res = client
187
+
.post(format!(
188
+
"{}/xrpc/com.atproto.server.refreshSession",
189
+
base_url().await
190
+
))
191
.bearer_auth(&refresh_jwt)
192
.send()
193
.await
···
204
#[tokio::test]
205
async fn test_delete_session() {
206
let client = client();
207
+
let res = client
208
+
.post(format!(
209
+
"{}/xrpc/com.atproto.server.deleteSession",
210
+
base_url().await
211
+
))
212
.bearer_auth(AUTH_TOKEN)
213
.send()
214
.await
+11
-5
tests/sync.rs
+11
-5
tests/sync.rs
···
6
#[ignore]
7
async fn test_get_repo() {
8
let client = client();
9
-
let params = [
10
-
("did", AUTH_DID),
11
-
];
12
-
let res = client.get(format!("{}/xrpc/com.atproto.sync.getRepo", base_url().await))
13
.query(¶ms)
14
.send()
15
.await
···
26
("did", AUTH_DID),
27
// "cids" would be a list of CIDs
28
];
29
-
let res = client.get(format!("{}/xrpc/com.atproto.sync.getBlocks", base_url().await))
30
.query(¶ms)
31
.send()
32
.await
···
6
#[ignore]
7
async fn test_get_repo() {
8
let client = client();
9
+
let params = [("did", AUTH_DID)];
10
+
let res = client
11
+
.get(format!(
12
+
"{}/xrpc/com.atproto.sync.getRepo",
13
+
base_url().await
14
+
))
15
.query(¶ms)
16
.send()
17
.await
···
28
("did", AUTH_DID),
29
// "cids" would be a list of CIDs
30
];
31
+
let res = client
32
+
.get(format!(
33
+
"{}/xrpc/com.atproto.sync.getBlocks",
34
+
base_url().await
35
+
))
36
.query(¶ms)
37
.send()
38
.await