this repo has no description
1use crate::api::error::ApiError;
2use crate::api::repo::record::utils::create_signed_commit;
3use crate::auth::BearerAuth;
4use crate::delegation::{self, DelegationActionType};
5use crate::oauth::db as oauth_db;
6use crate::state::{AppState, RateLimitKind};
7use crate::types::{Did, Handle};
8use crate::util::extract_client_ip;
9use axum::{
10 Json,
11 extract::{Query, State},
12 http::{HeaderMap, StatusCode},
13 response::{IntoResponse, Response},
14};
15use jacquard::types::{integer::LimitedU32, string::Tid};
16use jacquard_repo::{mst::Mst, storage::BlockStore};
17use serde::{Deserialize, Serialize};
18use serde_json::json;
19use std::sync::Arc;
20use tracing::{error, info, warn};
21
22#[derive(Debug, Serialize)]
23#[serde(rename_all = "camelCase")]
24pub struct ControllerInfo {
25 pub did: Did,
26 pub handle: Handle,
27 pub granted_scopes: String,
28 pub granted_at: chrono::DateTime<chrono::Utc>,
29 pub is_active: bool,
30}
31
32#[derive(Debug, Serialize)]
33pub struct ListControllersResponse {
34 pub controllers: Vec<ControllerInfo>,
35}
36
37pub async fn list_controllers(State(state): State<AppState>, auth: BearerAuth) -> Response {
38 let controllers = match delegation::get_delegations_for_account(&state.db, &auth.0.did).await {
39 Ok(c) => c,
40 Err(e) => {
41 tracing::error!("Failed to list controllers: {:?}", e);
42 return ApiError::InternalError(Some("Failed to list controllers".into())).into_response();
43 }
44 };
45
46 Json(ListControllersResponse {
47 controllers: controllers
48 .into_iter()
49 .map(|c| ControllerInfo {
50 did: c.did.into(),
51 handle: c.handle,
52 granted_scopes: c.granted_scopes,
53 granted_at: c.granted_at,
54 is_active: c.is_active,
55 })
56 .collect(),
57 })
58 .into_response()
59}
60
61#[derive(Debug, Deserialize)]
62pub struct AddControllerInput {
63 pub controller_did: Did,
64 pub granted_scopes: String,
65}
66
67pub async fn add_controller(
68 State(state): State<AppState>,
69 auth: BearerAuth,
70 Json(input): Json<AddControllerInput>,
71) -> Response {
72 if let Err(e) = delegation::scopes::validate_delegation_scopes(&input.granted_scopes) {
73 return ApiError::InvalidScopes(e).into_response();
74 }
75
76 let controller_exists: bool = sqlx::query_scalar!(
77 r#"SELECT EXISTS(SELECT 1 FROM users WHERE did = $1) as "exists!""#,
78 input.controller_did.as_str()
79 )
80 .fetch_one(&state.db)
81 .await
82 .unwrap_or(false);
83
84 if !controller_exists {
85 return ApiError::ControllerNotFound.into_response();
86 }
87
88 match delegation::controls_any_accounts(&state.db, &auth.0.did).await {
89 Ok(true) => {
90 return ApiError::InvalidDelegation(
91 "Cannot add controllers to an account that controls other accounts".into(),
92 )
93 .into_response();
94 }
95 Err(e) => {
96 tracing::error!("Failed to check delegation status: {:?}", e);
97 return ApiError::InternalError(Some("Failed to verify delegation status".into()))
98 .into_response();
99 }
100 Ok(false) => {}
101 }
102
103 match delegation::has_any_controllers(&state.db, &input.controller_did).await {
104 Ok(true) => {
105 return ApiError::InvalidDelegation(
106 "Cannot add a controlled account as a controller".into(),
107 )
108 .into_response();
109 }
110 Err(e) => {
111 tracing::error!("Failed to check controller status: {:?}", e);
112 return ApiError::InternalError(Some("Failed to verify controller status".into()))
113 .into_response();
114 }
115 Ok(false) => {}
116 }
117
118 match delegation::create_delegation(
119 &state.db,
120 &auth.0.did,
121 &input.controller_did,
122 &input.granted_scopes,
123 &auth.0.did,
124 )
125 .await
126 {
127 Ok(_) => {
128 let _ = delegation::log_delegation_action(
129 &state.db,
130 &auth.0.did,
131 &auth.0.did,
132 Some(&input.controller_did),
133 DelegationActionType::GrantCreated,
134 Some(serde_json::json!({
135 "granted_scopes": input.granted_scopes
136 })),
137 None,
138 None,
139 )
140 .await;
141
142 (
143 StatusCode::OK,
144 Json(serde_json::json!({
145 "success": true
146 })),
147 )
148 .into_response()
149 }
150 Err(e) => {
151 tracing::error!("Failed to add controller: {:?}", e);
152 ApiError::InternalError(Some("Failed to add controller".into())).into_response()
153 }
154 }
155}
156
157#[derive(Debug, Deserialize)]
158pub struct RemoveControllerInput {
159 pub controller_did: Did,
160}
161
162pub async fn remove_controller(
163 State(state): State<AppState>,
164 auth: BearerAuth,
165 Json(input): Json<RemoveControllerInput>,
166) -> Response {
167 match delegation::revoke_delegation(&state.db, &auth.0.did, &input.controller_did, &auth.0.did)
168 .await
169 {
170 Ok(true) => {
171 let revoked_app_passwords = sqlx::query_scalar!(
172 r#"DELETE FROM app_passwords
173 WHERE user_id = (SELECT id FROM users WHERE did = $1)
174 AND created_by_controller_did = $2
175 RETURNING id"#,
176 &auth.0.did,
177 input.controller_did.as_str()
178 )
179 .fetch_all(&state.db)
180 .await
181 .map(|r| r.len())
182 .unwrap_or(0);
183
184 let revoked_oauth_tokens = oauth_db::revoke_tokens_for_controller(
185 &state.db,
186 &auth.0.did,
187 &input.controller_did,
188 )
189 .await
190 .unwrap_or(0);
191
192 let _ = delegation::log_delegation_action(
193 &state.db,
194 &auth.0.did,
195 &auth.0.did,
196 Some(&input.controller_did),
197 DelegationActionType::GrantRevoked,
198 Some(serde_json::json!({
199 "revoked_app_passwords": revoked_app_passwords,
200 "revoked_oauth_tokens": revoked_oauth_tokens
201 })),
202 None,
203 None,
204 )
205 .await;
206
207 (
208 StatusCode::OK,
209 Json(serde_json::json!({
210 "success": true
211 })),
212 )
213 .into_response()
214 }
215 Ok(false) => ApiError::DelegationNotFound.into_response(),
216 Err(e) => {
217 tracing::error!("Failed to remove controller: {:?}", e);
218 ApiError::InternalError(Some("Failed to remove controller".into())).into_response()
219 }
220 }
221}
222
223#[derive(Debug, Deserialize)]
224pub struct UpdateControllerScopesInput {
225 pub controller_did: Did,
226 pub granted_scopes: String,
227}
228
229pub async fn update_controller_scopes(
230 State(state): State<AppState>,
231 auth: BearerAuth,
232 Json(input): Json<UpdateControllerScopesInput>,
233) -> Response {
234 if let Err(e) = delegation::scopes::validate_delegation_scopes(&input.granted_scopes) {
235 return ApiError::InvalidScopes(e).into_response();
236 }
237
238 match delegation::update_delegation_scopes(
239 &state.db,
240 &auth.0.did,
241 &input.controller_did,
242 &input.granted_scopes,
243 )
244 .await
245 {
246 Ok(true) => {
247 let _ = delegation::log_delegation_action(
248 &state.db,
249 &auth.0.did,
250 &auth.0.did,
251 Some(&input.controller_did),
252 DelegationActionType::ScopesModified,
253 Some(serde_json::json!({
254 "new_scopes": input.granted_scopes
255 })),
256 None,
257 None,
258 )
259 .await;
260
261 (
262 StatusCode::OK,
263 Json(serde_json::json!({
264 "success": true
265 })),
266 )
267 .into_response()
268 }
269 Ok(false) => ApiError::DelegationNotFound.into_response(),
270 Err(e) => {
271 tracing::error!("Failed to update controller scopes: {:?}", e);
272 ApiError::InternalError(Some("Failed to update controller scopes".into())).into_response()
273 }
274 }
275}
276
277#[derive(Debug, Serialize)]
278#[serde(rename_all = "camelCase")]
279pub struct DelegatedAccountInfo {
280 pub did: Did,
281 pub handle: Handle,
282 pub granted_scopes: String,
283 pub granted_at: chrono::DateTime<chrono::Utc>,
284}
285
286#[derive(Debug, Serialize)]
287pub struct ListControlledAccountsResponse {
288 pub accounts: Vec<DelegatedAccountInfo>,
289}
290
291pub async fn list_controlled_accounts(State(state): State<AppState>, auth: BearerAuth) -> Response {
292 let accounts = match delegation::get_accounts_controlled_by(&state.db, &auth.0.did).await {
293 Ok(a) => a,
294 Err(e) => {
295 tracing::error!("Failed to list controlled accounts: {:?}", e);
296 return ApiError::InternalError(Some("Failed to list controlled accounts".into()))
297 .into_response();
298 }
299 };
300
301 Json(ListControlledAccountsResponse {
302 accounts: accounts
303 .into_iter()
304 .map(|a| DelegatedAccountInfo {
305 did: a.did.into(),
306 handle: a.handle,
307 granted_scopes: a.granted_scopes,
308 granted_at: a.granted_at,
309 })
310 .collect(),
311 })
312 .into_response()
313}
314
315#[derive(Debug, Deserialize)]
316pub struct AuditLogParams {
317 #[serde(default = "default_limit")]
318 pub limit: i64,
319 #[serde(default)]
320 pub offset: i64,
321}
322
323fn default_limit() -> i64 {
324 50
325}
326
327#[derive(Debug, Serialize)]
328#[serde(rename_all = "camelCase")]
329pub struct AuditLogEntry {
330 pub id: String,
331 pub delegated_did: Did,
332 pub actor_did: Did,
333 pub controller_did: Option<Did>,
334 pub action_type: String,
335 pub action_details: Option<serde_json::Value>,
336 pub created_at: chrono::DateTime<chrono::Utc>,
337}
338
339#[derive(Debug, Serialize)]
340pub struct GetAuditLogResponse {
341 pub entries: Vec<AuditLogEntry>,
342 pub total: i64,
343}
344
345pub async fn get_audit_log(
346 State(state): State<AppState>,
347 auth: BearerAuth,
348 Query(params): Query<AuditLogParams>,
349) -> Response {
350 let limit = params.limit.clamp(1, 100);
351 let offset = params.offset.max(0);
352
353 let entries =
354 match delegation::audit::get_audit_log_for_account(&state.db, &auth.0.did, limit, offset)
355 .await
356 {
357 Ok(e) => e,
358 Err(e) => {
359 tracing::error!("Failed to get audit log: {:?}", e);
360 return ApiError::InternalError(Some("Failed to get audit log".into())).into_response();
361 }
362 };
363
364 let total = delegation::audit::count_audit_log_entries(&state.db, &auth.0.did)
365 .await
366 .unwrap_or_default();
367
368 Json(GetAuditLogResponse {
369 entries: entries
370 .into_iter()
371 .map(|e| AuditLogEntry {
372 id: e.id.to_string(),
373 delegated_did: e.delegated_did.into(),
374 actor_did: e.actor_did.into(),
375 controller_did: e.controller_did.map(Into::into),
376 action_type: format!("{:?}", e.action_type),
377 action_details: e.action_details,
378 created_at: e.created_at,
379 })
380 .collect(),
381 total,
382 })
383 .into_response()
384}
385
386#[derive(Debug, Serialize)]
387pub struct ScopePresetInfo {
388 pub name: &'static str,
389 pub label: &'static str,
390 pub description: &'static str,
391 pub scopes: &'static str,
392}
393
394#[derive(Debug, Serialize)]
395pub struct GetScopePresetsResponse {
396 pub presets: Vec<ScopePresetInfo>,
397}
398
399pub async fn get_scope_presets() -> Response {
400 Json(GetScopePresetsResponse {
401 presets: delegation::SCOPE_PRESETS
402 .iter()
403 .map(|p| ScopePresetInfo {
404 name: p.name,
405 label: p.label,
406 description: p.description,
407 scopes: p.scopes,
408 })
409 .collect(),
410 })
411 .into_response()
412}
413
414#[derive(Debug, Deserialize)]
415#[serde(rename_all = "camelCase")]
416pub struct CreateDelegatedAccountInput {
417 pub handle: String,
418 pub email: Option<String>,
419 pub controller_scopes: String,
420 pub invite_code: Option<String>,
421}
422
423#[derive(Debug, Serialize)]
424#[serde(rename_all = "camelCase")]
425pub struct CreateDelegatedAccountResponse {
426 pub did: Did,
427 pub handle: Handle,
428}
429
430pub async fn create_delegated_account(
431 State(state): State<AppState>,
432 headers: HeaderMap,
433 auth: BearerAuth,
434 Json(input): Json<CreateDelegatedAccountInput>,
435) -> Response {
436 let client_ip = extract_client_ip(&headers);
437 if !state
438 .check_rate_limit(RateLimitKind::AccountCreation, &client_ip)
439 .await
440 {
441 warn!(ip = %client_ip, "Delegated account creation rate limit exceeded");
442 return ApiError::RateLimitExceeded(Some(
443 "Too many account creation attempts. Please try again later.".into(),
444 ))
445 .into_response();
446 }
447
448 if let Err(e) = delegation::scopes::validate_delegation_scopes(&input.controller_scopes) {
449 return ApiError::InvalidScopes(e).into_response();
450 }
451
452 match delegation::has_any_controllers(&state.db, &auth.0.did).await {
453 Ok(true) => {
454 return ApiError::InvalidDelegation(
455 "Cannot create delegated accounts from a controlled account".into(),
456 )
457 .into_response();
458 }
459 Err(e) => {
460 tracing::error!("Failed to check controller status: {:?}", e);
461 return ApiError::InternalError(Some("Failed to verify controller status".into()))
462 .into_response();
463 }
464 Ok(false) => {}
465 }
466
467 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
468 let pds_suffix = format!(".{}", hostname);
469
470 let handle = if !input.handle.contains('.') || input.handle.ends_with(&pds_suffix) {
471 let handle_to_validate = if input.handle.ends_with(&pds_suffix) {
472 input
473 .handle
474 .strip_suffix(&pds_suffix)
475 .unwrap_or(&input.handle)
476 } else {
477 &input.handle
478 };
479 match crate::api::validation::validate_short_handle(handle_to_validate) {
480 Ok(h) => format!("{}.{}", h, hostname),
481 Err(e) => {
482 return ApiError::InvalidRequest(e.to_string()).into_response();
483 }
484 }
485 } else {
486 input.handle.to_lowercase()
487 };
488
489 let email = input
490 .email
491 .as_ref()
492 .map(|e| e.trim().to_string())
493 .filter(|e| !e.is_empty());
494 if let Some(ref email) = email
495 && !crate::api::validation::is_valid_email(email)
496 {
497 return ApiError::InvalidEmail.into_response();
498 }
499
500 if let Some(ref code) = input.invite_code {
501 let valid = sqlx::query_scalar!(
502 "SELECT available_uses > 0 AND NOT disabled FROM invite_codes WHERE code = $1",
503 code
504 )
505 .fetch_optional(&state.db)
506 .await
507 .ok()
508 .flatten()
509 .unwrap_or(Some(false));
510
511 if valid != Some(true) {
512 return ApiError::InvalidInviteCode.into_response();
513 }
514 } else {
515 let invite_required = std::env::var("INVITE_CODE_REQUIRED")
516 .map(|v| v == "true" || v == "1")
517 .unwrap_or(false);
518 if invite_required {
519 return ApiError::InviteCodeRequired.into_response();
520 }
521 }
522
523 use k256::ecdsa::SigningKey;
524 use rand::rngs::OsRng;
525
526 let pds_endpoint = format!("https://{}", hostname);
527 let secret_key = k256::SecretKey::random(&mut OsRng);
528 let secret_key_bytes = secret_key.to_bytes().to_vec();
529
530 let signing_key = match SigningKey::from_slice(&secret_key_bytes) {
531 Ok(k) => k,
532 Err(e) => {
533 error!("Error creating signing key: {:?}", e);
534 return ApiError::InternalError(None).into_response();
535 }
536 };
537
538 let rotation_key = std::env::var("PLC_ROTATION_KEY")
539 .unwrap_or_else(|_| crate::plc::signing_key_to_did_key(&signing_key));
540
541 let genesis_result = match crate::plc::create_genesis_operation(
542 &signing_key,
543 &rotation_key,
544 &handle,
545 &pds_endpoint,
546 ) {
547 Ok(r) => r,
548 Err(e) => {
549 error!("Error creating PLC genesis operation: {:?}", e);
550 return ApiError::InternalError(Some("Failed to create PLC operation".into()))
551 .into_response();
552 }
553 };
554
555 let plc_client = crate::plc::PlcClient::with_cache(None, Some(state.cache.clone()));
556 if let Err(e) = plc_client
557 .send_operation(&genesis_result.did, &genesis_result.signed_operation)
558 .await
559 {
560 error!("Failed to submit PLC genesis operation: {:?}", e);
561 return ApiError::UpstreamErrorMsg(format!(
562 "Failed to register DID with PLC directory: {}",
563 e
564 ))
565 .into_response();
566 }
567
568 let did = genesis_result.did;
569 info!(did = %did, handle = %handle, controller = %&auth.0.did, "Created DID for delegated account");
570
571 let mut tx = match state.db.begin().await {
572 Ok(tx) => tx,
573 Err(e) => {
574 error!("Error starting transaction: {:?}", e);
575 return ApiError::InternalError(None).into_response();
576 }
577 };
578
579 let user_insert: Result<(uuid::Uuid,), _> = sqlx::query_as(
580 r#"INSERT INTO users (
581 handle, email, did, password_hash, password_required,
582 account_type, preferred_comms_channel
583 ) VALUES ($1, $2, $3, NULL, FALSE, 'delegated'::account_type, 'email'::comms_channel) RETURNING id"#,
584 )
585 .bind(&handle)
586 .bind(&email)
587 .bind(&did)
588 .fetch_one(&mut *tx)
589 .await;
590
591 let user_id = match user_insert {
592 Ok((id,)) => id,
593 Err(e) => {
594 if let Some(db_err) = e.as_database_error()
595 && db_err.code().as_deref() == Some("23505")
596 {
597 let constraint = db_err.constraint().unwrap_or("");
598 if constraint.contains("handle") {
599 return ApiError::HandleNotAvailable(None).into_response();
600 } else if constraint.contains("email") {
601 return ApiError::EmailTaken.into_response();
602 }
603 }
604 error!("Error inserting user: {:?}", e);
605 return ApiError::InternalError(None).into_response();
606 }
607 };
608
609 let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) {
610 Ok(bytes) => bytes,
611 Err(e) => {
612 error!("Error encrypting signing key: {:?}", e);
613 return ApiError::InternalError(None).into_response();
614 }
615 };
616
617 if let Err(e) = sqlx::query!(
618 "INSERT INTO user_keys (user_id, key_bytes, encryption_version, encrypted_at) VALUES ($1, $2, $3, NOW())",
619 user_id,
620 &encrypted_key_bytes[..],
621 crate::config::ENCRYPTION_VERSION
622 )
623 .execute(&mut *tx)
624 .await
625 {
626 error!("Error inserting user key: {:?}", e);
627 return ApiError::InternalError(None).into_response();
628 }
629
630 if let Err(e) = sqlx::query!(
631 r#"INSERT INTO account_delegations (delegated_did, controller_did, granted_scopes, granted_by)
632 VALUES ($1, $2, $3, $4)"#,
633 did,
634 &auth.0.did,
635 input.controller_scopes,
636 &auth.0.did
637 )
638 .execute(&mut *tx)
639 .await
640 {
641 error!("Error creating initial delegation: {:?}", e);
642 return ApiError::InternalError(None).into_response();
643 }
644
645 let mst = Mst::new(Arc::new(state.block_store.clone()));
646 let mst_root = match mst.persist().await {
647 Ok(c) => c,
648 Err(e) => {
649 error!("Error persisting MST: {:?}", e);
650 return ApiError::InternalError(None).into_response();
651 }
652 };
653 let rev = Tid::now(LimitedU32::MIN);
654 let (commit_bytes, _sig) =
655 match create_signed_commit(&did, mst_root, rev.as_ref(), None, &signing_key) {
656 Ok(result) => result,
657 Err(e) => {
658 error!("Error creating genesis commit: {:?}", e);
659 return ApiError::InternalError(None).into_response();
660 }
661 };
662 let commit_cid: cid::Cid = match state.block_store.put(&commit_bytes).await {
663 Ok(c) => c,
664 Err(e) => {
665 error!("Error saving genesis commit: {:?}", e);
666 return ApiError::InternalError(None).into_response();
667 }
668 };
669 let commit_cid_str = commit_cid.to_string();
670 let rev_str = rev.as_ref().to_string();
671 if let Err(e) = sqlx::query!(
672 "INSERT INTO repos (user_id, repo_root_cid, repo_rev) VALUES ($1, $2, $3)",
673 user_id,
674 commit_cid_str,
675 rev_str
676 )
677 .execute(&mut *tx)
678 .await
679 {
680 error!("Error inserting repo: {:?}", e);
681 return ApiError::InternalError(None).into_response();
682 }
683 let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()];
684 if let Err(e) = sqlx::query!(
685 r#"
686 INSERT INTO user_blocks (user_id, block_cid)
687 SELECT $1, block_cid FROM UNNEST($2::bytea[]) AS t(block_cid)
688 ON CONFLICT (user_id, block_cid) DO NOTHING
689 "#,
690 user_id,
691 &genesis_block_cids
692 )
693 .execute(&mut *tx)
694 .await
695 {
696 error!("Error inserting user_blocks: {:?}", e);
697 return ApiError::InternalError(None).into_response();
698 }
699
700 if let Some(ref code) = input.invite_code {
701 let _ = sqlx::query!(
702 "UPDATE invite_codes SET available_uses = available_uses - 1 WHERE code = $1",
703 code
704 )
705 .execute(&mut *tx)
706 .await;
707
708 let _ = sqlx::query!(
709 "INSERT INTO invite_code_uses (code, used_by_user) VALUES ($1, $2)",
710 code,
711 user_id
712 )
713 .execute(&mut *tx)
714 .await;
715 }
716
717 if let Err(e) = tx.commit().await {
718 error!("Error committing transaction: {:?}", e);
719 return ApiError::InternalError(None).into_response();
720 }
721
722 if let Err(e) =
723 crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle)).await
724 {
725 warn!("Failed to sequence identity event for {}: {}", did, e);
726 }
727 if let Err(e) = crate::api::repo::record::sequence_account_event(&state, &did, true, None).await
728 {
729 warn!("Failed to sequence account event for {}: {}", did, e);
730 }
731
732 let profile_record = json!({
733 "$type": "app.bsky.actor.profile",
734 "displayName": handle
735 });
736 if let Err(e) = crate::api::repo::record::create_record_internal(
737 &state,
738 &did,
739 "app.bsky.actor.profile",
740 "self",
741 &profile_record,
742 )
743 .await
744 {
745 warn!("Failed to create default profile for {}: {}", did, e);
746 }
747
748 let _ = delegation::log_delegation_action(
749 &state.db,
750 &did,
751 &auth.0.did,
752 Some(&auth.0.did),
753 DelegationActionType::GrantCreated,
754 Some(json!({
755 "account_created": true,
756 "granted_scopes": input.controller_scopes
757 })),
758 None,
759 None,
760 )
761 .await;
762
763 info!(did = %did, handle = %handle, controller = %&auth.0.did, "Delegated account created");
764
765 Json(CreateDelegatedAccountResponse { did: did.into(), handle: handle.into() }).into_response()
766}