use crate::state::AppState; use crate::sync::util::{AccountStatus, assert_repo_availability, get_account_with_status}; use axum::{ Json, extract::{Query, State}, http::StatusCode, response::{IntoResponse, Response}, }; use cid::Cid; use jacquard_repo::commit::Commit; use jacquard_repo::storage::BlockStore; use serde::{Deserialize, Serialize}; use serde_json::json; use std::str::FromStr; use tracing::error; async fn get_rev_from_commit(state: &AppState, cid_str: &str) -> Option { let cid = Cid::from_str(cid_str).ok()?; let block = state.block_store.get(&cid).await.ok()??; let commit = Commit::from_cbor(&block).ok()?; Some(commit.rev().to_string()) } #[derive(Deserialize)] pub struct GetLatestCommitParams { pub did: String, } #[derive(Serialize)] pub struct GetLatestCommitOutput { pub cid: String, pub rev: String, } pub async fn get_latest_commit( State(state): State, Query(params): Query, ) -> Response { let did = params.did.trim(); if did.is_empty() { return ( StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidRequest", "message": "did is required"})), ) .into_response(); } let account = match assert_repo_availability(&state.db, did, false).await { Ok(a) => a, Err(e) => return e.into_response(), }; let repo_root_cid = match account.repo_root_cid { Some(cid) => cid, None => { return ( StatusCode::BAD_REQUEST, Json(json!({"error": "RepoNotFound", "message": "Repo not initialized"})), ) .into_response(); } }; let rev = match get_rev_from_commit(&state, &repo_root_cid).await { Some(r) => r, None => { error!( "Failed to parse commit for DID {}: CID {}", did, repo_root_cid ); return ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to read repo commit"})), ) .into_response(); } }; ( StatusCode::OK, Json(GetLatestCommitOutput { cid: repo_root_cid, rev, }), ) .into_response() } #[derive(Deserialize)] pub struct ListReposParams { pub limit: Option, pub cursor: Option, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct RepoInfo { pub did: String, pub head: String, pub rev: String, pub active: bool, #[serde(skip_serializing_if = "Option::is_none")] pub status: Option, } #[derive(Serialize)] pub struct ListReposOutput { #[serde(skip_serializing_if = "Option::is_none")] pub cursor: Option, pub repos: Vec, } pub async fn list_repos( State(state): State, Query(params): Query, ) -> Response { let limit = params.limit.unwrap_or(50).clamp(1, 1000); let cursor_did = params.cursor.as_deref().unwrap_or(""); let result = sqlx::query!( r#" SELECT u.did, u.deactivated_at, u.takedown_ref, r.repo_root_cid, r.repo_rev FROM repos r JOIN users u ON r.user_id = u.id WHERE u.did > $1 ORDER BY u.did ASC LIMIT $2 "#, cursor_did, limit + 1 ) .fetch_all(&state.db) .await; match result { Ok(rows) => { let has_more = rows.len() as i64 > limit; let mut repos: Vec = Vec::new(); for row in rows.iter().take(limit as usize) { let rev = match get_rev_from_commit(&state, &row.repo_root_cid).await { Some(r) => r, None => { if let Some(ref stored_rev) = row.repo_rev { stored_rev.clone() } else { tracing::warn!( "Failed to parse commit for DID {} in list_repos: CID {}", row.did, row.repo_root_cid ); continue; } } }; let status = if row.takedown_ref.is_some() { AccountStatus::Takendown } else if row.deactivated_at.is_some() { AccountStatus::Deactivated } else { AccountStatus::Active }; repos.push(RepoInfo { did: row.did.clone(), head: row.repo_root_cid.clone(), rev, active: status.is_active(), status: status.as_str().map(String::from), }); } let next_cursor = if has_more { repos.last().map(|r| r.did.clone()) } else { None }; ( StatusCode::OK, Json(ListReposOutput { cursor: next_cursor, repos, }), ) .into_response() } Err(e) => { error!("DB error in list_repos: {:?}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"})), ) .into_response() } } } #[derive(Deserialize)] pub struct GetRepoStatusParams { pub did: String, } #[derive(Serialize)] pub struct GetRepoStatusOutput { pub did: String, pub active: bool, #[serde(skip_serializing_if = "Option::is_none")] pub status: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rev: Option, } pub async fn get_repo_status( State(state): State, Query(params): Query, ) -> Response { let did = params.did.trim(); if did.is_empty() { return ( StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidRequest", "message": "did is required"})), ) .into_response(); } let account = match get_account_with_status(&state.db, did).await { Ok(Some(a)) => a, Ok(None) => { return ( StatusCode::BAD_REQUEST, Json(json!({"error": "RepoNotFound", "message": format!("Could not find repo for DID: {}", did)})), ) .into_response() } Err(e) => { error!("DB error in get_repo_status: {:?}", e); return ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"})), ) .into_response(); } }; let rev = if account.status.is_active() { if let Some(ref cid) = account.repo_root_cid { get_rev_from_commit(&state, cid).await } else { None } } else { None }; ( StatusCode::OK, Json(GetRepoStatusOutput { did: account.did, active: account.status.is_active(), status: account.status.as_str().map(String::from), rev, }), ) .into_response() }